A Rust FFI Example
Using bindgen and Rust to call C/C++ code
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:
- threaded
- uses callbacks for when the query result is available
- ~20 years old
- has custom static and dynamic libraries to link against
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++");
}- Using Rust code to manipulate the build is great, but odd that emitting strings is how it’s done
- The
dylib=stdc++link flag to due some of the libraries being in C++ and dealing with name mangling - Mixed both system and custom libraries (will need
LD_LIBRARY_PATHlater to load them)
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.rsOur 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);
}
}- Lots of
unsafe, but that can be hidden behind a pure Rust interface - Callbacks aren’t as scary as I had expected
- I’m not 100% sure I got a lot of the
libcthings right, butrustchasn’t complained
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 selectedAll that in just a few hours!