FFI, as defined by Wikipedia, represents a way in which one language can call methods in another language. FFI use
- Improve the efficiency of programs, for example, cpu-sensitive parts of Python can be written in C
- Functions that call libraries written in other languages, such as TensorFlow written in C++ but exposing C interfaces to use in other languages
Writing an FFI interface to Rust is not difficult, but there are some challenges and complications. One of the most troubling is the need to handle Pointers in the unsafe block, which goes beyond Rust’s memory-safe model. This article is simply about the experience of writing battery-FFI.
configuration
The first step is to add liBC’s dependencies to Cargo. Toml. Libc provides definitions for all interactions with C. Then change crate-type to cdylib, which will compile to dynamic libraries (so,dylib, DLL) depending on your system. Rust is packaged as rlib by default.
[dependencies]
libc = "*"
[lib]
crate-type = ["cdylib"]
Copy the code
FFI grammar
The following is an example of a method that returns a percentage of the Battery from the Battery structure.
#[no_mangle]
pub unsafe extern fn battery_get_percentage(ptr: *const Battery) -> libc::c_float {
unimplemented!(a)// Example below will contain the full function
}
Copy the code
The opening #[no_mangle] indicates the name of the forbidden method, Simple terms is to make the other languages can be found through battery_get_percentage the method called rather than by the compiler to generate a similar _ZN7battery_get_percentage17h5179a29d7b114f74E function name. And then the two keywords unsafe,extern.
unsafe
The keyword indicates that the function has UB behavior such as null Pointers.extern
The keyword indicates that the method complies with C’s calling convention.
The return value
In this case, Rust structures are exposed, but since Rust structures may contain some of Rust’s most complex structures, such as Mutex, which cannot be handled in C, we return only a pointer and all other operations are handled through the interface provided by the Rust library. The returned type must be allocated on the heap, so Box is used, and native types such as U8 can be returned directly.
#[no_mangle]
pub extern fn battery_manager_new() - > *mut Manager {
let manager: Manager = Manager::new();
let boxed: Box<Manager> = Box::new(manager);
Box::into_raw(boxed);
}
Copy the code
The ginseng
The following method interface takes a pointer to Manager and calls its iter method to return a Battery structure.
#[no_mangle]
pub unsafe extern fn battery_manager_iter(ptr: *mut Manager) -> *mut Batteries {
assert!(! ptr.is_null());let manager = &*ptr;
Box::into_raw(Box::new(manager.iter()));
}
Copy the code
First we use Assert! (! ptr.is_null()); To check if the argument is NULL, which is required for all passed pointer arguments. Next we create a reference to the Manager using &* PTR.
Destroy the pointer
Mem ::forget is automatically called when Box::into_raw() is called, which means Rust does not destruct the memory automatically, so we also need to provide a method to handle the returned pointer to prevent memory leaks.
#[no_mangle]
pub unsafe extern fn battery_manager_free(ptr: *mut Manager) {
if ptr.is_null() {
return;
}
Box::from_raw(ptr);
}
Copy the code
Expose the interface used
The main function of the battery library is to provide the resource information of the battery used by the laptop, so we need to provide the GET method to return some methods of the previous battery structure, for example:
#[no_mangle]
pub unsafe extern fn battery_get_energy(ptr: *const Battery) -> libc::uinit32_t {
assert!(! ptr.is_null());let battery = &* ptr;
battery.energy();
}
Copy the code
Treatment Option
There are some Battery methods that return Option
, which is not defined in the C ABI, and T cannot return NULL directly because it might not be a pointer. There are generally three solutions to this situation:
- Returns values that are unlikely to occur, such as -1.
- Create a thread local value, usually called
errno
, provides a method to get last Error - Create a structure like this and check it each time you return
present == true
.
#[repr(C)]
struct COption<T> {
value: T,
present: bool
}
Copy the code
Handling strings
The string of Rust and the string of C are two completely different types and cannot be easily converted from one to the other. Rust provides CString and CStr to interact with strings in C. In the following example, battery.serial_number() returns an Option<& STR >, which is converted to CString when Some is returned, or NULL if None.
#[no_mangle]
pub unsafe extern fn battery_get_serial_number(ptr: *const Battery) -> *mut libc::c_char {
assert!(! ptr.is_null());let battery = &*ptr;
match battery.serial_number() {
Some(sn) => {
let c_str = CString::new(*sn).unwrap();
c_str.into_raw()
},
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern fn battery_str_free(ptr: *mut libc::c_char) {
if ptr.is_null() {
return;
}
CString::from_raw(ptr);
}
Copy the code
When free, the pointer must be checked for NULL to prevent double free.
To generate the binding
Once compiled and packaged, it can be used in other languages, such as Python
import ctypes
class Manager(ctypes.Structure):
pass
lib = ctypes.cdll.LoadLibrary('libmy_lib_ffi.so'))
lib.battery_manager_new.argtypes = None
lib.battery_manager_new.restype = ctypes.POINTER(Manager)
lib.battery_manager_free.argtypes = (ctypes.POINTER(Manager), )
lib.battery_manager_free.restype = None
Copy the code
You can also use CBindGen to automatically generate bindings. Add in Cargo. Toml
[build-dependiencies]
cbindgen = "0.8.0"
[package.metadata.docs.rs]
no-default-features = true
Copy the code
Create cbindgen toml
include_guard = "my_lib_ffi_h"
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
language = "C"
Copy the code
Add build. Rs
use std::env;
use std::path::PathBuf;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let config = cbindgen::Config::from_file("cbindgen.toml").unwrap();
cbindgen::generate_with_config(&crate_dir, config)
.unwrap()
.write_to_file(out_dir.join("my_lib_ffi.h"));
}
Copy the code
This will result in my_lib_ffi.h in OUT_DIR when cargo build is run.