7.4 KiB
Open printer_driver_adapter.dll
A drop-in replacement for RustDesk's closed printer adapter, so remote printing works in custom-branded builds.
Why this exists
RustDesk's printer_driver_adapter.dll verifies the calling executable's
Authenticode signature inside init() and refuses to run for anything not
signed by RustDesk. Evidence from the shipped binary:
- imports
WinVerifyTrust(wintrust.dll),CertGetNameStringW,GetModuleFileNameW - links
codesign-verify-rs, a crate whose only purpose is Authenticode checks - carries one hardcoded identity constant:
PURSLANEHUABINGRUSTDESKfollowed by two SHA-256 digests. Purslane Ltd is RustDesk's company; Huabing is the founder.
An unsigned custom build fails with:
ERROR [src\server.rs:160] printer service init failed: Failed to init printer driver
which is the fn_init() != 0 branch. Signing with your own certificate does
not help — the check is an allow-list of specific identities, not "is it signed".
Everything else in RustDesk's printing chain is open source and works. Only the capture shim is gated, and it is replaceable: this crate reimplements the same four-function ABI with no signature check.
The ABI
From src/server/printer_service.rs. Symbols are resolved by these exact
undecorated names, so all exports are #[no_mangle] extern "C":
pub type Init = fn(tag_name: *const i8) -> i32; // 0 = success
pub type Uninit = fn();
pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32);
pub type FreePrnData = fn(data: *mut i8);
Verified: this DLL's PE export table is byte-for-byte the same set of names as
RustDesk's own (init, uninit, get_prn_data, free_prn_data).
How capture works
No custom print driver, therefore no WHQL signing:
print job
│
▼
virtual printer ──► Local Port whose NAME IS A FILE PATH
(signed driver) %ProgramData%\<Tag>\printer-spool\job.prn
│
▼
this adapter polls the directory,
opens each file with share_mode = 0
(fails while the spooler still holds it),
returns the bytes, deletes the file
│
▼
printer_service::run() ──► on_printer_data()
│
▼
existing open-source transport → controller prints
A Local Port whose name is a full file path makes the spooler write output straight to that file with no "save as" prompt. The exclusive-open test is how job completion is detected — no timers, no guessing.
init() clears the spool directory, so a stale job can never be replayed onto
the next connection, and any file present afterwards is by definition new.
Build
cargo build --release
Output: target\release\printer_driver_adapter.dll (~140 KB, no dependencies —
std only).
How the build wires it in
DVForge does all of this automatically for Windows builds — nothing here needs to be run by hand.
builder/orchestrator.py→_install_open_printer_adapter()- cargo-builds this crate and copies the DLL into the Flutter
Release/folder, overwriting the signature-checked one that_ensure_windows_printer_driver()downloaded. It runs before the MSI harvest and the portable packer, so the replacement ships inside both installers. builder/customize.py→_apply_printer_port()- repoints the printer's port at the spool file. Two implementations exist
and both are patched, because patching only one leaves the printer on the
wrong port:
libs/remote_printer/src/lib.rs— Rust, used by--install-remote-printerand the in-app Settings button.res/msi/CustomActions/RemotePrinter.cpp— a full C++ reimplementation used by the MSI'sInstallPrintercustom action. This is the one that runs during a normal installer run.
Only the port changes. The printer keeps its name, and the driver stays
RustDesk v4 Printer Driver.
It runs after _apply_appname(), since both files have the app name substituted
into them.
Verifying a build
After installing, confirm the adapter loaded:
Get-ChildItem "C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\<App>\log" `
-Recurse -Filter *.log | Select-String "printer service"
Expect printer service initialized. The old failure reads
printer service init failed: Failed to init printer driver.
Confirm the port is the spool file, not a named port:
Get-Printer -Name "<App> Printer" | Select-Object Name,DriverName,PortName
PortName should be C:\ProgramData\<App>\printer-spool\job.prn.
Then connect from another machine, print on the controlled one, and check
%ProgramData%\<App>\printer-spool\adapter.log for a captured line and the
server log for Got prn data, data len:.
Which driver to render with
The controller prints received bytes via PrintXPSRawData()
(src/platform/windows.rs:4261), so the payload must be a valid XPS package.
RustDesk v4 Printer Driver— already installed if you ever ran the app's printer install, and it declaresXpsFormat=XPS(MS-XPS), which is exactly what the receiving end expects. Note its render filter is signed by Microsoft ("Windows Hardware Compatibility Publisher"), not RustDesk — the signature gate is only in the adapter, so this driver has no objection to who calls it. Best format match. Check redistribution terms before shipping it yourself.Microsoft XPS Document Writer v4— inbox on every Windows machine and unambiguously redistributable, but emits OpenXPS (.oxps), not MS-XPS. May need conversion beforePrintXPSRawDataaccepts it.
Resolved 2026-08-19 by inspecting a real captured job. A test print through
Microsoft XPS Document Writer v4 produced a valid OPC package whose root
relationship is:
Type="http://schemas.openxps.org/oxps/v1.0/fixedrepresentation"
That is OpenXPS, not MS-XPS — confirming the mismatch. Use
RustDesk v4 Printer Driver instead, which declares XpsFormat=XPS. If you must
stay on the inbox driver, the fallback is converting OXPS to MS-XPS, or swapping
send_raw_data_to_printer on the receiving end — which is your own code.
PrintXPSRawData writes diagnostics to C:\Windows\temp\test_rustdesk.log on
failure; check there if a job arrives but does not print.
Status
| Piece | State |
|---|---|
| Four-function ABI, exports match upstream | verified |
| Capture, completion detection, cleanup | verified by test suite |
| Panic safety across the FFI boundary | catch_unwind on every export |
| Loads and initialises inside the real server | not yet run |
| Spooler renders a job into the file port | verified — 37 KB job captured |
| End-to-end print to a remote machine | not yet run |
| XPS vs OpenXPS payload | resolved — inbox v4 driver emits OpenXPS, use RustDesk's driver |
Notes
panic = "abort" is deliberately not set. A panic escaping a cdylib into the
RustDesk service would take the whole service down, so every export wraps its body
in catch_unwind instead.
Licensing: this is clean-room work against an ABI declared in RustDesk's own AGPL-3.0 source. It contains no RustDesk code and defeats no protection on their binary — it replaces it.