One of Rust’s greatest strengths is its ability to interoperate with other programming languages. This interoperability allows developers to gradually introduce Rust into existing projects, leverage specialized libraries from other ecosystems, and build components that can be used across different platforms and languages. Whether you’re looking to speed up a Python application with Rust, integrate Rust components into a C++ codebase, or expose Rust functionality to JavaScript, the language provides robust tools and patterns for seamless integration.
In this comprehensive guide, we’ll explore Rust’s interoperability capabilities with various languages, from low-level C and C++ to high-level Python and JavaScript. You’ll learn about the Foreign Function Interface (FFI), binding generation tools, and best practices for creating safe and efficient cross-language interfaces. By the end, you’ll have a solid understanding of how to leverage Rust’s strengths while preserving your investments in existing codebases and ecosystems.
Understanding Rust’s Foreign Function Interface (FFI)
At the core of Rust’s interoperability is its Foreign Function Interface (FFI), which allows Rust code to call and be called by code written in other languages:
Calling C from Rust
// Declaring external C functions
use std::os::raw::{c_char, c_int};
// Link to the C standard library
#[link(name = "c")]
extern "C" {
// Declare the C function signatures
fn strlen(s: *const c_char) -> usize;
fn printf(format: *const c_char, ...) -> c_int;
}
fn main() {
// Create a C-compatible string
let c_string = std::ffi::CString::new("Hello from Rust!").unwrap();
unsafe {
// Call C's strlen function
let length = strlen(c_string.as_ptr());
println!("String length according to C's strlen: {}", length);
// Call C's printf function
printf(b"Printing from C: %s\n\0".as_ptr() as *const c_char, c_string.as_ptr());
}
}
Exposing Rust Functions to C
// Export a Rust function with C calling convention
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
// Export a Rust function that handles strings
#[no_mangle]
pub extern "C" fn process_string(input: *const c_char) -> *mut c_char {
// Safety: Ensure the pointer is valid
let c_str = unsafe {
if input.is_null() {
return std::ptr::null_mut();
}
std::ffi::CStr::from_ptr(input)
};
// Convert to Rust string and process
let rust_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let processed = format!("Processed: {}", rust_str.to_uppercase());
// Convert back to C string
let output = match std::ffi::CString::new(processed) {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
// Transfer ownership to the caller
output.into_raw()
}
// Function to free memory allocated by Rust
#[no_mangle]
pub extern "C" fn free_string(ptr: *mut c_char) {
unsafe {
if !ptr.is_null() {
let _ = std::ffi::CString::from_raw(ptr);
// Memory is freed when CString is dropped
}
}
}
Building a C-Compatible Library in Rust
To create a shared library that can be used from C:
# Cargo.toml
[package]
name = "rust_lib"
version = "0.1.0"
edition = "2021"
[lib]
name = "rust_lib"
crate-type = ["cdylib"] # Creates a dynamic library with C ABI
// src/lib.rs
use std::os::raw::c_char;
use std::ffi::{CStr, CString};
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn hello() -> *mut c_char {
let s = CString::new("Hello from Rust!").unwrap();
s.into_raw()
}
#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
unsafe {
if !s.is_null() {
let _ = CString::from_raw(s);
}
}
}
Rust and C++
Integrating Rust with C++ requires additional considerations due to C++’s more complex features:
Using cxx for Safe Rust and C++ Integration
The cxx
crate provides a safe bridge between Rust and C++:
// src/lib.rs
#[cxx::bridge]
mod ffi {
// Shared structs
struct Point {
x: f32,
y: f32,
}
// Functions implemented in C++
unsafe extern "C++" {
include!("rust_lib/include/cpp_functions.h");
fn calculate_distance(p1: &Point, p2: &Point) -> f32;
fn create_point(x: f32, y: f32) -> Point;
}
// Functions implemented in Rust
extern "Rust" {
fn midpoint(p1: &Point, p2: &Point) -> Point;
}
}
// Implement the Rust function
fn midpoint(p1: &ffi::Point, p2: &ffi::Point) -> ffi::Point {
ffi::Point {
x: (p1.x + p2.x) / 2.0,
y: (p1.y + p2.y) / 2.0,
}
}
// include/cpp_functions.h
#pragma once
#include "rust_lib/src/lib.rs.h"
float calculate_distance(const Point& p1, const Point& p2);
Point create_point(float x, float y);
// src/cpp_functions.cpp
#include "rust_lib/include/cpp_functions.h"
#include <cmath>
float calculate_distance(const Point& p1, const Point& p2) {
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
return std::sqrt(dx * dx + dy * dy);
}
Point create_point(float x, float y) {
Point p;
p.x = x;
p.y = y;
return p;
}
Using bindgen for C++ Header Parsing
For more complex C++ libraries, bindgen
can automatically generate Rust bindings:
// build.rs
use std::env;
use std::path::PathBuf;
fn main() {
// Tell cargo to invalidate the built crate whenever the header changes
println!("cargo:rerun-if-changed=include/library.hpp");
// Generate bindings
let bindings = bindgen::Builder::default()
.header("include/library.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
// Write the bindings to an output file
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
// src/lib.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
// Include the generated bindings
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
// Safe wrapper functions
pub fn safe_cpp_function(value: i32) -> i32 {
unsafe { cpp_function(value) }
}
Rust and Python
Python’s popularity for data science, web development, and scripting makes it a common integration target for Rust:
Using PyO3 for Python Bindings
The PyO3
crate makes it easy to create Python extensions in Rust:
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
/// Calculate the fibonacci sequence up to n
#[pyfunction]
fn fibonacci(n: u64) -> PyResult<Vec<u64>> {
if n == 0 {
return Ok(vec![]);
} else if n == 1 {
return Ok(vec![0]);
}
let mut sequence = vec![0, 1];
while sequence.len() < n as usize {
let next = sequence[sequence.len() - 1] + sequence[sequence.len() - 2];
sequence.push(next);
}
Ok(sequence)
}
/// A Python module implemented in Rust
#[pymodule]
fn rust_extension(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
Ok(())
}
Creating Python Classes in Rust
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
#[pyclass]
struct Point {
x: f64,
y: f64,
}
#[pymethods]
impl Point {
#[new]
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
#[getter]
fn get_x(&self) -> f64 {
self.x
}
#[getter]
fn get_y(&self) -> f64 {
self.y
}
#[setter]
fn set_x(&mut self, x: f64) {
self.x = x;
}
#[setter]
fn set_y(&mut self, y: f64) {
self.y = y;
}
fn __repr__(&self) -> PyResult<String> {
Ok(format!("Point({}, {})", self.x, self.y))
}
}
#[pymodule]
fn geometry(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Point>()?;
Ok(())
}
Using Rust from Python
Once you’ve built your Rust extension, you can use it from Python:
# Python code
import rust_extension
# Call Rust function
sequence = rust_extension.fibonacci(10)
print(f"Fibonacci sequence: {sequence}")
# Use Rust class
from geometry import Point
p1 = Point(1.0, 2.0)
p2 = Point(4.0, 6.0)
print(f"Distance: {p1.distance(p2)}")
p1.translate(2.0, 3.0)
print(f"New position: {p1}")
Rust and JavaScript/WebAssembly
WebAssembly (Wasm) allows Rust code to run in web browsers and Node.js:
Using wasm-bindgen for JavaScript Integration
use wasm_bindgen::prelude::*;
// Export a function to JavaScript
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Export a struct to JavaScript
#[wasm_bindgen]
pub struct Point {
x: f64,
y: f64,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Point {
Point { x, y }
}
pub fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
#[wasm_bindgen(getter)]
pub fn x(&self) -> f64 {
self.x
}
#[wasm_bindgen(getter)]
pub fn y(&self) -> f64 {
self.y
}
#[wasm_bindgen(setter)]
pub fn set_x(&mut self, x: f64) {
self.x = x;
}
#[wasm_bindgen(setter)]
pub fn set_y(&mut self, y: f64) {
self.y = y;
}
}
// Import JavaScript functions
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// Use imported JavaScript functions
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
log(&format!("Greeted {}", name));
}
Using Rust-Generated WebAssembly in JavaScript
// JavaScript code
import { add, Point, greet } from './rust_lib';
// Call Rust function
console.log(`2 + 3 = ${add(2, 3)}`);
// Use Rust class
const p1 = new Point(1.0, 2.0);
const p2 = new Point(4.0, 6.0);
console.log(`Distance: ${p1.distance(p2)}`);
p1.x = 3.0;
console.log(`New x coordinate: ${p1.x}`);
// Call Rust function that uses JavaScript functions
greet("World");
Building a Web Application with Rust and WebAssembly
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, HtmlElement, Window};
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
// Get the window object
let window = web_sys::window().expect("No global window exists");
let document = window.document().expect("No document exists");
// Create a button
let button = document.create_element("button")?;
button.set_inner_html("Click me");
// Add an event listener
let closure = Closure::wrap(Box::new(move || {
let window = web_sys::window().unwrap();
window.alert_with_message("Button clicked!").unwrap();
}) as Box<dyn FnMut()>);
button.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closure.forget(); // Prevent the closure from being dropped
// Append the button to the body
let body = document.body().expect("No body exists");
body.append_child(&button)?;
Ok(())
}
Rust and Java/JVM
For Java and other JVM languages, there are several options for interoperability:
Using jni-rs for Java Native Interface
use jni::JNIEnv;
use jni::objects::{JClass, JString};
use jni::sys::jstring;
#[no_mangle]
pub extern "system" fn Java_com_example_RustLib_hello(env: JNIEnv, _class: JClass) -> jstring {
// Create a Java string from a Rust string
let output = env.new_string("Hello from Rust!")
.expect("Couldn't create Java string!");
// Convert to raw pointer that Java understands
output.into_raw()
}
#[no_mangle]
pub extern "system" fn Java_com_example_RustLib_add(
_env: JNIEnv,
_class: JClass,
a: i32,
b: i32,
) -> i32 {
a + b
}
// Java code
package com.example;
public class RustLib {
static {
System.loadLibrary("rust_lib");
}
// Declare native methods
public static native String hello();
public static native int add(int a, int b);
public static void main(String[] args) {
System.out.println(hello());
System.out.println("2 + 3 = " + add(2, 3));
}
}
Using j4rs for Higher-Level Java Integration
use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a JVM
let jvm = JvmBuilder::new().build()?;
// Create a Java string
let string_arg = InvocationArg::try_from("Hello from Rust!")?;
let java_string = jvm.create_instance("java.lang.String", &[string_arg])?;
// Call a method on the Java string
let uppercase = jvm.invoke(&java_string, "toUpperCase", &[])?;
// Convert the result back to a Rust string
let result: String = jvm.to_rust(uppercase)?;
println!("Result: {}", result);
Ok(())
}
Rust and Go
While less common, Rust and Go can interoperate through C bindings:
Calling Go from Rust
First, create a Go library with C exports:
package main
import "C"
import "fmt"
//export Add
func Add(a, b int) int {
return a + b
}
//export SayHello
func SayHello(name *C.char) *C.char {
goName := C.GoString(name)
greeting := fmt.Sprintf("Hello, %s from Go!", goName)
return C.CString(greeting)
}
func main() {}
Then, call it from Rust:
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
#[link(name = "go_lib")]
extern "C" {
fn Add(a: c_int, b: c_int) -> c_int;
fn SayHello(name: *const c_char) -> *mut c_char;
}
fn main() {
// Call Go's Add function
let result = unsafe { Add(2, 3) };
println!("2 + 3 = {}", result);
// Call Go's SayHello function
let name = CString::new("Rust").unwrap();
let greeting = unsafe {
let ptr = SayHello(name.as_ptr());
let result = CStr::from_ptr(ptr).to_string_lossy().into_owned();
libc::free(ptr as *mut libc::c_void);
result
};
println!("{}", greeting);
}
Calling Rust from Go
First, create a Rust library with C exports:
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
#[no_mangle]
pub extern "C" fn add(a: c_int, b: c_int) -> c_int {
a + b
}
#[no_mangle]
pub extern "C" fn say_hello(name: *const c_char) -> *mut c_char {
let c_str = unsafe {
if name.is_null() {
return std::ptr::null_mut();
}
CStr::from_ptr(name)
};
let rust_str = match c_str.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let greeting = format!("Hello, {} from Rust!", rust_str);
CString::new(greeting).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn free_string(ptr: *mut c_char) {
unsafe {
if !ptr.is_null() {
let _ = CString::from_raw(ptr);
}
}
}
Then, call it from Go:
package main
/*
#cgo LDFLAGS: -L. -lrust_lib
#include <stdlib.h>
#include <stdint.h>
int32_t add(int32_t a, int32_t b);
char* say_hello(const char* name);
void free_string(char* ptr);
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// Call Rust's add function
result := C.add(2, 3)
fmt.Printf("2 + 3 = %d\n", result)
// Call Rust's say_hello function
name := C.CString("Go")
defer C.free(unsafe.Pointer(name))
greeting := C.say_hello(name)
defer C.free_string(greeting)
fmt.Println(C.GoString(greeting))
}
Best Practices for Rust Interoperability
Based on experience from real-world projects, here are some best practices for Rust interoperability:
1. Define Clear Boundaries
Decide which parts of your system will be implemented in which language, and define clear interfaces between them:
// Define a clear API boundary
#[no_mangle]
pub extern "C" fn process_data(
input: *const u8,
input_len: usize,
output: *mut u8,
output_len: *mut usize,
) -> i32 {
// Safety checks
if input.is_null() || output.is_null() || output_len.is_null() {
return -1; // Invalid input
}
// Convert raw pointers to Rust slices
let input_slice = unsafe { std::slice::from_raw_parts(input, input_len) };
let output_capacity = unsafe { *output_len };
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, output_capacity) };
// Process the data
// ...
// Update output length
let actual_output_len = /* actual length */;
unsafe { *output_len = actual_output_len; }
0 // Success
}
2. Handle Memory Management Carefully
Be explicit about who owns memory and how it should be freed:
// Rust side
#[no_mangle]
pub extern "C" fn create_string() -> *mut c_char {
let s = CString::new("Hello, World!").unwrap();
s.into_raw() // Transfer ownership to caller
}
#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
unsafe {
if !s.is_null() {
let _ = CString::from_raw(s); // Take ownership back and drop
}
}
}
// C side
char* str = create_string();
printf("%s\n", str);
free_string(str); // Must call this to avoid memory leak
3. Use High-Level Binding Tools When Possible
For complex integrations, use specialized binding tools:
// Using wasm-bindgen for JavaScript
#[wasm_bindgen]
pub struct User {
name: String,
age: u32,
}
#[wasm_bindgen]
impl User {
#[wasm_bindgen(constructor)]
pub fn new(name: String, age: u32) -> User {
User { name, age }
}
#[wasm_bindgen(getter)]
pub fn name(&self) -> String {
self.name.clone()
}
#[wasm_bindgen(getter)]
pub fn age(&self) -> u32 {
self.age
}
}
4. Write Tests Across Language Boundaries
Test your interoperability code from both languages:
// Rust test for C interoperability
#[test]
fn test_c_interop() {
unsafe {
let result = c_function(42);
assert_eq!(result, 84);
}
}
# Python test for Rust interoperability
import unittest
import rust_module
class TestRustInterop(unittest.TestCase):
def test_fibonacci(self):
result = rust_module.fibonacci(10)
self.assertEqual(result, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34])
if __name__ == '__main__':
unittest.main()
5. Document FFI Contracts Clearly
Document the expected behavior, memory ownership, and thread safety of your FFI functions:
/// Processes an image buffer and returns a new processed buffer.
///
/// # Safety
///
/// - `input` must be a valid pointer to a buffer of at least `width * height * 4` bytes
/// - `width` and `height` must be non-zero
/// - The function allocates a new buffer that must be freed with `free_image_buffer`
///
/// # Returns
///
/// A pointer to the processed image buffer, or null on error
#[no_mangle]
pub unsafe extern "C" fn process_image(
input: *const u8,
width: u32,
height: u32,
) -> *mut u8 {
// Implementation...
}
Conclusion
Rust’s interoperability capabilities make it an excellent choice for incrementally adopting the language in existing projects or for building components that need to work across language boundaries. By leveraging Rust’s FFI and the ecosystem of binding tools, you can combine Rust’s performance and safety with the strengths of other languages and their ecosystems.
The key takeaways from this exploration of Rust interoperability are:
- FFI provides the foundation for calling C code from Rust and exposing Rust code to C
- Binding tools like PyO3, wasm-bindgen, and cxx simplify integration with specific languages
- Memory management requires careful attention when crossing language boundaries
- Clear API boundaries help manage complexity in multi-language projects
- Testing across languages ensures reliable interoperability
Whether you’re speeding up a Python application with Rust, integrating Rust components into a C++ codebase, or building WebAssembly modules for the web, Rust’s interoperability features provide the tools you need to create seamless integrations while maintaining safety and performance.