Search code examples
c++rustffi

How to implement the RVExtension function for an ArmA 3 DLL in Rust?


I'm trying to write a DLL extension for ArmA 3 and the game docs say:

The dll is expected to contain an entry point of a form _RVExtension@12, with a following C signature:

void __stdcall RVExtension(char *output, int outputSize, const char *function);

A part of C++ code example is:

// ...

extern "C" {
    __declspec(dllexport) void __stdcall RVExtension(
        char *output,
        int outputSize,
        const char *function
    ); 
};

void __stdcall RVExtension(
    char *output,
    int outputSize,
    const char *function
) {
    outputSize -= 1;
    strncpy(output,function,outputSize);
}

The docs also have plenty of examples in other languages like: C#, D and even Pascal, but those don't help me much, because I don't have a good understanding their FFI =(.

I'm stuck with the following Rust code:

#[no_mangle]
pub extern "stdcall" fn RVExtension(
    game_output: *mut c_char,
    output_size: c_int,
    game_input: *const c_char
) {
    // ...
}

But ArmA refuses to call it.


Solution

  • Thanks to @Shepmaster's advice about Dependency Walker, I was able to discover that the problem was in the function's name mangling. I expected the function name would be transformed into _name@X, but it wasn't. RVExtension was exported literally, and ArmA wasn't able to find it by the name _RVExtension@12.

    It is strange, but it seems like the compiler version may play a part. I tried ~8 different versions, and was able to make it work only with Rust nightly 1.8 (GNU ABI) 32-bit.

    The working code is:

    #![feature(libc)]
    extern crate libc;
    
    use libc::{strncpy, size_t};
    
    use std::os::raw::c_char;
    use std::ffi::{CString, CStr};
    use std::str;
    
    #[allow(non_snake_case)]
    #[no_mangle]
    /// copy the input to the output
    pub extern "stdcall" fn _RVExtension(
        response_ptr: *mut c_char,
        response_size: size_t,
        request_ptr: *const c_char,
    ) {
        // get str from arma
        let utf8_arr: &[u8] = unsafe { CStr::from_ptr(request_ptr).to_bytes() };
        let request: &str = str::from_utf8(utf8_arr).unwrap();
    
        // send str to arma
        let response: *const c_char = CString::new(request).unwrap().as_ptr();
        unsafe { strncpy(response_ptr, response, response_size) };
    }
    

    It is also possible to rewrite the function into:

    #[export_name="_RVExtension"]
    pub extern "stdcall" fn RVExtension(
    

    Some other Rust compilers may also work with:

    #[export_name="_RVExtension@12"]
    pub extern "stdcall" fn RVExtension(
    

    But, for example, nightly 1.8 (MSVC ABI) 32-bit with VS 2015 will not allow @ symbol and throws an error at compilation time. The MSVC version will not add @12 by itself.

    Other compilers may add @12 and the function will be exported as _RVExtension@12@12.


    It's also worth mentioning that ArmA is 32-bit app, so it does not work with a 64-bit DLL.