Post

Creating a DLL in Rust

How to create a DLL using Rust and execute the custom DLL function(s) using rundll32.

  1. Create a new Rust Project
  2. Rust Code
  3. Execute Function using rundll32
  4. Function Declaration Note
  5. DllMain
  6. Checking Exported Functions in DLLs
  7. 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).

1
cargo init --lib .

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).
1
cargo build --release

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

1
rabin2 -E dll_test.dll
  • -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"
1
Magic   010b   (PE32)
This post is licensed under CC BY 4.0 by the author.