Rust で手作業で DLL の動的ロードをする (Windows)

はじめに

DLL を自前でロードする必要が出てきたのでやり方をまとめました。

  • 目的の API へのアクセスに #link 属性は使わない
  • win32-rs 等は使わない

要点

DLL ロードに必要な API だけ #link 属性で取得します。

use std::ffi::c_void;
type Error = u32;

#[link(name = "kernel32")]
#[no_mangle]
extern "stdcall" {
    fn GetLastError() -> Error;
    fn LoadLibraryExW(lpLibFileName: *const u16, hFile: *const c_void, dwFlags: u32) -> *const c_void;
    fn FreeLibrary(hLibModule: *const c_void) -> i32;
    fn GetProcAddress(hModule: *const c_void, lpProcName: *const u8) -> *const c_void;
}

Win32 の W 系 APIUTF-16 文字列なので、変換する関数を用意します。

trait IntoNullTerminatedU16 {
    fn to_nullterminated_u16(&self) -> Vec<u16>;
}

impl IntoNullTerminatedU16 for str {
    //  略
}

Win32 API の多くは GetLastError でエラー取得をします。のでそれに対応した関数も用意します。

trait ToResult: Sized {
    fn to_result(&self) -> Result<Self, Error>;
}

impl ToResult for *const c_void {
    //  略
}

この組み合わせで LoadLibrary は次のように行いました。

let h = LoadLibraryExW("user32.dll".to_nullterminated_u16().as_ptr(),
                        ptr::null(),
                        0x800).to_result().unwrap();

API の関数ポインタを Rust の関数ポインタに変換するには次のようにします。 Rust → FFI の関数ポインタ (生ポインタ) への変換は as でできますが、逆は transmute で行います。

type FnMessageBox = extern "stdcall" fn(hWnd: *const c_void, lpText: *const u16, lpCaption: *const u16, uType: u32) -> i32;

let p = GetProcAddress(h, "MessageBoxW\0".as_ptr()).to_result().unwrap();
let fn_message_box = std::mem::transmute::<_, FnMessageBox>(p);

fn_message_box(ptr::null(),
                "Hello, Rust!".to_nullterminated_u16().as_ptr(),
                "MessageBox".to_nullterminated_u16().as_ptr(),
                0);

コード

gist485b3a3703f95bccac5d1503e55db8b4

おわりに

やってる事は簡単なのですが、 Rust の作法がいま一つよくわかってなくてこれでよいのか自信がないのでつっこみをお待ちしてます。