Rust Interoperability: Seamlessly Working with Other Languages in 2025

13 min read 2757 words

Table of Contents

In today’s complex software landscape, few applications are built using a single programming language. Different languages offer different strengths, and existing codebases represent significant investments that can’t be rewritten overnight. This reality makes language interoperability—the ability for code written in different languages to work together seamlessly—a critical feature for any modern programming language. Rust, with its focus on safety, performance, and practicality, has developed robust interoperability capabilities that allow it to integrate smoothly with a wide range of other languages.

In this comprehensive guide, we’ll explore Rust’s interoperability features as they stand in early 2025. We’ll examine how Rust can interface with languages like C, C++, Python, JavaScript, Java, and more, providing practical examples and best practices for each. Whether you’re looking to gradually introduce Rust into an existing codebase, leverage specialized libraries from other languages, or build components that can be used across language boundaries, this guide will provide you with the knowledge and techniques you need to build effective multilingual software systems with Rust at their core.


Rust and C/C++ Interoperability

Rust’s interoperability with C and C++ is one of its strongest features, allowing for gradual migration and leveraging existing codebases:

Calling C from Rust

// Calling C functions from Rust
use std::os::raw::{c_char, c_int};
use std::ffi::{CStr, CString};

// Declare the external C functions
#[link(name = "mylib")]
extern "C" {
    fn add(a: c_int, b: c_int) -> c_int;
    fn process_string(s: *const c_char) -> *mut c_char;
    fn free_string(s: *mut c_char);
}

fn main() {
    // Call a simple C function
    let result = unsafe { add(5, 7) };
    println!("5 + 7 = {}", result);
    
    // Work with strings
    let input = CString::new("Hello from Rust").expect("CString::new failed");
    let output = unsafe {
        let raw_output = process_string(input.as_ptr());
        
        // Convert C string to Rust string and handle ownership
        let rust_output = CStr::from_ptr(raw_output).to_string_lossy().into_owned();
        
        // Free the C string to avoid memory leaks
        free_string(raw_output);
        
        rust_output
    };
    
    println!("Processed string: {}", output);
}

Calling Rust from C

// Exporting Rust functions to C
use std::os::raw::{c_char, c_int};
use std::ffi::{CStr, CString};

// Export a simple function to C
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
    a + b
}

// Export a string processing function to C
#[no_mangle]
pub extern "C" fn rust_process_string(s: *const c_char) -> *mut c_char {
    let c_str = unsafe {
        if s.is_null() {
            return std::ptr::null_mut();
        }
        CStr::from_ptr(s)
    };
    
    let rust_str = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => return std::ptr::null_mut(),
    };
    
    // Process the string
    let processed = format!("Processed by Rust: {}", rust_str);
    
    // Convert back to C string and transfer ownership to the caller
    let c_processed = CString::new(processed).expect("CString::new failed");
    c_processed.into_raw()
}

// Provide a function to free strings created by Rust
#[no_mangle]
pub extern "C" fn rust_free_string(s: *mut c_char) {
    unsafe {
        if !s.is_null() {
            let _ = CString::from_raw(s);
        }
    }
}
// C code calling Rust functions
#include <stdio.h>
#include <stdlib.h>

// Declare the Rust functions
extern int rust_add(int a, int b);
extern char* rust_process_string(const char* s);
extern void rust_free_string(char* s);

int main() {
    // Call a simple Rust function
    int result = rust_add(5, 7);
    printf("5 + 7 = %d\n", result);
    
    // Call a Rust string processing function
    const char* input = "Hello from C";
    char* output = rust_process_string(input);
    
    if (output) {
        printf("Processed string: %s\n", output);
        rust_free_string(output);
    } else {
        printf("Error processing string\n");
    }
    
    return 0;
}

Rust and C++ Interoperability

// Using cxx for safer Rust and C++ interoperability
// Cargo.toml: cxx = "1.0"

#[cxx::bridge]
mod ffi {
    // Shared structs
    struct Point {
        x: f64,
        y: f64,
    }
    
    // C++ functions accessible from Rust
    unsafe extern "C++" {
        include!("mylib.h");
        
        fn calculate_distance(p1: &Point, p2: &Point) -> f64;
        fn process_points(points: &[Point]) -> Vec<Point>;
    }
    
