2. Let's build a C++ Dynamic library
for this tutorial
2.1 What will export via the C++ Dynamic Library
2.2 Install C++
and cmake
building tools
2.3 Use cmake
to compile a dynamic library
2.4 How to inspect the library's dynamic symbol table
3. How Rust deal with FFI
?
3.1 #[link]
3.2 extern
block
3.3 How to transfers data type between Rust
and C/C++
?
3.4 How to generate the extern
block from a C/C++
header file?
3.5 How cargo build
knows where to link the C++ dynamic library
?
4. Let's call C++
function in Rust
4.1 Use manual FFI
bindings
4.2 Use bindgen
automatic FFI
bindings
5. Let's build a Rust Dynamic library
5.1 What will export via the Rust Dynamic Library
5.2 How to inspect the library's dynamic symbol table
6. Let's call Rust
function in C++
6.1 Create calling-ffi/cpp/src/ffi.h
6.2 Create calling-ffi/cpp/src/main.cpp
6.3 Create calling-ffi/cpp/CMakeLists.txt
6.4 Build and run
7. Let's call Rust
function in Node.JS
7.1 Setup node project and dependencies
7.2 Know more about the ffi-napi
module
7.2.1 What is ffi-napi
7.2.2 What C++
types supported by the ffi-napi
7.2.3 How to load dynamic library and call extern function
7.3 Build and run
-
ABI
which stands forApplication Binary Interface
.It's an interface between two binary program modules. It looks like the
API
but focus on theCompiler & Linker
, as it covers:- processor instruction set (with details like register file structure, stack organization, memory access types, ...)
- the sizes, layouts, and alignments of basic data types that the processor can directly access
- the calling convention, which controls how the arguments of functions are passed, and return values retrieved. For example, it controls:
- whether all parameters are passed on the stack, or some are passed in registers;
- which registers are used for which function parameters;
- and whether the first function parameter passed on the stack is pushed first or last onto the stack.
- how an application should make system calls to the operating system, and if the ABI specifies direct system calls rather than procedure calls to system call stubs, the system call numbers.
- and in the case of a complete operating system ABI, the binary format of object files, program libraries, and so on.
-
FFI
which stands forForeign Function Interface
It's talking about how the rust code can call the function outside the rust world.
#pragma once
//#include <string>
namespace Demo {
// Simple function case
void print_helloworld();
//
// A more complex case with `enum`, `struct`, and a couple of
// functions to manipulate those data.
//
enum Gender {
Female, Male
};
struct Location {
// string street_address;
// string city;
// string state;
// string country;
const char* street_address;
const char* city;
const char* state;
const char* country;
};
struct Person {
// string first_name;
// string last_name;
const char* first_name;
const char* last_name;
Gender gender;
unsigned char age;
Location location;
~Person();
};
// Create `Person` instance on the heap and return pointer
Person* create_new_person(
// string first_name,
// string last_name,
const char* first_name,
const char* last_name,
Gender gender,
unsigned char age,
Location location);
// Pass the `Person` pointer as parameter
void print_person_info(Person* ptr);
// Pass the `Person` pointer as parameter and get back C-style string
const char* get_person_info(Person* p);
// Pass the `Person` pointer as parameter
void release_person_pointer(Person* ptr);
} // namespace Demo
As you can see above, the C++ Dynamic Library
will export some enum
and struct
types and some functions to maniplulate those stuff.
Because the std::string
actually is a class
(like a vector<char>
or vector<w_char>
) to manage the strings, it uses to enhance the C-style string
(a char array), so we don't use this type at this moment to reduce the complexicy.
-
Arch
sudo pacman --sync --refresh clang libc++ cmake
-
MacOS
brew install llvm clang cmake
cd ffi-dynamic-lib/cpp/ && rm -rf build && mkdir build && cd build
cmake ../ && make
After that, cmake
compiles your cpp project and generate the files below in the cpp/build
folder:
ffi-demo-cpp-lib_debug_version
ffi-demo-cpp-lib
# This is the C++ dynamic library which uses in this FFI demo
# The library filename extension will be:
# - `.dylib` on `MacOS`
libdemo.dylib
# - `.so` on `Linux`
libdemo.so
# - `.dll` on `Windows`
libdemo.dll
-
Linux
objdump -T libdemo.so | grep "hello\|person\|Person\|Location" # 0000000000000000 DF *UND* 0000000000000000 __gxx_personality_v0 # 0000000000003310 g DF .text 00000000000000b4 Base _ZN4Demo16print_helloworldEv # 00000000000036f0 g DF .text 0000000000000224 Base _ZN4Demo17print_person_infoEPNS_6PersonE # 00000000000033d0 g DF .text 0000000000000107 Base _ZN4Demo6PersonD1Ev # 00000000000033d0 g DF .text 0000000000000107 Base _ZN4Demo6PersonD2Ev # 0000000000003920 g DF .text 00000000000003ed Base _ZN4Demo15get_person_infoEPNS_6PersonE # 00000000000034e0 g DF .text 00000000000001ba Base _ZN4DemolsERNSt3__113basic_ostreamIcNS0_11char_traitsIcEEEERKNS_6PersonE # 0000000000003d10 g DF .text 0000000000000018 Base _ZN4Demo22release_person_pointerEPNS_6PersonE # 00000000000036a0 g DF .text 0000000000000049 Base _ZN4Demo17create_new_personEPKcS1_NS_6GenderEhNS_8LocationE # Or nm -f bsd libdemo.so | grep "hello\|person\|Person\|Location" # 0000000000007128 d DW.ref.__gxx_personality_v0 # U __gxx_personality_v0 # 0000000000003920 T _ZN4Demo15get_person_infoEPNS_6PersonE # 0000000000003310 T _ZN4Demo16print_helloworldEv # 00000000000036a0 T _ZN4Demo17create_new_personEPKcS1_NS_6GenderEhNS_8LocationE # 00000000000036f0 T _ZN4Demo17print_person_infoEPNS_6PersonE # 0000000000003d10 T _ZN4Demo22release_person_pointerEPNS_6PersonE # 00000000000033d0 T _ZN4Demo6PersonD1Ev # 00000000000033d0 T _ZN4Demo6PersonD2Ev # 00000000000034e0 T _ZN4DemolsERNSt3__113basic_ostreamIcNS0_11char_traitsIcEEEERKNS_6PersonE
-
MacOS
objdump -t libdemo.dylib | grep "hello\|person\|Person\|Location" # 00000000000019a0 g F __TEXT,__text __ZN4Demo15get_person_infoEPNS_6PersonE # 0000000000001310 g F __TEXT,__text __ZN4Demo16print_helloworldEv # 0000000000001690 g F __TEXT,__text __ZN4Demo17create_new_personEPKcS1_NS_6GenderEhNS_8LocationE # 00000000000016f0 g F __TEXT,__text __ZN4Demo17print_person_infoEPNS_6PersonE # 0000000000001d80 g F __TEXT,__text __ZN4Demo22release_person_pointerEPNS_6PersonE # 00000000000014b0 g F __TEXT,__text __ZN4Demo6PersonD1Ev # 00000000000013b0 g F __TEXT,__text __ZN4Demo6PersonD2Ev # 00000000000014c0 g F __TEXT,__text __ZN4DemolsERNSt3__113basic_ostreamIcNS0_11char_traitsIcEEEERKNS_6PersonE # Or nm -f bsd libdemo.dylib | grep "hello\|person\|Person\|Location" # 00000000000019a0 T __ZN4Demo15get_person_infoEPNS_6PersonE # 0000000000001310 T __ZN4Demo16print_helloworldEv # 0000000000001690 T __ZN4Demo17create_new_personEPKcS1_NS_6GenderEhNS_8LocationE # 00000000000016f0 T __ZN4Demo17print_person_infoEPNS_6PersonE # 0000000000001d80 T __ZN4Demo22release_person_pointerEPNS_6PersonE # 00000000000014b0 T __ZN4Demo6PersonD1Ev # 00000000000013b0 T __ZN4Demo6PersonD2Ev # 00000000000014c0 T __ZN4DemolsERNSt3__113basic_ostreamIcNS0_11char_traitsIcEEEERKNS_6PersonE # Also, you can print the shared libraries used for linked Mach-O files: objdump -macho -dylibs-used libdemo.dylib # libdemo.dylib: # @rpath/libdemo.dylib (compatibility version 0.0.0, current version 0.0.0) # /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.4) # /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)
The link attribute specifies the name of a native library that the compiler should link with for the items within an extern block.
#[link(name = "demo")]
extern {
// …
}
In the above sample, rustc
would try to link with libdemo.so
on unix-like systems and demo.dll
on Windows at runtime. It
panics if it can't find something to link to. That's why you need
to make sure rustc
can find the library file when linking.
Also, you can add the kind
value to say which kind the library it is:
dylib
— Indicates a dynamic library. This is the default if kind is not specified.static
— Indicates a static library.framework
— Indicates amacOS
framework. This is only valid formacOS
targets.
Here is the sample:
#[link(name = "CoreFoundation", kind = "framework")]
extern {
// …
}
Another value you can put there is the wasm_import_module
which use for linking
to the WebAssembly
module case:
#[link(wasm_import_module = "wasm_demo")]
extern {
// …
}
Actually, the best practice is NOT use #[link]
on the extern
block. Instead, use the cargo instructions below in build.rs
which will mentioned in the later chapters.
cargo:rustc-link-lib=dylib=demo
cargo:rustc-link-search=native=cpp/build
The extern
block includes all the external function signatures.
#[link(name = "demo_c_lib")]
extern "C" {
#[link_name = "\u{1}_ZN4Demo16my_c_functionEv"]
fn my_c_function(x: i32) -> bool;
}
The C
part actually is the ABI
string, you can just write extern
without C
as the C
is the default ABI.
Below is the ABI
support list from official ABI
section:
The #[link_name]
helps link to the correct external function which can be generated by the bindgen
command.
There are two modules to handle that:
-
std::os:raw
: Platform-specific types, as defined by C.Rust type C/C++ type c_char
Equivalent to C's char
type.c_double
Equivalent to C's double
type.c_float
Equivalent to C's float
type.c_int
Equivalent to C's signed int (int)
type.c_long
Equivalent to C's signed long (long)
type.c_longlong
Equivalent to C's signed long long (long long)
type.c_schar
Equivalent to C's signed char
type.c_short
Equivalent to C's signed short (short)
type.c_uchar
Equivalent to C's unsigned char
type.c_uint
Equivalent to C's unsigned int
type.c_ulong
Equivalent to C's unsigned long
type.c_ulonglong
Equivalent to C's unsigned long long
type.c_ushort
Equivalent to C's unsigned short
type.
-
In particular, the
C-Style String
is the standardC
string when dealing withC/C++ FFI
.C-Style String
just anarray of char (char[])
, but the string is nul-terminated which means they have a\0
character at the end of the string.The usual form is been using as a pointer:
// const char[] pointer const char* ptr; // char[] pointer char* ptr;
Below is usual case in
C/C++
function to accept aC-Style String
or return aC-Style String
:// `*const c_char` is rust type which equivalent to `const char*` in C/C++ extern "C" { fn c_function_return_c_style_string() -> *const c_char; } // `*const c_char` is rust type which equivalent to `const char*` in C/C++ extern "C" { fn c_function_accept_c_style_string_parameter(s: *const c_char); }
For dealing with that
C-Style string
,std::ffi
module introduces 2 extra data types:Rust type Use case CString
Pass Rust String
asC-Style String
CStr
Get back the Rust String
by theC-Style String
So here is the use case sample:
-
Get back the
Rust String
by theC-Style String
:As
c_function_return_c_style_string()
returnconst char*
which means it just a raw pointer NOT guarantees still valid, that's why you need to wrap inunsafe
block!The methods below dont' own the
C heap allocated
string which means you can use that string without copying or allocating:CStr::from_ptr().to_str()
CStr::from_ptr().to_string_lossy()
CStr::from_ptr().into_c_string()
But if you're responsible for destroying that
C heap-allocated
string, then you should own it and drop it after leaving the scope!
unsafe { let rust_string: String = CStr::from_ptr(c_function_return_c_style_string()) .to_string_lossy() .into_owned(); }
-
Pass
Rust String
asC-Style String
let c_string = CString::new("Hello, world!").unwrap(); unsafe { c_function_accept_c_style_string_parameter(c_string.as_ptr()); }
-
# Install `bindgen`:
cargo install bindgen
#
# bindgen [FLAGS] [OPTIONS] <header> -- <clang-args>...
#
# --disable-header-comment: Not include bindgen's version.
# --enable-cxx-namespaces: Enable support for C++ namespaces.
# --ignore-functions: Ignore functions, good for the case you only care about the `struct`.
# --no-derive-copy: No `#[derive(Copy)]` needed.
# --no-derive-debug: No `#[derive(Debug)]` needed.
# --no-doc-comments: No doc comment needed.
# --no-include-path-detection: Do not try to detect default include paths
# --no-layout-tests: No layout tests for any type.
#
# `--` Follow by all `clang_arg`:
# `-x c++`: Indictes that's the C++ if the header file not end with `.hpp`
# `-std=c++17`: The language standard version
# `-stdlib=libc++`: C++ standard library to use
#
cd calling-ffi/rust
bindgen \
--disable-header-comment \
--enable-cxx-namespaces \
--no-derive-copy \
--no-derive-debug \
--no-doc-comments \
--no-include-path-detection \
--no-layout-tests \
--output src/manual_bindings.rs \
../../ffi-dynamic-lib/cpp/src/dynamic-lib/lib.h \
-- -x c++ \
-std=c++17 \
-stdlib=libc++
For macOS
, you might see the error below:
fatal error: 'XXXX' file not found
Then try to add the -I
clang
flag explicitly like below:
cd calling-ffi/rust
bindgen \
--disable-header-comment \
--enable-cxx-namespaces \
--no-derive-copy \
--no-derive-debug \
--no-doc-comments \
--no-include-path-detection \
--no-layout-tests \
--output src/manual_bindings.rs \
../../ffi-dynamic-lib/cpp/src/dynamic-lib/lib.h \
-- -x c++ \
-I/Library/Developer/CommandLineTools/usr/include/c++/v1 \
-std=c++17 \
-stdlib=libc++
That's what exactly the build script
does.
Placing a file named build.rs
in the root of a package will cause Cargo
to compile that script and execute it just before building the package. That's the right place to let rustc
to know where to link the C++ dynamic library
:
// FFI custom build script.
fn main() {
//
// The `rustc-link-lib` instruction tells `Cargo` to link the
// given library using the compiler's `-l` flag. This is typically
// used to link a native library using FFI.
//
// If you've already add a `#[link(name = "demo"]` in the `extern`
// block, then you don't need to provide this.
//
println!("cargo:rustc-link-lib=dylib=demo");
//
// The `rustc-link-search` instruction tells Cargo to pass the `-L`
// flag to the compiler to add a directory to the library search path.
//
// The optional `KIND` may be one of the values below:
//
// - `dependency`: Only search for transitive dependencies in this directory.
// - `crate`: Only search for this crate's direct dependencies in this directory.
// - `native`: Only search for native libraries in this directory.
// - `framework`: Only search for macOS frameworks in this directory.
// - `all`: Search for all library kinds in this directory. This is the default
// if KIND is not specified.
//
println!("cargo:rustc-link-search=native=../../ffi-dynmaic-lib/cpp/build");
}
Make sure cd calling-ffi/rust
before doing the following steps!!!
-
Add the particular features to
Cargo.toml
:[features] default = [] enable-manual-bindings = []
enable-manual-bindings
uses for compilingbuild.rs
with the particular condition.
-
Generate
src/manual_bindings.rs
by running the command below:bindgen \ --disable-header-comment \ --enable-cxx-namespaces \ --no-derive-debug \ --no-derive-copy \ --no-doc-comments \ --no-include-path-detection \ --no-layout-tests \ --output src/manual_bindings.rs \ ../../ffi-dynamic-lib/cpp/src/dynamic-lib/lib.h \ -- -x c++ \ -std=c++17 \ -stdlib=libc++
After that, you can see some bindings like below:
#[repr(C)] pub struct Person { pub first_name: *const ::std::os::raw::c_char, pub last_name: *const ::std::os::raw::c_char, pub gender: root::Demo::Gender, pub age: ::std::os::raw::c_uchar, pub location: root::Demo::Location, } extern "C" { #[link_name = "\u{1}__ZN4Demo17create_new_personEPKcS1_NS_6GenderEhNS_8LocationE"] pub fn create_new_person( first_name: *const ::std::os::raw::c_char, last_name: *const ::std::os::raw::c_char, gender: root::Demo::Gender, age: ::std::os::raw::c_uchar, location: root::Demo::Location, ) -> *mut root::Demo::Person; }
-
#[repr(C)]
:repr
stands forrepresentation
, it describes aType Layout
which you will find more explanation at here.This is the most important
repr
. It has fairly simple intent: do what C does. The order, size, and alignment of fields is exactly what you would expect from C or C++. Any type you expect to pass through an FFI boundary should haverepr(C)
.If you don't do that, you will get the warning like below and your executable will crash with
SIGSEGV
error.:warning: `extern` block uses type `Person`, which is not FFI-safe
-
[link_name]
The
link_name
attribute indicates the symbol to import for the given function which you've already saw it above via theobjdump
command.
-
-
src/bin/manual_ffi_binding_demo.rs
includes all the FFI calling samples.
-
Create
build.rs
with the following content:// FFI custom build script. fn main() { // // Link to `libdemo` dynamic library file // println!("cargo:rustc-link-lib=dylib=demo"); // // Let `Cargo` to pass the `-L` flag to the compiler to add // the searching directory for the`native` library file // println!("cargo:rustc-link-search=native=../../ffi-dynamic-lib/cpp/build"); }
This allows
Cargo
to know where to link theC++ dynamic library
file.
-
Build and run the demo
cargo clean && cargo build \ --bin manual_ffi_binding_demo \ --features "enable-manual-bindings" \ --release LD_LIBRARY_PATH=../../ffi-dynamic-lib/cpp/build/ ./target/release/manual_ffi_binding_demo
You should see demo output like below:
If you print the symbol table for the release executable, you should be able to notic that it relies on the FFI functions in the
C++ Dynamic Library
:nm -f bsd target/release/manual_ffi_binding_demo | grep "hello\|person\|Person\|Location" U __ZN4Demo15get_person_infoEPNS_6PersonE U __ZN4Demo16print_helloworldEv U __ZN4Demo17create_new_personEPKcS1_NS_6GenderEhNS_8LocationE U __ZN4Demo17print_person_infoEPNS_6PersonE U __ZN4Demo22release_person_pointerEPNS_6PersonE
So, you've already learned how to do that in a manual bindings
way. The advantage of this
way is that you have an FFI binding source code when you're coding, then your editor (with
Rust language plugin) can detect any error or show you the code completion handy feature
when you're typing.
But the disadvantage is that you need to run bindgen
manually every time, as the function
symbol will be changed every time after you re-generate the C++ Dynamic Library
. That
will be trouble or inconvenience. That's how bindgen
automatic FFI
bindings can help.
Make sure cd calling-ffi/rust
before doing the following steps!!!
-
Add the build dependencies to
Cargo.toml
:[build-dependencies] bindgen = "~0.53.1"
-
Replace the following content to the
build.rs
:// FFI custom build script. #[cfg(not(feature = "enable-manual-bindings"))] use bindgen; #[cfg(not(feature = "enable-manual-bindings"))] use std::env; #[cfg(not(feature = "enable-manual-bindings"))] use std::path::PathBuf; fn main() { // // Link to `libdemo` dynamic library file // println!("cargo:rustc-link-lib=dylib=demo"); // // Let `Cargo` to pass the `-L` flag to the compiler to add // the searching directory for the`native` library file // println!("cargo:rustc-link-search=native=../../ffi-dynamic-lib/cpp/build"); #[cfg(not(feature = "enable-manual-bindings"))] { // Tell cargo to invalidate the built crate whenever the wrapper changes println!("cargo:rerun-if-changed=../../ffi-dynamic-lib/cpp/src/dynamic-lib/lib.h"); // // Write the bindings to the $OUT_DIR/bindings.rs file. // // For example: // - target/debug/build/ffi-demo-XXXXXXXXXXXXXXXX/out/bindings.rs // - target/release/build/ffi-demo-XXXXXXXXXXXXXXXX/out/bindings.rs // let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); println!("out_put: {:#?}", &out_path); // The bindgen::Builder is the main entry point to bindgen, and lets // you build up options for the resulting bindings. let bindings = bindgen::Builder::default() // The input header we would like to generate bindings for. .header("../../ffi-dynamic-lib/cpp/src/dynamic-lib/lib.h") // Not generate the layout test code .layout_tests(false) // Not derive `Debug, Clone, Copy, Default` trait by default .derive_debug(false) .derive_copy(false) .derive_default(false) // Enable C++ namespace support .enable_cxx_namespaces() // Add extra clang args for supporting `C++` .clang_arg("-x") .clang_arg("c++") .clang_arg("-std=c++17") .clang_arg("-stdlib=libc++") // Tell cargo to invalidate the built crate whenever any of the // included header files changed. .parse_callbacks(Box::new(bindgen::CargoCallbacks)) // Finish the builder and generate the bindings. .generate() // Unwrap the Result and panic on failure. .expect("Unable to generate bindings"); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); } }
-
Add the following content to
src/main.rs
:#![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] include!(concat!(env!("OUT_DIR"), "/bindings.rs")); use root::Demo::{ create_new_person, get_person_info, print_helloworld, print_person_info, release_person_pointer, Gender_Male, Location, Person, }; use std::ffi::{CStr, CString}; fn main() { println!("[ Auto FFI bindgins call demo ]\n"); // // Ignore the same source code from `src/bin/manual_ffi_binding_demo.rs` here // }
The
inclulde!
line macros actually put all the bindings source code into that line.If you have the problem below when using
rust-analyzer
:Plz make sure you DO NOT have the
"rust-analyzer.cargo.loadOutDirsFromCheck": false,
settings in your configuration file (likecoc-settings.json
for example).
-
Build and run the demo:
cargo clean && cargo build --release LD_LIBRARY_PATH=../../ffi-dynamic-lib/cpp/build/ ./target/release/ffi-demo
From now on, target/{BUILD_TYPE}/build/ffi-demo-XXXXXXXXXXXXXXXX/out/bindings.rs
will
be generated automatic every time when you run cargo build
. Then you don't need to
worry about running that manually or bindings.rs
is the older version, more convenient
than before.
Make sure cd ffi-dynamic-lib/rust
before doing the following steps!!!
There are several parts inside this library:
///
#[derive(Debug)]
pub enum Gender {
Female,
Male,
Unknown,
}
///
#[derive(Debug)]
pub struct Location {
street_address: String,
city: String,
state: String,
country: String,
}
///
pub struct Person {
first_name: String,
last_name: String,
gender: Gender,
age: u8,
location: Location,
}
Because Rust
has the ownership and borrowing
concept, all rust code
under borrow checker
control, actually should say under borrow checker
's
protection.
But the FFI
caller doesn't have that concept. If we pass the instance to
the outside world, then the borrow checker
can't guarantee and protect
that instance memory.
The easy way is that allocates the instance on the heap, and then return its raw pointer.
As we hand over the instance raw pointer to the FFI
caller, that will lose
control of the memory, that's why should have the release
extern function
to return the control of memory we given out to make sure release the instance
memory correctly!!!
-
Create new
Person
instance on the heap and return the raw pointer#[no_mangle] pub extern "C" fn create_new_person( first_name: *const c_char, last_name: *const c_char, gender: c_uchar, age: c_uchar, street_address: *const c_char, city: *const c_char, state: *const c_char, country: *const c_char, ) -> *mut Person { let temp_gender = match gender { 0 => Gender::Female, 1 => Gender::Male, _ => Gender::Unknown, }; unsafe { let new_person = Person { first_name: CStr::from_ptr(first_name).to_string_lossy().into_owned(), last_name: CStr::from_ptr(last_name).to_string_lossy().into_owned(), gender: temp_gender, age: age as u8, location: Location { street_address: CStr::from_ptr(street_address) .to_string_lossy() .into_owned(), city: CStr::from_ptr(city).to_string_lossy().into_owned(), state: CStr::from_ptr(state).to_string_lossy().into_owned(), country: CStr::from_ptr(country).to_string_lossy().into_owned(), }, }; Box::into_raw(Box::new(new_person)) } }
A couple of things happen here:
-
*const c_char
:The
C-Style String
(const char*
) needs to be converted intoString
, that why uses*const std::os::raw::c_char
(immutable pointer toc_char
).
-
#[no_mangle]
:The
no_mangle
attribute instructs therustc
compiler to not alter the function name when it is inserted to a binary file. This makes it easier for FFI users to call it, as the name is kept as "human-readable".When inspecting the dynamic library symbol table, you would see something like this
_create_new_person
instead of this_rust_eh_personality
.
-
extern "C"
:extern "C"
defines that this function should be callable outside Rust codebases, and use the "C ABI" calling convention.
-
Box::into_raw(Box::new(new_person))
:Box::new()
allocates the instance on the heap, then it can leave as long as needed for theFFI
caller to use.Box::into_raw()
consumes theBox<Person>
and return the wrapped raw pointer.
-
-
Release the
Person
instance raw pointer correctlypub extern "C" fn release_person_pointer(ptr: *mut Person) { if ptr.is_null() { return; } unsafe { Box::from_raw(ptr); } }
This extern function accepts a raw pointer which returned from
create_new_person()
and convert it back intoBox<Person>
, then the box destructor will cleanup thePerson
instance correctly.
-
Release
CString
instance raw pointer correctly#[no_mangle] pub extern "C" fn get_person_info(ptr: *mut Person) -> *mut c_char { if ptr.is_null() { return CString::new("").unwrap().into_raw(); } unsafe { CString::new((*ptr).get_info()).unwrap().into_raw() } } #[no_mangle] pub extern "C" fn release_get_person_info(info_ptr: *mut c_char) { if info_ptr.is_null() { return; } unsafe { CString::from_raw(info_ptr); } }
Because
Person.get_info()
returns aString
instance, but theFFI
caller can't use it, then we need to convert it into aCString
instance and call itsinto_raw()
to produce a raw pointer which theFFI
caller can use it as achar *
string.CString.into_raw()
consumes theCString
and transfers ownership of the string to aFFI(C)
caller.In particular, that raw pointer SHOULD NOT be deallocated by using the standard C
free()
. That's why we have therelease_get_person_info()
for doing the release step.
-
The
Drop
trait:This can prove that
Person
instance (includesPerson.location
) has been destroyed correctly./// /// Customized drop trait /// impl Drop for Person { /// fn drop(&mut self) { println!( " [ Person instance get destroyed ] - first name: {}, last name: {}", &self.first_name, &self.last_name ); } } /// /// Customized drop trait /// impl Drop for Location { /// fn drop(&mut self) { println!( " [ Person location instance get destroyed ] - street address: {}, city: {}", &self.street_address, &self.city ); } }
Here is the
ffi-dynamic-lib/rust/src/main.rs
.
[lib]
crate-type = ["cdylib"]
The setting above indicates that a dynamic system library will be produced. This is used when compiling a dynamic library to be loaded from another language. The output file extension will be different for the particular OS:
- Linux:
*.so
- MacOS:
*.dylib
- Windows:
*.dll
cargo clean && cargo build --release
-
Linux
objdump -T ./target/release/librust.so | grep "person\|Person\|Location" # 0000000000026a00 g DF .text 0000000000000351 Base rust_eh_personality # 00000000000054f0 g DF .text 000000000000065f Base create_new_person # 0000000000005ba0 g DF .text 000000000000006e Base print_person_info # 0000000000005b50 g DF .text 0000000000000048 Base release_person_pointer # 0000000000005c10 g DF .text 000000000000018c Base get_person_info # 0000000000005da0 g DF .text 0000000000000028 Base release_get_person_info # Or nm -f bsd ./target/release/librust.so | grep "person\|Person\|Location" # 00000000000054f0 T create_new_person # 0000000000049008 d DW.ref.rust_eh_personality # 0000000000005c10 T get_person_info # 0000000000005ba0 T print_person_info # 0000000000005da0 T release_get_person_info # 0000000000005b50 T release_person_pointer # 0000000000026a00 T rust_eh_personality # 0000000000032880 t _ZN4core5panic8Location6caller17h7a7acf437630d90eE # 0000000000005e40 t _ZN51_$LT$rust..Location$u20$as$u20$core..fmt..Debug$GT$3fmt17hd5037e5c9d432ecbE # 0000000000032890 t _ZN60_$LT$core..panic..Location$u20$as$u20$core..fmt..Display$GT$3fmt17hb4680bb747c9c063E
-
MacOS
objdump -t ./target/release/librust.dylib | grep "person\|Person\|Location" # 0000000000001eb0 l F __TEXT,__text __ZN51_$LT$rust..Location$u20$as$u20$core..fmt..Debug$GT$3fmt17h33f040e226ce3834E # 000000000002a4c0 l F __TEXT,__text __ZN4core5panic8Location6caller17hb3a7d4b2fc73787cE # 000000000002a4d0 l F __TEXT,__text __ZN60_$LT$core..panic..Location$u20$as$u20$core..fmt..Display$GT$3fmt17h450055633af24029E # 0000000000001280 g F __TEXT,__text _create_new_person # 0000000000001c70 g F __TEXT,__text _get_person_info # 0000000000001c00 g F __TEXT,__text _print_person_info # 0000000000001e10 g F __TEXT,__text _release_get_person_info # 0000000000001bb0 g F __TEXT,__text _release_person_pointer # 0000000000022460 g F __TEXT,__text _rust_eh_personality # Or nm -f bsd ./target/release/librust.dylib | grep "person\|Person\|Location" # 000000000002a4c0 t __ZN4core5panic8Location6caller17hb3a7d4b2fc73787cE # 0000000000001eb0 t __ZN51_$LT$rust..Location$u20$as$u20$core..fmt..Debug$GT$3fmt17h33f040e226ce3834E # 000000000002a4d0 t __ZN60_$LT$core..panic..Location$u20$as$u20$core..fmt..Display$GT$3fmt17h450055633af24029E # 0000000000001280 T _create_new_person # 0000000000001c70 T _get_person_info # 0000000000001c00 T _print_person_info # 0000000000001e10 T _release_get_person_info # 0000000000001bb0 T _release_person_pointer # 0000000000022460 T _rust_eh_personality # Also, you can print the shared libraries used for linked Mach-O files: objdump -macho -dylibs-used ./target/release/librust.dylib # ./target/release/librust.dylib: # /Users/wison/Rust/rust-ffi-demo/ffi-dynamic-lib/rust/target/release/deps/librust.dylib (compatibility version 0.0.0, current version 0.0.0) # /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1) # /usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)
Make sure cd calling-ffi/cpp
before doing the following steps!!!
6.1 Create calling-ffi/cpp/src/ffi.h
with the following content:
#pragma once
//
// Declare extern FFI functions from Rust dynamic library
//
#ifdef __cplusplus
extern "C" {
#endif
typedef struct person person_t;
person_t *create_new_person(const char *first_name,
const char *last_name,
unsigned char gender, unsigned char age,
const char *street_address,
const char *city, const char *state,
const char *country);
void release_person_pointer(person_t *);
void print_person_info(person_t *);
char *get_person_info(person_t *);
void release_get_person_info(char *);
#ifdef __cplusplus
}
#endif
6.2 Create calling-ffi/cpp/src/main.cpp
with the following content:
#include "ffi.h"
#include <iostream>
using namespace std;
int main() {
//
// Call FFI functions
//
const char *first_name = "Wison";
const char *last_name = "Ye";
const char *street_address = "Wison's street_address here";
const char *city = "Wison's city here";
const char *state = "Wison's state here";
const char *country = "Wison's country here";
person_t *wison = create_new_person(
first_name,
last_name,
1,
88,
street_address,
city,
state,
country
);
print_person_info(wison);
char *person_info_ptr = get_person_info(wison);
cout << "\n>>> C++ caller print >>>\n" << person_info_ptr << "\n\n";
release_get_person_info(person_info_ptr);
release_person_pointer(wison);
return 0;
}
6.3 Create calling-ffi/cpp/CMakeLists.txt
with the following content:
cmake_minimum_required(VERSION "3.17.2")
set(CMAKE_HOST_SYSTEM_PROCESSOR X86_64)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++ -stdlib=libc++)
# Same with adding the compile flag `-std=c++17`
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED on)
# Build type
set(CMAKE_BUILD_TYPE Release)
#-------------------------------------------------------------------------------------
# Project settings
#-------------------------------------------------------------------------------------
# Define project name. After this, we can use "${PROJECT_NAME}" var to
# dereference/re-use the project name as a String value.
project("calling-rust-in-cpp")
# Add directories in which the linker will look for libraries.
# This setting HAS TO define BEFORE `add_executable`!!!
link_directories(../../ffi-dynamic-lib/rust/target/release)
# Compile and build the executable
add_executable("${PROJECT_NAME}" "src/main.cpp")
# Link the particular library to the executable we build.
# It asks the linker to use `-llibrust` option which means
# link to the particular library file below for different OS:
#
# Linux - librust.so
# MacOS - librust.dylib
# Windows - librust.dll
target_link_libraries("${PROJECT_NAME}" "rust")
rm -rf build && mkdir build && cd build
cmake ../ && make
./calling-rust-in-cpp
You should see the output like below:
Make sure cd calling-ffi/node
before doing the following steps!!!
npm init -y
npm install ffi-napi
mkdir src
touch src/calling-rust-in-node.js
Add some npm scripts to package.json
and it looks like below:
{
"name": "calling-rust-in-node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node src/calling-rust-in-node.js",
"print_ffi_types": "node src/print_ffi_types.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ffi-napi": "^4.0.3"
}
}
Node.js Foreign Function Interface for N-API
.
ffi-napi
is a Node.js addon for loading and calling dynamic
libraries using pure JavaScript. It can be used to create
bindings to native libraries without writing any C++ code.
Here is the src/print_ffi_types.js
const ffi = require('ffi-napi');
// console.log(`ffi: `, ffi)
const ffiTypesKeys = Object.keys(ffi.types)
console.log(`ffiTypes: `)
ffiTypesKeys.forEach(key => {
const separator = key.length <= 8 ?`\t--> ` : `--> `
console.log(`key: `, key, separator, ffi.types[key].ffi_type)
})
If you run npm run print_ffi_types
, then you should be able to see
the output like below:
Here is the src/calling-rust-in-node.js
.
const ffi = require('ffi-napi');
// console.log(ffi.types);
//
// Load `librust` dynmaic library
//
const librust = ffi.Library(`../../ffi-dynamic-lib/rust/target/release/librust`, {
//
// #[no_mangle]
// pub extern "C" fn create_new_person(
// first_name: *const c_char,
// last_name: *const c_char,
// gender: c_uchar,
// age: c_uchar,
// street_address: *const c_char,
// city: *const c_char,
// state: *const c_char,
// country: *const c_char,
//
'create_new_person': ['pointer', [
'string',
'string',
'uchar',
'uchar',
'string',
'string',
'string',
'string'
]
],
//
// #[no_mangle]
// pub extern "C" fn release_get_person_info(info_ptr: *mut c_char) {
//
'release_person_pointer': ['void', ['pointer']],
//
// #[no_mangle]
// pub extern "C" fn print_person_info(ptr: *mut Person) {
//
'print_person_info': ['void', ['pointer']],
//
// #[no_mangle]
// pub extern "C" fn get_person_info(ptr: *mut Person) -> *mut c_char {
//
'get_person_info': ['char *', ['pointer']],
//
// #[no_mangle]
// pub extern "C" fn release_get_person_info(info_ptr: *mut c_char) {
//
'release_get_person_info': ['void', ['char *']],
})
const newPersonPtr = librust.create_new_person(
`Wison`,
`Ye`,
1,
50,
`Wison's street_address here`,
`Wison's city here`,
`Wison's state here`,
`Wison's country here`,
)
try {
console.log(`>>> 'print_person_info' print >>>`)
librust.print_person_info(newPersonPtr)
const personInfoPtr = librust.get_person_info(newPersonPtr)
console.log(`\n>>> 'get_person_info' print >>>\n${personInfoPtr.readCString()}\n`)
librust.release_get_person_info(personInfoPtr)
} finally {
librust.release_person_pointer(newPersonPtr)
}
It looks pretty easy to understand, ffi.Library
function accepts 2 two parameters:
-
../../ffi-dynamic-lib/rust/target/release/librust
:This is the library filename to load, you don't need to add the extension, it will figure out automatic:
/** * The extension to use on libraries. * i.e. libm -> libm.so on linux */ const EXT = Library.EXT = { 'linux': '.so', 'linux2': '.so', 'sunos': '.so', 'solaris':'.so', 'freebsd':'.so', 'openbsd':'.so', 'darwin': '.dylib', 'mac': '.dylib', 'win32': '.dll' }[process.platform];
-
The second one JSON option is used to apply the extern function to the returned result object.
Here is the option syntax:
const librust = ffi.Library(`LIBRARY_FILENAME`, { 'EXTERN_FUNCTION_NAME': [ RETURN_TYPE, [PARAMETER_TYPE_LIST]] })
The
RETURN_TYPE
andPARAMETER_TYPE
can be any type print in thenpm run print_ffi_types
output:key: void --> <Buffer@0x1046bbed8 name: 'void'> key: int8 --> <Buffer@0x1046bbf08 name: 'int8'> key: uint8 --> <Buffer@0x1046bbef0 name: 'uint8'> key: int16 --> <Buffer@0x1046bbf38 name: 'int16'> key: uint16 --> <Buffer@0x1046bbf20 name: 'uint16'> key: int32 --> <Buffer@0x1046bbf68 name: 'int32'> key: uint32 --> <Buffer@0x1046bbf50 name: 'uint32'> key: int64 --> <Buffer@0x1046bbf98 name: 'int64'> key: uint64 --> <Buffer@0x1046bbf80 name: 'uint64'> key: float --> <Buffer@0x1046bbfc8 name: 'float'> key: double --> <Buffer@0x1046bbfe0 name: 'double'> key: Object --> <Buffer@0x1046bbfb0 name: 'pointer'> key: CString --> <Buffer@0x1046bbfb0 name: 'pointer'> key: bool --> <Buffer@0x1046bbef0 name: 'uint8'> key: byte --> <Buffer@0x1046bbef0 name: 'uint8'> key: char --> <Buffer@0x1046bbf08 name: 'char'> key: uchar --> <Buffer@0x1046bbef0 name: 'uchar'> key: short --> <Buffer@0x1046bbf38 name: 'short'> key: ushort --> <Buffer@0x1046bbf20 name: 'ushort'> key: int --> <Buffer@0x1046bbf68 name: 'int'> key: uint --> <Buffer@0x1046bbf50 name: 'uint'> key: long --> <Buffer@0x1046bbf98 name: 'int64'> key: ulong --> <Buffer@0x1046bbf80 name: 'uint64'> key: longlong --> <Buffer@0x1046bbf98 name: 'longlong'> key: ulonglong --> <Buffer@0x1046bbf80 name: 'ulonglong'> key: size_t --> <Buffer@0x1046bbfb0 name: 'pointer'>
As you can see that they're all wrapped by the
Buffer
type, that's what happens under the hood.For more details about how to handle different cases in
ffi-napi
, plz access the links below:
If you run npm start
, then you should see the output like below: