3exp8 / rust-ffi-demo

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rust FFI (Foreign Function Interface) Demo

1. What is ABI and FFI?

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

1. What is ABI and FFI

  • ABI which stands for Application Binary Interface.

    It's an interface between two binary program modules. It looks like the API but focus on the Compiler & 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 for Foreign Function Interface

    It's talking about how the rust code can call the function outside the rust world.


2. Let's build a C++ Dynamic library for this tutorial

2.1 What will export via the C++ Dynamic Library:

#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.


2.2 Install C++ and cmake building tools

  • Arch

    sudo pacman --sync --refresh clang libc++ cmake
  • MacOS

    brew install llvm clang cmake

2.3 Use cmake to compile a dynamic library

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

2.4 How to inspect the library's dynamic symbol table

  • 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)

3. How Rust deal with FFI

3.1 #[link]

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 a macOS framework. This is only valid for macOS 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

3.2 extern

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:

rust-abi-list.png


The #[link_name] helps link to the correct external function which can be generated by the bindgen command.


3.3 How to transfers data type between Rust and C/C++?

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.

  • std::ffi:

    In particular, the C-Style String is the standard C string when dealing with C/C++ FFI. C-Style String just an array 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 a C-Style String or return a C-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 as C-Style String
    CStr Get back the Rust String by the C-Style String

    So here is the use case sample:

    • Get back the Rust String by the C-Style String:

      As c_function_return_c_style_string() return const char* which means it just a raw pointer NOT guarantees still valid, that's why you need to wrap in unsafe 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 as C-Style String

      let c_string = CString::new("Hello, world!").unwrap();
      unsafe {
          c_function_accept_c_style_string_parameter(c_string.as_ptr());
      }

3.4 How to generate the extern block from a C/C++ header file?

# 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++

3.5 How cargo build knows where to link the C++ dynamic library?

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");
}

4. Let's call C++ function in Rust

4.1 Use manual FFI bindings


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 compiling build.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 for representation, it describes a Type 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 have repr(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 the objdump 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 the C++ 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:

    manual_ffi_binding_demo-png.png

    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.


4.2 Use bindgen automatic FFI bindings


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:

    rust-analyzer-out-dir-issue.png

    Plz make sure you DO NOT have the "rust-analyzer.cargo.loadOutDirsFromCheck": false, settings in your configuration file (like coc-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.



5. Let's build a Rust Dynamic library


Make sure cd ffi-dynamic-lib/rust before doing the following steps!!!


5.1 What will export via the Rust Dynamic Library:

There are several parts inside this library:

5.1.1 The struct definition:
///
#[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,
}

5.1.2 The extern functions export to the outside world:

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 into String, that why uses *const std::os::raw::c_char (immutable pointer to c_char).


    • #[no_mangle]:

      The no_mangle attribute instructs the rustc 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 the FFI caller to use.

      Box::into_raw() consumes the Box<Person> and return the wrapped raw pointer.


  • Release the Person instance raw pointer correctly

    pub 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 into Box<Person>, then the box destructor will cleanup the Person 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 a String instance, but the FFI caller can't use it, then we need to convert it into a CString instance and call its into_raw() to produce a raw pointer which the FFI caller can use it as a char * string. CString.into_raw() consumes the CString and transfers ownership of the string to a FFI(C) caller.

    In particular, that raw pointer SHOULD NOT be deallocated by using the standard C free(). That's why we have the release_get_person_info() for doing the release step.


  • The Drop trait:

    This can prove that Person instance (includes Person.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.


5.2 Add the content below to Cargo.toml

[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

5.3 Build the library

cargo clean && cargo build --release

5.2 How to inspect the library's dynamic symbol table

  • 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)

6. Let's call Rust function in C++


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")

6.4 Build and run

rm -rf build && mkdir build && cd build
cmake ../ && make

./calling-rust-in-cpp

You should see the output like below:

calling-ffi-cpp-demo.png


7. Let's call Rust function in Node.JS


Make sure cd calling-ffi/node before doing the following steps!!!


7.1 Setup node project and dependencies

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"
  }
}

7.2 Know more about the ffi-napi module

7.2.1 What is ffi-napi

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.


7.2.2 What C++ types supported by the ffi-napi

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:

print-ffi-types.png


7.2.3 How to load dynamic library and call extern function

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 and PARAMETER_TYPE can be any type print in the npm 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:


7.3 Build and run

If you run npm start, then you should see the output like below:

calling-rust-in-node.png


About


Languages

Language:Rust 52.4%Language:CMake 19.4%Language:C++ 19.1%Language:JavaScript 7.3%Language:C 1.8%