    // Rust functions accessible from C++
    extern "Rust" {
        fn create_point(x: f64, y: f64) -> Point;
        fn transform_point(point: &mut Point, scale: f64);
    }
}

// Implement the Rust functions
fn create_point(x: f64, y: f64) -> ffi::Point {
    ffi::Point { x, y }
}

fn transform_point(point: &mut ffi::Point, scale: f64) {
    point.x *= scale;
    point.y *= scale;
}

fn main() {
    let p1 = create_point(1.0, 2.0);
    let p2 = create_point(4.0, 6.0);
    
    let distance = ffi::calculate_distance(&p1, &p2);
    println!("Distance: {}", distance);
    
    let points = vec![p1, p2];
    let processed = ffi::process_points(&points);
    
    for (i, p) in processed.iter().enumerate() {
        println!("Processed point {}: ({}, {})", i, p.x, p.y);
    }
}

Rust and Python Interoperability

Python’s popularity and Rust’s performance make them natural companions:

PyO3: Rust Extensions for Python

// Using PyO3 to create Python extensions in Rust
// Cargo.toml:
// [dependencies]
// pyo3 = { version = "0.20", features = ["extension-module"] }

use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

// A simple function exposed to Python
#[pyfunction]
fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

// A class exposed to Python
#[pyclass]
struct RustCalculator {
    value: f64,
}

#[pymethods]
impl RustCalculator {
    #[new]
    fn new(initial_value: Option<f64>) -> Self {
        RustCalculator {
            value: initial_value.unwrap_or(0.0),
        }
    }
    
    fn add(&mut self, other: f64) -> PyResult<f64> {
        self.value += other;
        Ok(self.value)
    }
    
    fn subtract(&mut self, other: f64) -> PyResult<f64> {
        self.value -= other;
        Ok(self.value)
    }
    
    fn multiply(&mut self, other: f64) -> PyResult<f64> {
        self.value *= other;
        Ok(self.value)
    }
    
    fn divide(&mut self, other: f64) -> PyResult<f64> {
        if other == 0.0 {
            return Err(PyErr::new::<pyo3::exceptions::PyZeroDivisionError, _>("Cannot divide by zero"));
        }
        self.value /= other;
        Ok(self.value)
    }
    
    #[getter]
    fn value(&self) -> PyResult<f64> {
        Ok(self.value)
    }
}

// Module initialization
#[pymodule]
fn rust_extension(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
    m.add_class::<RustCalculator>()?;
    Ok(())
}
# Python code using the Rust extension
from rust_extension import fibonacci, RustCalculator

# Use the Rust fibonacci function
result = fibonacci(30)
print(f"Fibonacci(30) = {result}")

# Use the Rust calculator class
calc = RustCalculator(10.0)
print(f"Initial value: {calc.value}")
print(f"Add 5: {calc.add(5.0)}")
print(f"Multiply by 2: {calc.multiply(2.0)}")
print(f"Subtract 7: {calc.subtract(7.0)}")
print(f"Divide by 2: {calc.divide(2.0)}")

Calling Python from Rust

// Using PyO3 to call Python from Rust
// Cargo.toml:
// [dependencies]
// pyo3 = { version = "0.20", features = ["auto-initialize"] }

use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};

fn main() -> PyResult<()> {
    Python::with_gil(|py| {
        // Import Python modules
        let sys = py.import("sys")?;
        let version: String = sys.getattr("version")?.extract()?;
        println!("Python version: {}", version);
        
        // Execute Python code
        let result = py.eval("2 + 2", None, None)?;
        let four: i32 = result.extract()?;
        println!("2 + 2 = {}", four);
        
        // Call Python functions
        let math = py.import("math")?;
        let pi: f64 = math.getattr("pi")?.extract()?;
        let sin_of_pi: f64 = math.getattr("sin")?.call1((pi,))?.extract()?;
        println!("sin(pi) = {}", sin_of_pi);
        
        // Create Python objects
        let dict = PyDict::new(py);
        dict.set_item("key1", "value1")?;
        dict.set_item("key2", 42)?;
        
        let list = PyList::new(py, &[1, 2, 3, 4, 5]);
        
        // Call a Python function with multiple arguments
        let locals = PyDict::new(py);
        locals.set_item("dict", dict)?;
        locals.set_item("list", list)?;
        
        let result = py.eval("dict['key2'] + sum(list)", None, Some(locals))?;
        let sum_result: i32 = result.extract()?;
        println!("dict['key2'] + sum(list) = {}", sum_result);
        
        Ok(())
    })
}

