Olúṣeun

Interface
Foreign Function Interface in Rust



I was considering working on a program that would require scanning for publicly available Wi-Fi that is compatible across major operating systems. I looked up available crates that could handle such a task, but to no avail - no actively maintained crate did that.

Building from scratch seemed like the go-to option at that point; however, I only had access to macOS, hence testing for other OSes was a pain in the ass, and honestly, it didn’t feel like it was worth it.

So I decided to explore one of the unmaintained crates. Last commit? Over 5 years ago.

I tried it locally on macOS only to find out it wasn’t working and returned an interesting error:

 thread 'main' (4539332) panicked at src/bin/main.rs:2:40:
 Cannot scan network: CommandNotFound
 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 

Now that’s interesting.

Not much context was provided, but we had a clue - the command we were using is most likely not available or simply not found.

Back to the code. The error could be traced to how it attempts to get available Wi-Fi networks.

It uses Rust’s standard library std::process::Command, which allows you to execute Unix-based commands, basically what you’d normally type in your terminal.

In this case, the crate calls the macOS airport CLI:

 Command::new(
    "/System/Library/PrivateFrameworks/Apple80211.\
        framework/Versions/Current/Resources/airport",
    )
    .arg("-s")
    .output()
    .map_err(|_| Error::CommandNotFound)?; 

As we can see, our error originated from here.

But why?

A few years ago, Apple deprecated that API, citing security concerns. The airport binary still exists in some versions, but its usage has been discouraged and restricted. So relying on it programmatically is fragile at best.

Instead, Apple provides access via its Objective-C framework called CoreWLAN.

CoreWLAN allows you to scan for available Wi-Fi networks, but it comes with caveats. Some fields, like BSSID, security type, etc., may not always be exposed depending on permissions and OS version.

So now we have a new problem.

I’m writing in Rust. CoreWLAN API is exposed in Objective-C.

How do we bridge that?

Enters FFI.

So, what is Foreign Function Interface (FFI)?

Foreign Function Interface (FFI) is a mechanism that allows programmers writing in one language to call functions written in another language.

Rust has first-class support for FFI, particularly with C and C++. Which makes sense as Rust was designed to interoperate with existing C/C++ ecosystems, and it uses the same underlying linking model

In our case, we were trying to access Objective-C++ code. Rust does not directly interoperate with Objective-C++. However, since Objective-C++ is essentially C++ with Objective-C extensions, we can expose a C-compatible interface and link against it.

Back to Rust, piecing all these together.

Introducing Our “Foreign Friends” to Rust

Logically, it would make sense to first build our foreign friends before Rust builds the crate, and that’s exactly what happens.

First, we have to introduce our foreign friends to Rust by introducing our build script [build.rs]. build.rs lives at the root of your project. When you run cargo build, Cargo automatically runs it before compiling your Rust code.

For this program, we use the cc crate.

Add it under build-dependencies

 [build-dependencies]
 cc = "1.2.14" 

That checked. Let’s introduce our objective c++ file to cc to compile for us

 cc::Build::new()
    .file("src/ffi_c/scan.mm")
    .cpp(true) // important for Objective-C++
    .flag_if_supported("-std=c++17") // optional
    .compile("wifiscanner"); 

This compiles our Objective-C++ file (.mm) and links it into our Rust project.

From there, we can then import our C++ function to our Rust code via the extern block, where we declare our foreign functions here, looking similar to what we have in our C++ code

 unsafe extern "C" {
     pub(crate) fn cc_scan() -> WifiScanResult;
     pub(crate) fn cc_free_scan(result: WifiScanResult);
 } 

The full implementation can be found here

Let’s explore another Example

This time more straightforward. We’d be using another library called bindgen. bindgen approaches ffi differently; this time, it automatically generates Rust FFI bindings, so instead of having an extern block with functions with the exact function name as the C++ function, bindgen does it for you.

Just as we did with cc, we'd also import our bindgen crate via the build-dependencies section of our Cargo.toml

 [build-dependencies]
 bindgen = "0.55.1"

That also means you have to declare where you want the generated code to exist. Customarily, it’s saved in our OUT DIR. We get that done by adding this to our build script

 let bindings = bindgen::Builder::default()
    .header("libebpf.h")
    .clang_arg("-I/opt/homebrew/include")
    .allowlist_function("bpf_object__load")
    .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
    .generate()
    .expect("Unable to generate bindings");

 let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

 bindings
    .write_to_file(out_path.join("bindings.rs"))
    .expect("Couldn't write bindings!"); 

You might be wondering why not use bindgen in the first example. The reason is that, for the first example, Rust cannot directly call Objective-C++ objects hence need for a C wrapper as a bridge.

In this case, we’re calling the bpf_object__load “user” function from libbpf, which is part of the eBPF ecosystem.

Speaking of which, eBPF is a Linux kernel technology that allows safe, sandboxed programs to run inside the kernel written in C++.

With all these checked, we can then call our generated code in our code:

 mod ffi {
    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
 }

 let obj = unsafe { ffi::bpf_object__load(obj_struct) }; 

The full implementation can be found here

Final note

When developing programs, huddles would always come up, some annoying, but they must be crossed. And as we have seen here, we have no choice but to interact with code written in a completely different language.

Thanks to FFI, we’re able to achieve our goals