interoptopus 0.16.0-alpha.6

The polyglot bindings generator for your library (C#, C, Python, ...). ๐Ÿ™
Documentation

crates.io-badge docs.rs-badge license-badge rust-version-badge rust-build-badge

Interoptopus ๐Ÿ™

The polyglot bindings generator for your library.

Write a robust library in Rust, easily access it from your second-favorite language:

  • Design a single .dll / .so in Rust, consume it from anywhere.
  • Get QoL features (e.g., classes, strings) in languages that have them.
  • Painless workflow, no external tooling required.
  • Easy to support more languages, backends fully decoupled from main project.

We strive to make our generated bindings zero cost. They should be as idiomatic as you could have reasonably written them yourself, but never magic or hiding the interface you actually wanted to expose.

Code you write ...

use interoptopus::{ffi, function};
use interoptopus::inventory::RustInventory;

#[ffi]
pub struct Vec2 {
    pub x: f32,
    pub y: f32,
}

#[ffi]
pub fn my_function(input: Vec2) {
    println!("{}", input.x);
}

// List functions you want to export, types are inferred.
pub fn ffi_inventory() -> RustInventory {
    RustInventory::new()
        .register(function!(my_function))
        .validate()
}

... Interoptopus generates

Language Crate Sample Output1 Status
C# interoptopus_csharp Interop.cs โœ…
C interoptopus_c - โฏ๏ธ
Python interoptopus_cpython - โฏ๏ธ
Other Write your own backend2 -

โœ… Tier 1 target. Active maintenance and production use. Full support of all features. โฏ๏ธ Tier 2 target. Currently suspended, contributors wanted! 1 For the reference project. 2 Add basic support for a new language in just a few hours. No pull request needed.

Getting Started ๐Ÿผ

If you want to ...

Supported Rust Constructs

See the reference project for an overview:

  • functions (freestanding functions and delegates)
  • types (composites, enums, opaques, references, ...)
  • constants ( primitive constants; results of const evaluation)
  • patterns (ASCII pointers, options, slices, ...)
  • services (turn to classes in C# and Python, and async methods)

Performance ๐Ÿ

Generated low-level bindings are zero cost w.r.t. hand-crafted bindings for that language. That said, even hand-crafted bindings encounter some target-specific overhead at the FFI boundary (e.g., marshalling, pinning, and safety checks). For C# that cost is often nanoseconds, for Python it can be microseconds.

For a quick overview, this table lists some common round trip times in ns / call, measured on .NET 10 and Windows 11:

C# -> Rust

The 'forward calling mode', i.e, a C# application calling an embedded Rust .dll. Used when you have a legacy app but want high-performance Rust under the hood.

Construct ns / call
primitive_void() 3
primitive_u64(0) 4
pattern_delegate_retained(delegate) 21
pattern_ascii_pointer("hello world") 20
pattern_utf8_string("hello world") 52
await serviceAsync.Success() 361 1

1 Full round trip to tokio and back. Although async calls have some intrinsic overhead (e.g., spawning a new TaskCompletionSource is ~100ns), some of that overhead appears to be a benchmarking effect when spin-waiting for a newly spawned task. In essence, if your application benefits from async this overhead is negligible, but simple getters or setters shouldn't needlessly be made async.

Rust -> .NET

The 'reverse calling mode', a Rust application loading a .NET .dll. Used when you have a modern Rust app, but need to rely on legacy .NET libraries.

Construct ns / call
plugin.primitive_void() 6
plugin.primitive_u32(42) 4
plugin.wire_hashmap_string({"foo": "bar"}).unwire() 951
plugin.wire_hashmap_string(16 x {_16: _16}).unwire() 5268
plugin.add_one(1).await 1097

Loading the .NET runtime and a plugin adds about ~20 MB to the process' memory footprint. Note this heavily depends on what your plugin actually does; the numbers here are for a 'hello world' use case:

Phase RSS (MB)
Pure Rust app 4.94
+ .NET Runtime 5.96
+ .NET Plugin Loaded 24.33
+ Method call 24.34

In essence, plain calls are near-zero overhead. Wire-based (JSON) transfers scale with payload size. Async calls add 300 ns to 1 ยตs due to task scheduling on both sides. The .NET runtime adds ~20 MB RSS on first plugin load.

Feature Flags

Gated behind feature flags, these enable:

  • derive - Proc macros such as #[ffi].
  • serde - Serde attributes on internal types.
  • tokio - Convenience support for async services via Tokio.
  • unstable-plugins - Experimental 'reverse interop' plugins. Not semver stable!

Changelog

  • v0.16 - Total rewrite: better architecture,safety, diagnostics.
  • v0.15 - Massive cleanup, bugfix, UX overhaul (+syn2).
  • v0.14 - Better inventory UX.
  • v0.13 - Python backend uses ctypes now.

Also see our upgrade instructions.

FAQ

Contributing

PRs are very welcome!

  • Submit small bug fixes directly. Major changes should be issues first.
  • New features or patterns must be materialized in the reference project and accompanied by at least an C# interop test.