Rust and JavaScript/WebAssembly Interoperability

Rust has become a leading language for WebAssembly development:

Rust to WebAssembly

// Using wasm-bindgen to create WebAssembly modules
// Cargo.toml:
// [dependencies]
// wasm-bindgen = "0.2"
// js-sys = "0.3"
// web-sys = { version = "0.3", features = ["console", "Document", "Element", "HtmlElement", "Window"] }

use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, HtmlElement};

// Export a simple function to JavaScript
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

// 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_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
    
    pub fn distance_to(&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(setter)]
    pub fn set_x(&mut self, x: f64) {
        self.x = x;
    }
    
    #[wasm_bindgen(getter)]
    pub fn y(&self) -> f64 {
        self.y
    }
    
    #[wasm_bindgen(setter)]
    pub fn set_y(&mut self, y: f64) {
        self.y = y;
    }
}

// DOM manipulation from Rust
#[wasm_bindgen]
pub fn create_element(tag: &str, text: &str, parent_id: &str) -> Result<(), JsValue> {
    // Get the document
    let window = web_sys::window().expect("No global window exists");
    let document = window.document().expect("No document exists");
    
    // Create a new element
    let element = document.create_element(tag)?;
    element.set_text_content(Some(text));
    
    // Append it to the parent
    let parent = document.get_element_by_id(parent_id)
        .ok_or_else(|| JsValue::from_str(&format!("No element with id {}", parent_id)))?;
    
    parent.append_child(&element)?;
    
    Ok(())
}
// JavaScript code using the Rust WebAssembly module
import { fibonacci, Point, create_element } from './rust_module';

// Use the Rust fibonacci function
console.log(`Fibonacci(10) = ${fibonacci(10)}`);

// Use the Rust Point class
const p1 = new Point(3, 4);
console.log(`Distance from origin: ${p1.distance_from_origin()}`);

const p2 = new Point(7, 7);
console.log(`Distance between points: ${p1.distance_to(p2)}`);

// Update point coordinates
p1.x = 5;
p1.y = 12;
console.log(`New distance from origin: ${p1.distance_from_origin()}`);

// Create a DOM element using Rust
document.body.innerHTML = '<div id="container"></div>';
create_element('p', 'This paragraph was created by Rust!', 'container');

Rust and Java/JVM Interoperability

Rust can interoperate with Java and other JVM languages:

Rust and Java via JNI

// Using JNI to create Java bindings for Rust
// Cargo.toml:
// [dependencies]
// jni = "0.21"

use jni::JNIEnv;
use jni::objects::{JClass, JString};
use jni::sys::{jint, jlong, jstring};

// Export a simple function to Java
#[no_mangle]
pub extern "system" fn Java_com_example_RustBindings_fibonacci(
    _env: JNIEnv,
    _class: JClass,
    n: jint,
) -> jint {
    match n {
        0 => 0,
        1 => 1,
        n if n > 0 => {
            let n1 = Java_com_example_RustBindings_fibonacci(_env, _class, n - 1);
            let n2 = Java_com_example_RustBindings_fibonacci(_env, _class, n - 2);
            n1 + n2
        }
        _ => 0,
    }
}

// Process a Java string
#[no_mangle]
pub extern "system" fn Java_com_example_RustBindings_processString(
    env: JNIEnv,
    _class: JClass,
    input: JString,
) -> jstring {
    // Convert Java string to Rust string
    let input: String = env
        .get_string(input)
        .expect("Couldn't get Java string!")
        .into();
    
    // Process the string
    let output = format!("Processed by Rust: {}", input);
    
    // Convert Rust string back to Java string
    env.new_string(output)
        .expect("Couldn't create Java string!")
        .into_raw()
}
// Java code calling Rust functions
package com.example;

public class RustBindings {
    // Load the native library
    static {
        System.loadLibrary("rust_bindings");
    }
    
