- Create a new Rust Project
- Rust Code
- Execute Function using rundll32
- Function Declaration Note
- DllMain
- Checking Exported Functions in DLLs
- Building for 32-bit
Create a new Rust Project
To start we’ll create a new folder and initialize it with --lib to create a package with a library target (src/lib.rs).
Next we’ll add the crate-type field in the [lib] section of cargo.toml.
1
2
3
4
5
6
7
8
9
| [package]
name = "dll_test"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
|
cdylib means a dynamic system library will be produced. This is used when compiling a dynamic library to be loaded from another language.- This output type will create
*.so files on Linux, *.dylib files on macOS, and *.dll files on Windows.
Rust Code
Next we’ll create a new function MyTestFunction that we want to be able to call.
1
2
3
4
5
6
7
| #[unsafe(no_mangle)]
pub fn MyTestFunction() {
let file_name = "C:\\Temp\\Rust DLL output file.txt";
let data = "Test file written using my Rust DLL";
let _ = std::fs::write(file_name, data);
}
|
unsafe is required for no_mangle in newer versions of Rust (2024 edition I believe).
In the target/release folder we can see our compiled DLL dll_test.dll.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Directory of C:\Rust\dll_test\target\release
28/04/2026 07:28 AM <DIR> .
28/04/2026 07:28 AM <DIR> ..
28/04/2026 07:28 AM 0 .cargo-lock
28/04/2026 07:28 AM <DIR> .fingerprint
28/04/2026 07:28 AM <DIR> build
28/04/2026 07:28 AM <DIR> deps
28/04/2026 07:28 AM 126 dll_test.d
28/04/2026 07:28 AM 129,024 dll_test.dll
28/04/2026 07:28 AM 883 dll_test.dll.exp
28/04/2026 07:28 AM 1,744 dll_test.dll.lib
28/04/2026 07:28 AM 1,200,128 dll_test.pdb
28/04/2026 07:28 AM <DIR> examples
28/04/2026 07:28 AM <DIR> incremental
6 File(s) 1,331,905 bytes
7 Dir(s) 1,637,719,965,696 bytes free
|
Execute Function using rundll32
We’ll copy dll_test.dll to C:\Temp then execute our custom function using rundll32.
1
| C:\Temp>rundll32 dll_test.dll,MyTestFunction
|
Checking the current directory we can see the file our DLL function made.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| C:\Temp>dir
Volume in drive C has no label.
Volume Serial Number is XXXX-XXXX
Directory of C:\Temp
28/04/2026 07:34 AM <DIR> .
28/04/2026 07:34 AM <DIR> ..
28/04/2026 07:34 AM 133,120 dll_test.dll
28/04/2026 07:34 AM 35 Rust DLL output file.txt
2 File(s) 133,155 bytes
2 Dir(s) 1,640,106,426,368 bytes free
C:\Temp>type "Rust DLL output file.txt"
Test file written using my Rust DLL
|
Function Declaration Note
From other sources I’ve seen you might need to use the one of the below function definitions if you are having issues with crashes due to unexpected parameters.
- I didn’t have any issues when using
pub fn MyTestFunction, but I only tested the DLL using rundll32, so other applications might not be as forgiving.
1
2
3
| pub extern "C" fn MyTestFunction()
pub extern "system" fn MyTestFunction()
pub extern "system" fn MyTestFunction(_hwnd: *mut std::ffi::c_void, _hinst: *mut std::ffi::c_void, _lp_cmd_line: *mut u8, _n_cmd_show: i32)
|
DllMain
Let’s look how to implement the function DllMain for our DLL. As per the Microsoft documentation:
- An optional entry point into a dynamic-link library (DLL). When the system starts or terminates a process or thread, it calls the entry-point function for each loaded DLL using the first thread of the process.
Something to be aware of, Microsoft states there is a bunch of things you shouldn’t do in this function. See link below:
The function signature for DllMain is as follows:
1
2
3
4
5
| BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved // reserved
)
|
We’ll convert this to Rust using the following code:
1
2
3
4
5
6
7
| use core::ffi::c_void;
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(_hinst: *mut c_void, _fdw_reason: u32, _lpv_reserved: *mut c_void) -> bool {
return true;
}
|
Let’s make our function actually do something to test it out. We’ll add the following features to our code:
- Get the current timestamp in microseconds.
- Write a file with the current timestamp and call reason (PROCESS_ATTACH, DETACH, etc) as the file name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| use std::time::{SystemTime, UNIX_EPOCH};
use core::ffi::c_void;
#[unsafe(no_mangle)]
pub extern "system" fn DllMain(_hinst: *mut c_void, _fdw_reason: u32, _lpv_reserved: *mut c_void) -> bool {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros();
let reason = if _fdw_reason == 1 {
"DLL_PROCESS_ATTACH"
} else if _fdw_reason == 0 {
"DLL_PROCESS_DETACH"
} else if _fdw_reason == 2 {
"DLL_THREAD_ATTACH"
} else if _fdw_reason == 3 {
"DLL_THREAD_DETACH"
} else {
"UNKNOWN"
};
let file_name = format!("C:\\Temp\\{} DLLMain - {}.txt", timestamp, reason);
let _ = std::fs::write(file_name, "");
return true;
}
#[unsafe(no_mangle)]
pub fn TestFunction() {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros();
let file_name = format!("C:\\Temp\\{} TestFunction.txt", timestamp);
let _ = std::fs::write(file_name, "");
}
|
If we build our code then execute our function TestFunction using rundll32, we can see that DllMain was automatically called twice. Once for DLL_PROCESS_ATTACH and another for DLL_PROCESS_DETACH. Note the order the functions were called in as well DLLMain (ProcessAttach), TestFunction, DLLMain (ProcessDetach).
1
2
3
4
5
6
7
8
9
| Directory of C:\Temp
02/05/2026 04:50 PM <DIR> .
02/05/2026 04:50 PM <DIR> ..
02/05/2026 04:50 PM 0 1777704632568747 DLLMain - DLL_PROCESS_ATTACH.txt
02/05/2026 04:50 PM 0 1777704632576558 TestFunction.txt
02/05/2026 04:50 PM 0 1777704632578194 DLLMain - DLL_PROCESS_DETACH.txt
3 File(s) 0 bytes
2 Dir(s) 1,637,991,088,128 bytes free
|
Checking Exported Functions in DLLs
dumpbin
This program should be installed if you have Visual Studio installed. However, it very likely won’t be in your PATH. Check your Visual Studio files:
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64
1
| dumpbin.exe /exports C:\Temp\dll_test.dll
|
1
2
3
4
5
6
7
8
9
10
11
12
| Dump of file C:\Temp\dll_test.dll
File Type: DLL
Section contains the following exports for dll_test.dll
...
ordinal hint RVA name
1 0 00001000 DllMain
2 1 000012B0 TestFunction
|
rabin2
-E means show globally exportable symbols
1
2
3
4
5
| [Exports]
nth paddr vaddr bind type size lib name demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 0x00000400 0x180001000 GLOBAL FUNC 0 dll_test.dll DllMain
2 0x000006b0 0x1800012b0 GLOBAL FUNC 0 dll_test.dll TestFunction
|
objdump
1
| objdump -x dll_test.dll | grep -A 30 "Export Tables"
|
-x means display the contents of all headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| The Export Tables (interpreted .rdata section contents)
Export Flags 0
Time/Date stamp ffffffff
Major/Minor 0/0
Name 000000000001e5ec dll_test.dll
Ordinal Base 1
Number in:
Export Address Table 00000002
[Name Pointer/Ordinal] Table 00000002
Table Addresses
Export Address Table 000000000001e5d8
Name Pointer Table 000000000001e5e0
Ordinal Table 000000000001e5e8
Export Address Table -- Ordinal Base 1
[ 0] +base[ 1] 1000 Export RVA
[ 1] +base[ 2] 12b0 Export RVA
[Ordinal/Name Pointer] Table
[ 0] DllMain
[ 1] TestFunction
The Function Table (interpreted .pdata section contents)
vma: BeginAddress EndAddress UnwindData
0000000180020000: 0000000180001000 00000001800011df 000000018001c8c0
...
|
Building for 32-bit
Add the 32-bit Windows target for rust.
1
| rustup target add i686-pc-windows-msvc
|
Build a 32-bit binary.
1
| cargo build --release --target i686-pc-windows-msvc
|
Confirm 32-bit Build
1
| objdump -x dll_test.dll | grep "Magic"
|