Rust Interoperability: Seamlessly Working with Other Languages

13 min read 2660 words

Table of Contents

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:

  1. FFI provides the foundation for calling C code from Rust and exposing Rust code to C
  2. Binding tools like PyO3, wasm-bindgen, and cxx simplify integration with specific languages
  3. Memory management requires careful attention when crossing language boundaries
  4. Clear API boundaries help manage complexity in multi-language projects
  5. 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.

Andrew
Andrew

Andrew is a visionary software engineer and DevOps expert with a proven track record of delivering cutting-edge solutions that drive innovation at Ataiva.com. As a leader on numerous high-profile projects, Andrew brings his exceptional technical expertise and collaborative leadership skills to the table, fostering a culture of agility and excellence within the team. With a passion for architecting scalable systems, automating workflows, and empowering teams, Andrew is a sought-after authority in the field of software development and DevOps.

Tags

Recent Posts