    // Declare the native methods
    public static native int fibonacci(int n);
    public static native String processString(String input);
    
    public static void main(String[] args) {
        // Call the Rust fibonacci function
        int result = fibonacci(10);
        System.out.println("Fibonacci(10) = " + result);
        
        // Call the Rust string processing function
        String processed = processString("Hello from Java");
        System.out.println(processed);
    }
}

Rust and Go Interoperability

Rust and Go can work together through C bindings:

Rust to Go via CGo

// Exporting Rust functions for Go
use std::os::raw::{c_char, c_int};
use std::ffi::{CStr, CString};

#[no_mangle]
pub extern "C" fn rust_fibonacci(n: c_int) -> c_int {
    match n {
        0 => 0,
        1 => 1,
        n if n > 0 => rust_fibonacci(n - 1) + rust_fibonacci(n - 2),
        _ => 0,
    }
}

#[no_mangle]
pub extern "C" fn rust_process_string(s: *const c_char) -> *mut c_char {
    let c_str = unsafe {
        if s.is_null() {
            return std::ptr::null_mut();
        }
        CStr::from_ptr(s)
    };
    
    let rust_str = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => return std::ptr::null_mut(),
    };
    
    // Process the string
    let processed = format!("Processed by Rust: {}", rust_str);
    
    // Convert back to C string and transfer ownership to the caller
    let c_processed = CString::new(processed).expect("CString::new failed");
    c_processed.into_raw()
}

#[no_mangle]
pub extern "C" fn rust_free_string(s: *mut c_char) {
    unsafe {
        if !s.is_null() {
            let _ = CString::from_raw(s);
        }
    }
}
// Go code calling Rust functions
package main

