Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions rust/minimal-example/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[target.i686-pc-windows-msvc]
rustflags = [
# Allows us to export the correct stdcall symbol names for Windows 32-bit binaries.
# Rust has no way to explicitly export a symbol named "_RVExtension@12", it cuts off the @12.
# This overrides the linker's /DEF argument, to force it to export the symbols we want.
"-Clink-arg=/DEF:Win32.def",
# Generate a map file so we can see what symbols exist, and what we're exporting.
"-Clink-arg=/MAP:SymbolInfo.map",
# Add exported symbol info to the map file
"-Clink-arg=/MAPINFO:EXPORTS"
]
2 changes: 2 additions & 0 deletions rust/minimal-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
13 changes: 13 additions & 0 deletions rust/minimal-example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "test_extension"
version = "0.1.0"
authors = ["Spoffy"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type=["cdylib"]

[dependencies]

67 changes: 67 additions & 0 deletions rust/minimal-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Readme

This project is an example of how to create a cross-platform Arma extension in Rust.
The extension has no dependencies except on the core Rust libraries, and builds into a single .dll.

The extension will run on Linux and Windows, both 32-bit and 64-bit.

It supports:
* RVExtensionVersion
* RVExtension
* RVExtensionArgs
* RVExtensionRegisterCallback

## Author

Spoffy (https://github.com/Spoffy/Rust-Arma-Extension-Example)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, I'll add it tomorrow.

## Motivation

Rust is a memory-safe, fast language that, while it has a bit of a learning curve, is much less prone to weird bugs in production than C and C++ (in my opinion).
It has the added advantage that its standard library is cross-platform, so it's possible to easily build Windows and Linux compatible extensions - particularly when utilising things such as file access and networking.

## Installation

You'll need to install an appropriate rust toolchain. It's recommended to use `rustup` for this on both Windows and Linux, as you'll likely be needing to cross-compile. Also, it's really easy.

Once you have a toolchain installed, clone this package, then execute `cargo build`, to build the library as appropriate for your platform.

If you're running on 64-bit Windows or Linux, you'll need to rename the .dll or .so to end in `\_x64.(so/dll)`. It's also recommended (but not required) to remove the 'lib' prefix on Linux, as it's not needed.

You then need to add your .dll or .so to the Arma server's root directory (same as where the server binary is), or into an addon. (mods/@MyExtension/MyThing.dll). I've personally found the root directory to be more reliable for testing purposes.

## 32-bit Linux Compilation (Cross-Compiled)

Most Linux servers these days are 64-bit. However, the Arma Linux server is typically 32-bit. As such, you'll generally need to cross-compile.

If you're using `rustup` to manage toolchains, you can just do `rustup target add i686-unknown-linux-gnu`, then build the .so using `cargo build --target=i686-unknown-linux-gnu`.

## 32-bit Windows Compilation (Cross-Compiled)

Firstly, you'll need to make sure you have a 32-bit toolchain. I recommend using `rustup`, so you can run `rustup target add i686-pc-windows-msvc`.

To cross-compile a 32-bit Windows library on 64-bit windows, simply type `cargo build --target=i686-pc-windows-msvc`.

Only the MSVC toolchain is supported for 32-bit Windows compilation, due to the nature of the hacks needed to make it work (see .cargo/config for specifics).

### 32-bit Compilation Workarounds

**This is an implementation detail. If you just want to build the example, you do not need to read this section**.

Broadly speaking, Windows uses "stdcall" calling conventions to call functions in the DLL. In 32-bit Windows, the symbol names for these functions are 'decorated', meaning they look something like this: `_RVExtension@12`. The @12 being the number of bytes of parameters the functions take.

As it happens, in the rust compiler's infinite wisdom, it generates a '.def' file for the MSVC toolchain linker, that describes the exported symbols. Unfortunately for us, there's no way to make it export the decorated functions (that I've been able to find).

Instead, we override the .def generated by Rust with our own file `Win32.def`, which exports the symbols with the correct names.

This means we don't transitively export any dependencies that our Rust code imports, which is a bit of a shame, but it doesn't seem to break anything.
This also is why the GCC toolchain is Windows is *not* supported. If someone wants to explore that possibility, feel free!

## Alternatives

This is meant more as an example piece of code, than something to be used in production (though it's entirely possible to use this in production).

As such, it isn't a particularly *nice* interface between Arma and Rust - it's pretty much the bare minimum to create a safe implementation.

Synixebrett has created a much more fully-featured Rust-Arma interface using some rather fancy Rust macros. If you're looking for something that hides away most of the implementation for you, it's probably worth checking out: https://github.com/synixebrett/arma-rs

6 changes: 6 additions & 0 deletions rust/minimal-example/Win32.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
LIBRARY
EXPORTS
_RVExtensionVersion@8
_RVExtension@12
_RVExtensionArgs@20

114 changes: 114 additions & 0 deletions rust/minimal-example/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use std::os::raw::c_char;
use std::ffi::{CString, CStr};
use std::str;
use std::cmp;

type size_t = usize;

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn RVExtensionVersion(output_ptr: *mut i8, output_size: size_t) {
unsafe { write_str_to_ptr("Test Extension v.1.00", output_ptr, output_size) };
}

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn RVExtension(
//Use a u8 here to make copying into the response easier. Fundamentally the same as a c_char.
response_ptr: *mut c_char,
response_size: size_t,
request_ptr: *const c_char,
) {
// get str from arma
let request: &str = {unsafe { CStr::from_ptr(request_ptr) }}.to_str().unwrap();

// send str to arma
unsafe { write_str_to_ptr(request, response_ptr, response_size) };
}

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn RVExtensionArgs(
response_ptr: *mut c_char,
response_size: size_t,
function_name_ptr: *const c_char,
args_ptr: *const *const c_char,
argsCount: i32
) {
let function_name: &str = {unsafe { CStr::from_ptr(function_name_ptr) }}.to_str().unwrap();
let mapped_args = unsafe {
//This is a safe cast, as long as argsCount isn't negative, which it never should be. Even so, debug_assert it.
debug_assert!(argsCount >= 0);
let arg_ptrs = std::slice::from_raw_parts(args_ptr, argsCount as usize);
arg_ptrs.iter()
.map(|&ptr| CStr::from_ptr(ptr))
.map(|cstr| cstr.to_str())
};

//Args here
let args: Vec<&str> = match(mapped_args.collect()) {
Ok(args) => args,
Err(_) => return
};

//Handle here
let response_message = "Test Response";
unsafe { write_str_to_ptr(response_message, response_ptr, response_size) };
}

type ArmaCallback = extern fn(*const c_char, *const c_char, *const c_char) -> i32;

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn RVExtensionRegisterCallback(
callback: ArmaCallback
) {
let name = "Test Extension";
let func = "Test Function";
let data = "Test Data";

call_extension_callback(callback, name, func, data);
}

/// A safer-way to call an extension callback.
/// Verifies input is ASCII, and turns &str into null-byte terminated C strings.
pub fn call_extension_callback(callback: ArmaCallback, name: &str, func: &str, data: &str) -> Option<()> {
if !(name.is_ascii() && func.is_ascii() && data.is_ascii()) {return None};

//Verify we created all of the CStrings successfully.
let cstr_name = CString::new(name).ok()?;
let cstr_func = CString::new(func).ok()?;
let cstr_data = CString::new(data).ok()?;

//into_raw() releases ownership of them. Arma becomes responsible for cleaning the strings up.
callback(cstr_name.into_raw(), cstr_func.into_raw(), cstr_data.into_raw());
Some(())
}

/// Copies an ASCII rust string into a memory buffer as a C string.
/// Performs necessary validation, including:
/// * Ensuring the string is ASCII
/// * Ensuring the string has no null bytes except at the end
/// * Making sure string length doesn't exceed the buffer.
/// # Returns
/// :Option with the number of ASCII characters written - *excludes the C null terminator*
unsafe fn write_str_to_ptr(string: &str, ptr: *mut c_char, buf_size: size_t) -> Option<usize> {
//We shouldn't encode non-ascii string as C strings, things will get weird. Better to abort, I think.
if !string.is_ascii() {return None};
//This should never fail, honestly - we'd have to have manually added null bytes or something.
let cstr = CString::new(string).ok()?;
let cstr_bytes = cstr.as_bytes();
//C Strings end in null bytes. We want to make sure we always write a valid string.
//So we want to be able to always write a null byte at the end.
let amount_to_copy = cmp::min(cstr_bytes.len(), buf_size - 1);
//We provide a guarantee to our unsafe code, that we'll never pass anything too large.
//In reality, I can't see this ever happening.
if amount_to_copy > isize::MAX as usize {return None}
//We'll never copy the whole string here - it will always be missing the null byte.
ptr.copy_from(cstr.as_ptr(), amount_to_copy);
//strncpy(ptr, cstr.as_ptr(), amount_to_copy);
//Add our null byte at the end
ptr.add(amount_to_copy).write(0x00);
Some(amount_to_copy)
}