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.

  • unsafeThe keyword indicates that the function has UB behavior such as null Pointers.
  • externThe 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 callederrno, provides a method to get last Error
  • Create a structure like this and check it each time you returnpresent == 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.