/*
#cgo LDFLAGS: -L./target/release -lrust_lib
#include <stdlib.h>
#include <stdint.h>

int32_t rust_fibonacci(int32_t n);
char* rust_process_string(const char* s);
void rust_free_string(char* s);
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    // Call the Rust fibonacci function
    result := C.rust_fibonacci(10)
    fmt.Printf("Fibonacci(10) = %d\n", result)
    
    // Call the Rust string processing function
    input := C.CString("Hello from Go")
    defer C.free(unsafe.Pointer(input))
    
    output := C.rust_process_string(input)
    if output != nil {
        // Convert C string to Go string
        goString := C.GoString(output)
        fmt.Println(goString)
        
        // Free the string allocated by Rust
        C.rust_free_string(output)
    } else {
        fmt.Println("Error processing string")
    }
}

Best Practices for Rust Interoperability

Here are some best practices for working with Rust across language boundaries:

Memory Management

// Safe memory management across FFI boundaries

// 1. Always clean up resources
#[no_mangle]
pub extern "C" fn create_resource() -> *mut Resource {
    let resource = Box::new(Resource::new());
    Box::into_raw(resource)
}

#[no_mangle]
pub extern "C" fn destroy_resource(ptr: *mut Resource) {
    unsafe {
        if !ptr.is_null() {
            let _ = Box::from_raw(ptr);
        }
    }
}

// 2. Use RAII patterns when possible
struct ResourceWrapper {
    ptr: *mut Resource,
}

impl ResourceWrapper {
    fn new() -> Self {
        let ptr = unsafe { create_resource() };
        ResourceWrapper { ptr }
    }
}

impl Drop for ResourceWrapper {
    fn drop(&mut self) {
        unsafe { destroy_resource(self.ptr) }
    }
}

// 3. Be careful with strings
fn safe_string_handling(s: *const c_char) -> Result<String, &'static str> {
    if s.is_null() {
        return Err("Null pointer provided");
    }
    
    unsafe {
        CStr::from_ptr(s)
            .to_str()
            .map(|s| s.to_owned())
            .map_err(|_| "Invalid UTF-8")
    }
}

Error Handling

// Error handling across FFI boundaries

// 1. Use result codes
#[no_mangle]
pub extern "C" fn process_data(data: *const c_char, result: *mut c_char, result_len: *mut c_int) -> c_int {
    // Error codes
    const SUCCESS: c_int = 0;
    const NULL_POINTER_ERROR: c_int = 1;
    const UTF8_ERROR: c_int = 2;
    const PROCESSING_ERROR: c_int = 3;
    
    // Check for null pointers
    if data.is_null() || result.is_null() || result_len.is_null() {
        return NULL_POINTER_ERROR;
    }
    
    // Convert to Rust string
    let data_str = match unsafe { CStr::from_ptr(data) }.to_str() {
        Ok(s) => s,
        Err(_) => return UTF8_ERROR,
    };
    
    // Process the data
    let processed = match process_string(data_str) {
        Ok(s) => s,
        Err(_) => return PROCESSING_ERROR,
    };
    
    // Copy the result back
    let c_processed = match CString::new(processed) {
        Ok(s) => s,
        Err(_) => return PROCESSING_ERROR,
    };
    
    let bytes = c_processed.as_bytes_with_nul();
    if bytes.len() > unsafe { *result_len as usize } {
        unsafe { *result_len = bytes.len() as c_int };
        return BUFFER_TOO_SMALL_ERROR;
    }
    
    unsafe {
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), result as *mut u8, bytes.len());
        *result_len = bytes.len() as c_int;
    }
    
    SUCCESS
}

// 2. Use thread-local error storage
thread_local! {
    static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
}

#[no_mangle]
pub extern "C" fn get_last_error_length() -> c_int {
    LAST_ERROR.with(|error| {
        error.borrow().as_ref().map_or(0, |e| e.len() as c_int)
    })
}

#[no_mangle]
pub extern "C" fn get_last_error(buffer: *mut c_char, length: c_int) -> c_int {
    if buffer.is_null() {
        return -1;
    }
    
    LAST_ERROR.with(|error| {
        if let Some(error_str) = error.borrow().as_ref() {
            let bytes = error_str.as_bytes();
            let len = std::cmp::min(bytes.len(), length as usize);
            
            unsafe {
                std::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer as *mut u8, len);
                if len < length as usize {
                    *buffer.add(len) = 0;
                }
            }
            
            len as c_int
        } else {
            0
        }
    })
}

Performance Considerations

// Performance optimizations for FFI

// 1. Minimize crossing the FFI boundary
// Bad: Many small FFI calls
#[no_mangle]
pub extern "C" fn process_item(item: c_int) -> c_int {
    item * 2
}

// Good: Batch processing
#[no_mangle]
pub extern "C" fn process_items(items: *const c_int, count: c_int, results: *mut c_int) -> c_int {
    if items.is_null() || results.is_null() || count <= 0 {
        return -1;
    }
    
    let items_slice = unsafe { std::slice::from_raw_parts(items, count as usize) };
    let results_slice = unsafe { std::slice::from_raw_parts_mut(results, count as usize) };
    
    for i in 0..count as usize {
        results_slice[i] = items_slice[i] * 2;
    }
    
    0
}

// 2. Use zero-copy when possible
#[no_mangle]
pub extern "C" fn analyze_data(data: *const u8, len: c_int) -> c_int {
    if data.is_null() || len <= 0 {
        return -1;
    }
    
    // Create a slice without copying the data
    let slice = unsafe { std::slice::from_raw_parts(data, len as usize) };
    
    // Analyze the data
    slice.iter().filter(|&&x| x > 128).count() as c_int
}

Conclusion

Rust’s interoperability capabilities have matured significantly, making it an excellent choice for multilingual software projects. Whether you’re gradually migrating a C/C++ codebase to Rust, building high-performance Python extensions, creating WebAssembly modules for the web, or integrating with Java or Go, Rust provides the tools and patterns you need to create safe, efficient interfaces between languages.

The key takeaways from this exploration of Rust interoperability are:

  1. C/C++ interoperability is robust and well-supported, with tools like cxx making it safer and more ergonomic
  2. Python integration via PyO3 enables high-performance extensions while maintaining Python’s ease of use
  3. WebAssembly support makes Rust a leading language for browser-based applications
  4. JVM languages can leverage Rust’s performance through JNI
  5. Best practices around memory management, error handling, and performance are essential for successful interoperability

As software systems continue to grow in complexity, the ability to combine languages effectively becomes increasingly valuable. Rust’s strong interoperability story allows you to use the right tool for each job while maintaining safety and performance across language boundaries. By following the patterns and practices outlined in this guide, you can build robust multilingual systems that leverage the strengths of each language involved.

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