-
Notifications
You must be signed in to change notification settings - Fork 3
Adds Minimal Rust Example #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Spoffy
wants to merge
2
commits into
arma3:master
Choose a base branch
from
Spoffy:rust-example
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /target | ||
| Cargo.lock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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] | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
|
|
||
| ## 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 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| LIBRARY | ||
| EXPORTS | ||
| _RVExtensionVersion@8 | ||
| _RVExtension@12 | ||
| _RVExtensionArgs@20 | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
|
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please insert a
How to setupsection (see https://github.com/Spoffy/RV-Extension-Examples/tree/rust-example/cs/net%20core#how-to-setup for reference).There was a problem hiding this comment.
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.