welcome to heck: lessons learned from Ikona writing rust bindings to C++ the hard way
rust is quite a neat language, isn't it? gigantic library ecosystem, memory safety, tons of developer-friendly tools in it. for Ikona, I decided to utilise this language, and instead of relying on binding generators that hide half the magic away from you, I wrote all bindings by hand.
rust –> C++ by hand: how?
obviously, rust and C++ are different programming languages and neither of them have language-level interop with each other. what they do both have is C. C—the lingua franca of the computing world. unfortunately, C is a very bad lingua franca. something as basic as passing arrays between programming languages becomes boilerplate hell fast. however, it is possible and once you set up a standardised method of passing arrays, it becomes far easier.
rust to C
so, in order to start going from rust to C++, you need to stop at C first. for Ikona, I put C API bindings in a separate crate in the same workspace. you have a few best friends when writing rust to C here:
– #[no_mangle]
: keeps rustc from mangling your symbols from pure C
– unsafe
: because C is ridiculously unsafe and Rust hates unsafety unless you tell it that you know what you're doing
– extern "C"
: makes rust expose a C ABI that can be eaten by the C++ half
– #[repr(C)]
: tells rust to lay out the memory of a thing like C does
– Box
: pointer management
– CString
: char*
management
memory management
Box
and CString
are your friends for memory management when talking to C. the general cycle looks like this:
pub unsafe extern "C" new_thing() -> *mut Type {
Box::into_raw(thing) // for non-rustaceans, the lack of a semicolon means this is returned
}
pub unsafe extern "C" free_thing(ptr: *mut Type) {
assert!(!ptr.is_null());
Box::from_raw(ptr);
}
into_raw
tells rust to let C have fun with the pointer for a while, so it won't free the memory.
when C is done playing with the pointer, it returns it to Rust so it can from_raw
the pointer to free the memory.
structs
for Ikona, I didn't bother attempting to convert Rust structs into C structs, instead opting for opaque pointers, as they're a lot easier to deal with on the Rust side.
an average function for accessing a struct value in Ikona looks like this:
#[no_mangle]
pub unsafe extern "C" fn ikona_theme_get_root_path(ptr: *const IconTheme) -> *mut c_char {
assert!(!ptr.is_null()); // make sure we don't have a null pointer
let theme = &*ptr; // grab a reference to the Rust value the pointer represents
CString::new(theme.root_path.clone()).expect("Failed to create CString").into_raw() // return a char* from the field being accessed
}
this is very similar to how calling methods on structs is bridged to C in Ikona.
#[no_mangle]
pub unsafe extern "C" fn ikona_icon_extract_subicon_by_id(
ptr: *mut Icon,
id: *mut c_char,
target_size: i32,
) -> *mut Icon {
assert!(!ptr.is_null()); // gotta make sure our Icon isn't null
assert!(!id.is_null()); // making sure our string isn't null
let id_string = CStr::from_ptr(id).to_str().unwrap(); // convert the C string into a Rust string, and explicitly crash instead of having undefined behaviour if something goes wrong
let icon = &*ptr; // grab a reference to the Rust object from the pointer
// now let's call the method C wanted to call
let proc = match icon.extract_subicon_by_id(id_string, target_size) {
Ok(icon) => icon,
Err(_) => return ptr::null_mut::<Icon>(),
};
// make a new Box for the icon
let boxed: Box<Icon> = Box::new(proc);
// let C have fun with the pointer
Box::into_raw(boxed)
}
enums
enums are very simple to bridge, given they aren't the fat enums Rust has. just declare them like this:
#[repr(C)]
pub enum IkonaDirectoryType {
Scalable,
Threshold,
Fixed,
None
}
and treat them as normal. no memory management shenanigans to be had here.
ABI? what about API?
C has header files, and we need to describe the C API for human usage.
structs
since Ikona operates on opaque pointers, C just needs to be told that the type for a struct is a pointer.
typedef void* IkonaIcon;
enums
enums are ridiculously easy.
#[repr(C)]
pub enum IkonaDirectoryType {
Scalable,
Threshold,
Fixed,
None
}
becomes
typedef enum {
ScalableType,
ThresholdType,
FixedType,
NoType,
} IkonaDirectoryType;
not much to it, eh?
methods
methods are the most boilerplate-y part of writing the header, but they're fairly easy. it's just keeping track of which rust thing corresponds to which C thing.
this declaration
pub unsafe extern "C" fn ikona_icon_new_from_path(in_path: *mut c_char) -> *mut Icon {
becomes
IkonaIcon ikona_icon_new_from_path(const char* in_path);
C to C++
once a C API is done being written, you can consume it from C++. you can either write a wrapper class to hide the ugly C or consume it directly. here in the KDE world where the wild Qt run free, you can use smart pointers and simple conversion methods to wrangle with the C types.
advantages
the big advantage for Ikona here is the library ecosystem for Rust. librsvg and resvg are both Rust SVG projects that Ikona can utilise, and both are better in many ways compared to the simplistic SVG machinery available from Qt. heck, resvg starts to near browser-grade SVG handling with a huge array of things to do to SVGs as well as general compatibility. Ikona barely taps into the potential of the Rust world currently, but future updates will leverage the boilerplate laid in 1.0 in order to implement new features that take advantage of the vibrant array, high performance, and fast speed of available Rust libraries.
what I would have done differently
writing a bunch of rust to C boilerplate isn't fun, especially with arrays. since glib-rs is already in the dependency chain of Ikona, I should have utilized the GList instead of writing my own list implementation.
tags: #libre