adamflott.com

A Rust FFI Example

Using bindgen and Rust to call C/C++ code
Authored: 2021-10-08; Updated:

I've used a number of FFIs (Foreign_function_interface) over the years, but was very impressed with how quickly I got something working with Rust and bindgen.

At work, we have our own SQL-like database, the wire protocol is complicated enough that it's not worth building a native version. Instead there is a C++/C library that handles everything. What makes this a tricky integration is the fact that the database library is:

I have some prior experience with using a binding against this library. I tried (and barely passed) using Go for a project, but ran into all sorts of random segfaulting due to the Go runtime. It may be better as that was around 2016.

First start with a Cargo.toml

[package]
name = "query-ffi-example"
version = "0.1.0"
edition = "2018"
build = "build.rs"

[dependencies]
libc = "0.2"

[build-dependencies]
cc = { version = "1.0", features = ["parallel"] }
pkg-config = "0.3"

In order to link against we need a build.rs file. Some of the libs are static archives (.a) and some a dynamic shared objects (.so).

build.rs
fn main() {
    println!("cargo:rustc-link-lib=static=liba");
    println!("cargo:rustc-link-lib=static=libb");
    println!("cargo:rustc-link-lib=crypto");
    println!("cargo:rustc-link-lib=static=eventloop");
    println!("cargo:rustc-link-lib=static=query");
    println!("cargo:rustc-link-search=/some/dir/query2lib");
    println!("cargo:rustc-flags=-l dylib=stdc++");
}

Generate the bindings via bindgen.

$ bindgen include/query/query.h -o bindings.rs -- -I../common/include/

This creates a large file we don't have to edit:

$ wc -l src/bindings.rs
5980 src/bindings.rs

Our lib.rs can include bindings.rs for convenience of building

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

include!("./bindings.rs");

Our main.rs looks like:

use std::ffi::{CStr, CString};
use std::ptr;
use std::{thread, time};

use query2lib::*;

extern "C" fn callback(qt: *mut query_table, _user_arg: *mut libc::c_void) {
    println!("I'm called from C with value");
    unsafe {
        let c_str = query_dump_table_as_string(qt, query_dump_format_DUMP_DEFAULT);

        let rust_str = CStr::from_ptr(c_str).to_string_lossy().into_owned();
        println!("{}", rust_str);
    }
}

fn main() {
    let id = CString::new("rust-test").unwrap();
    let hostname = CString::new("db-hostname").unwrap();

    unsafe {
        let conn = query_init(id.as_ptr(), hostname.as_ptr(), query_init_flag_INIT_NONE as i32);

        let sql = CString::new("SELECT current_timestamp;").unwrap();
        let fcb: query_callback = Some(callback);

        let p: *const i32 = ptr::null();
        let query_result = query_run(conn, sql.as_ptr(), fcb, p as *mut libc::c_void);

        println!("query_run result: {}", query_result);
        
        let ten_millis = time::Duration::from_millis(10000);
        thread::sleep(ten_millis);

        query_exit(conn);
    }
}

And when running we get:

$ export LD_LIBRARY_PATH=`pwd`
$ ./target/debug/query-ffi-example
query_run result: 0
I'm called from C with value
       CURRENT_TIMESTAMP
------------------------
Fri Oct 08 22:34:20 2021

1 rows selected

All that in just a few hours!


Categories

Tags