binary calling conventions that both sides adhere to. Think of it as a «communication protocol». Not all languages have fixed calling conventions. C does, C++ does not. 3/67
like adding two integers. That is a totally useless example, since reality is much more complex. Biggest pain point once you get started: Heap allocations and pointers. 4/67
in this talk, we’ll take a look at a simple library I’ve written. That library is a parser for ICE candidates with bindings for C and Java. Source: https://github.com/dbrgn/candidateparser 12/67
the parsing in detail. The parser is written in Rust using nom1. It provides a single function as entry point: pub fn parse(sdp: &[u8]) -> Option<IceCandidate> 1https://crates.io/crates/nom 16/67
from C, we need to: • Make sure that all involved data types are #[repr(C)] (simplifying Rust specific types) • Mark all exposed functions with extern "C" and #[no_mangle] 21/67
from C, we need to: • Make sure that all involved data types are #[repr(C)] (simplifying Rust specific types) • Mark all exposed functions with extern "C" and #[no_mangle] • Compile the crate as a cdylib 21/67
call Rust from C, then all involved data types need to use C representation as memory layout. By default, the memory layout in Rust is unspecified. Rust is free to optimize and reorder fields. 22/67
to a *const c_char through CString: use std::ffi::CString; use libc::c_char; let s: String = "Hello".to_string(); let cs: CString = CString::new(s).unwrap(); let ptr: *const c_char = cs.into_raw(); 26/67
not be exposed directly through FFI! ! △ Note: CString::into_raw() transfers memory ownership to a C caller! (The alternative would be CString::as_ptr()) 27/67
with associated data that cannot be represented directly as a C type. Return it as a C string instead! pub enum Transport { Udp, Extension(String) } impl Into<CString> for Transport { fn into(self) -> CString { match self { Transport::Udp => CString::new("udp").unwrap(), Transport::Extension(e) => CString::new(e).unwrap(), } } } 28/67
types like IpAddr. We cannot impl Into<CString> for those due to the orphan rule2. Instead, convert them to a C string using the ToString trait! let addr = CString::new(parsed.addr.to_string()) .unwrap() .into_raw(); 2You can write an impl only if either your crate defined the trait or defined one of the types the impl is for. 29/67
directly corresponding to Option<T>. Instead, when dealing with heap allocated types, use (yuck!) null pointers. let optional_ip = match parsed.rel_addr { Some(addr) => { CString::new(addr.to_string()).unwrap().into_raw() }, None => std::ptr::null(), } For simpler types, use an ”empty” value. let optional_port = parsed.rel_port.unwrap_or(0); 30/67
it is passed as a pointer to the first element. C also needs to know how long our vector is! let v: Vec<u8> = vec![1, 2, 3, 4]; let v_len: usize = v.len(); let v_ptr: Box<[u8]> = Box::into_raw(v.into_boxed_slice()); let raw_parts = (v_ptr, v_len); In C: for (size_t i = 0; i < rustvec.len; i++) { handle_byte(rustvec.ptr[i]); } 35/67
first need to convert the C char pointer to a Rust byte slice. // `sdp` is a *const c_char if sdp.is_null() { return std::ptr::null(); } let cstr_sdp = CStr::from_ptr(sdp); Note that we’re using CStr, not CString! 38/67
Rust type to the FFI type (using the techniques explained previously) and return a pointer to that. // Convert to FFI representation let ffi_candidate: IceCandidateFFI = ...; // Return a pointer Box::into_raw(Box::new(ffi_candidate)) 40/67
as a C compatible shared library, put this in your Cargo.toml: [lib] name = "candidateparser_ffi" crate-type = ["cdylib"] This will result in a candidateparser_ffi.so file. 41/67
library from C, you also need a header file. You can write such a header file by hand, or you can generate it at compile time using the cbindgen crate3. 3https://github.com/eqrion/cbindgen 42/67
simply call the function: #include "candidateparser.h" const IceCandidateFFI *candidate = parse_ice_candidate_sdp(sdp); Then link against the shared library when compiling: $ clang example.c -o example \ -L ../target/debug -l candidateparser_ffi \ -Wall -Wextra -g A full example is available in the candidateparser-ffi crate on Github. 43/67
that memory cannot be freed by C! If we don’t free it, we end up with memory leaks. We need to pass the pointers back to Rust to free the memory. 45/67
reconstruct Rust owned types from these pointers. The memory is freed as soon as those objects go out of scope! For strings: CString::from_raw(candidate.foundation as *mut c_char); For nullable strings: if !candidate.rel_addr.is_null() { CString::from_raw(candidate.rel_addr as *mut c_char); } 48/67
KeyValueMap is a bit more complex: let e = candidate.extensions; let pairs = Vec::from_raw_parts(e.values as *mut KeyValuePair, e.len as usize, e.len as usize); for p in pairs { Vec::from_raw_parts(p.key as *mut uint8_t, // Start p.key_len as usize, // Length p.key_len as usize); // Capacity Vec::from_raw_parts(p.val as *mut uint8_t, // Start p.val_len as usize, // Length p.val_len as usize); // Capacity } 49/67
languages is through JNI (Java Native Interface). There are newer options by now (namely JNA), but as far as I know there are issues with that if you want to run your code on Android. 52/67
types we’re going to use. Since it’s Java, it’s a bit verbose. package ch.dbrgn.candidateparser; import java.util.HashMap; public class IceCandidate { // Non-null fields private String foundation; private long componentId; private String transport; private long priority; private String connectionAddress; private int port; private String candidateType; 53/67
passed in as an argument we need to access it through the JNIEnv instance and convert it to a Rust String. let sdp: String = env.get_string(input).unwrap().into(); Now we can simply pass it to the regular Rust function! let candidate = match candidateparser::parse(sdp.as_bytes()) { Some(cand) => cand, None => return std::ptr::null_mut() as *mut _jobject, // hack }; 60/67
in JNI wrapper types. This makes sure that the JVM GC knows about them (memory ownership!). Two examples: let component_id = JValue::Long( candidate.component_id as jlong ); let foundation = JValue::Object( env.new_string(&candidate.foundation).unwrap().into() ); 62/67
signature descriptor for a method. $ javap -s -classpath app/src/main/java \ ch.dbrgn.candidateparser.IceCandidate Compiled from "IceCandidate.java" public class ch.dbrgn.candidateparser.IceCandidate { public ch.dbrgn.candidateparser.IceCandidate(); descriptor: ()V public ch.dbrgn.candidateparser.IceCandidate(java.lang.String, long, j descriptor: (Ljava/lang/String;JLjava/lang/String;JLjava/lang/String public java.lang.String getFoundation(); descriptor: ()Ljava/lang/String; ... 63/67
objects through the JNIEnv: let call_result = env.call_method( // Object containing the method obj, // Method name "setRelPort", // Method signature "(I)V", // Arguments &[JValue::Int(port as i32)] ); 64/67
JNIEnv, the original Rust memory can be freed (on drop) and the Java memory is tracked by the GC. We don’t need an explicit free_ice_candidate function. 65/67