惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tor Project blog
B
Blog RSS Feed
M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
H
Hackread – Cybersecurity News, Data Breaches, AI and More
罗磊的独立博客
GbyAI
GbyAI
N
Netflix TechBlog - Medium
博客园 - 司徒正美
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
W
WeLiveSecurity
Stack Overflow Blog
Stack Overflow Blog
Y
Y Combinator Blog
SecWiki News
SecWiki News
V
Vulnerabilities – Threatpost
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Register - Security
The Register - Security
Cisco Talos Blog
Cisco Talos Blog
Martin Fowler
Martin Fowler
A
About on SuperTechFans
S
Security @ Cisco Blogs
T
Tenable Blog
C
Check Point Blog
N
News and Events Feed by Topic
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Attack and Defense Labs
Attack and Defense Labs
美团技术团队
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Cisco Blogs
P
Palo Alto Networks Blog
V
V2EX
博客园 - 聂微东
Project Zero
Project Zero
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Docker
N
News | PayPal Newsroom
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
小众软件
小众软件
Application and Cybersecurity Blog
Application and Cybersecurity Blog
人人都是产品经理
人人都是产品经理
V2EX - 技术
V2EX - 技术
I
Intezer
L
LINUX DO - 最新话题

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
Diplomat: Multi-language FFI for Rust libraries
Manish Goregaokar · 2026-06-15 · via Lobsters

This is a post I’ve been meaning to write and publish for years, and only recently got around to doing it. I’m hoping to get back into writing more!

For the past few years, as a part of my work on ICU4X, I’ve been working on Diplomat, a multi-language unidirectional FFI tool for wrapping Rust libraries.

I originally designed Diplomat in 2021 as a response to the question “What is the best way to expose ICU4X (A Rust library) to other programming languages?”. For context, while written in Rust, one of ICU4X’s core design goals was to be available to any programming language, starting with a core set and expanding over time. This is in contrast to the existing Unicode libraries ICU4C and ICU4J, which serve C/C++ and Java respectively.

In the long run, for such a project, tooling becomes a necessity. If ICU4X was just being exposed to a single language, this could potentially be feasible: someone manually writes FFI for every new API that gets written in Rust, and you need to ramp up at least part of the team on writing FFI for one particular language. However, as the number of languages you wish to support grows, this becomes more and more untenable. It is unreasonable to expect most members of an engineering team to be experts on the FFI peculiarities of C++, JS, Dart, the JVM, etc.

When we were getting started, I performed an investigation of the available tooling at the time, and arrived at the conclusion that none of the existing tools served our use case: a library in Rust wishing to expose an API to multiple languages. Some of these tools answered part of the story but would need to be stitched together with other work. I also wrote down a design for my “pie in the sky FFI tool” that I figured would be too much of a yak shave to build, but would fill this gap in the Rust FFI tooling ecosystem I have felt for a long time. In the meantime, we stuck to manually written C bindings as we were still figuring stuff out.

One of the core reasons the existing FFI tools didn’t work was that they weren’t “unidirectional”, they were “bidirectional”, or “unidirectional” but going in the opposite direction1.

Speech bubble for character Confused pion

What’s “unidirectional” and “bidirectional” in the context of an FFI tool?

So, it’s possible this is terminology I just made up one day2, but it’s an ontology that I’ve found useful on many, many occasions, so I think it’s worth introducing

In general when doing FFI there are, broadly speaking, two distinct possible goals, with distinct characteristics.

One use case, served by tools like bindgen, cbindgen, wasm-bindgen, uniffi, and PyO3, is when you have a library in one language which you wish to use from another language. This is “unidirectional” FFI, since the wrapped library doesn’t need to know anything about the codebase calling into it.

Note that calls in “unidirectional” FFI can still go in both ways; a unidirectional FFI tool may support things like callbacks that allow the calling codebase to pass a closure to the library and have the library invoke it. This is still unidirectional since the API definition is within the wrapped library.

The other use case, served by tools like cxx, autocxx, crubit, and swift-bridge is where you are working on a combined codebase of two languages and need interop in “both ways”, e.g. you need Rust to be able to access C++ APIs and C++ to be able to access Rust APIs. This is the kind of interop situation I recall when working on Stylo, the project to use Servo’s style system in Firefox. Even with Servo being relatively modular, this was not a case of “call Servo like a library”, it was a case of integrating two codebases with a somewhat jagged API boundary. At the time there was not much tooling and we managed to convince bindgen to work for this, however this was very much a “bidirectional” use case.

Bidirectional tools can often be used for unidirectional use cases, but they are also usually designed with those two specific languages in mind, which constrains the utility of the underlying bindings for work with other languages. You can’t use the bindings as a neutral “hub” that many languages radiate out from.

When designing Diplomat, there were several things I had in mind that may not necessarily match choices made by other FFI tools:

No action-at-a-distance

Editing your regular library Rust code should never silently change your FFI layer. I did not want Diplomat to parse the full dependency graph: it should be abundantly clear when an edit to code is going to change the FFI layer, by restricting what Diplomat consumes to specially-tagged “bridge”3 code. In ICU4X, the FFI layer only changes when people update the Diplomat “bridge” code living under ffi/capi.

Speech bubble for character Confused pion

Why is this a useful property for a tool to have?

For one, it’s just easier to design a tool when it does not need to parse the full range of what Rust supports. Since Diplomat’s “bridge” code is only intended for consumption from Diplomat, we can forbid weird Rust things from being used there.

Speech bubble for character Negative pion

That means you, for<'a>.

Secondly, the FFI tool should not overly constrain the API exposed to regular Rust users; it should be possible to tailor that API to Rust user’s needs without having to think about other languages.

Finally, it’s extremely annoying for library developers if every part of their library is being monitored by a tool which may need to be worked around / pacified. ICU4X developers absolutely need to know how to operate Diplomat so that they can write FFI for every ICU4X API they design, however ought not need to constantly think about it when just designing the primary Rust code.

Generate a ready-to-use library

Diplomat should generate a ready-to-use library, not low level bindings. As such it should generate APIs that are idiomatic in the target language, and expose some degree of per-language configurability to allow the developer choices in how precisely to expose various functionality.

No IDLs

Ideally, the interface is smoothly specified in Rust code, rather than using some interface description language. This is an aesthetic choice; IDLs can work really well as well, and this is an option made available by uniffi.

Extensible for more languages

It should not be super hard to extend Diplomat to be able to produce bindings for more languages. The vision was that if we have people asking for a Dart API in ICU4X, we can write a Diplomat “backend” for Dart, and run it on the preexisting ICU4X Diplomat bridge code.

Speech bubble for character Positive pion

In fact, that’s exactly what happened, and ICU4X now has a Dart API.

This means that Diplomat’s constraints and design should from the get-go take into account the diversity of languages it may end up supporting: if a feature does not make sense for a particular language, it may need to be redesigned or made conditional.

Speech bubble for character Positive pion

This also means that third parties can build their own Diplomat backends if they wish, either by using Diplomat as a library, or by contributing upstream. This has happened multiple times: the Kotlin and Python backends were not written by the ICU4X team, though ICU4X now uses the Kotlin backend and is considering using the Python one!

An additional facet of extensibility is that Diplomat features themselves ought not to need support in all backends. If the person developing the Kotlin backend wants callback support, they need not figure out how to add it to all of the other backends, and the other backends also need not worry about callbacks for the most part — they can just mark it as unsupported.

This particular property has led to an explosion of features in Diplomat: at this point we have multiple users each who care about a different subset of backends, and they can each build the features they need without worrying too much about overcomplicating things for other users. Then, when those other users want those features, it’s much easier for them to adopt them.

Using Diplomat

The core workflow behind Diplomat is that you write a single “bridge crate” that wraps your Rust API, which, using a proc macro generates a common underlying extern "C" API. You can then run diplomat-tool on the bridge crate, invoking individual per-language “backends” to generate idiomatic language bindings that under the hood call the same underlying extern "C" APIs. This hub-and-spoke model means one bridge crate backs every language you target.

For example, you may write something like this:

#[diplomat::bridge]
mod ffi {
    pub struct Settings {
        pub something: u8,
        pub something_else: bool
    }

    #[diplomat::opaque]
    pub struct MyObject(my_library::MyObject);

    impl MyObject {
        #[diplomat::attr(auto, constructor)]
        pub fn create(settings: Settings) -> Box<MyObject> {
            Box::new(MyObject::new(settings))
        }

        pub fn do_thing(&self) {
            self.0.do_thing();
        }
    }

}

This will (via a proc macro) generate extern "C" APIs that look something like:

extern "C" fn MyObject_new(settings: Settings) -> *mut MyObject {...}
extern "C" fn MyObject_do_thing(this: &MyObject) {...}

as well as adding a repr(C) to Settings.

You can then pick a supported language, run diplomat-tool <language> <path> and generate bindings to that path.

Currently, we have c, cpp, js (includes TypeScript), dart, kotlin, and python-nanobind backends. There’s also a java backend being developed in a separate repo. We’re always looking for more!

In C++, this may generate a struct Settings and a class MyObject with methods create() and do_thing(). In JS it would have a similar class, but create() would be a constructor, and do_thing() would be renamed to doThing(). For a further idiomatic tweak, new MyObject() would accept untyped objects with the same fields as Settings as well. In both cases, the constructor/methods will call MyObject_new/MyObject_do_thing under the hood.

Diplomat supports three kinds of “custom” user-defined types: C-like enums, structs, and “opaques”. Structs are copied over the FFI boundary, whereas “opaques” wrap an underlying, opaque-to-foreign-languages Rust object that is behind an allocation and only ever passed around behind an owned or borrowed pointer. Some Diplomat backends also support traits as a fourth kind of custom type, letting the user plug in their own implementation of an interface.

Diplomat also supports Options, Results, and slices, mapping them to the target language’s idiomatic nullability, error, and list models. For example, a Rust Result throws an exception in JS, Dart, and Python, but maps to Kotlin’s Result type.

For a full list of types Diplomat supports passing across the FFI boundary, see the types chapter in the Diplomat book.

Customization

Diplomat supports a fair amount of customization. In the example code you can see #[diplomat::attr(auto, constructor)], which means that for backends which support constructors, create() is a constructor. The first argument for attr is a cfg-like syntax for selecting backends, and auto mostly means “select backends where the attribute is supported”. For constructors, Dart, JS, and Kotlin support them, but the C++ and C backends don’t.

Speech bubble for character Confused pion

Why doesn’t the C++ backend support constructors? C++ has constructors, yes?

Opaque types in C++ are behind a unique_ptr, and C++ doesn’t let you have constructors that return other types. We might still add some way of doing constructory things in C++, but for now having to write MyObject::create() is fine.

Diplomat supports a lot of customization via attributes, and all of these can be conditioned on specific backends or feature availability:

  • disable: Disabling APIs. This is useful to do if a backend doesn’t support features needed there, or if the API is a backend-specific optimization. You can also use #[diplomat::cfg(cpp)] as a shortcut for #[diplomat::attr(not(cpp), disable)]
  • rename: Renaming APIs. Can be used for overloading!
  • namespace: For organizing code into namespaces/submodules
  • constructor and named_constructor: For marking methods as constructors
  • iterator, iterable: For hooking in to builtin language iteration stuff, enabling things like for i in obj
  • getter, setter: For marking a method as an accessor
  • indexer, add, sub, comparison, etc: For overloading most builtin operators

Demo generation

Often when talking about what a library can do, I want people to be able to play around with its API and give it different numbers. For example, in ICU4X, it’s great to be able to show a progression of “look, it can format a date!” → “here’s that date in a less compact format” → “here’s that date in French” → “here’s that date in French, in the Chinese calendar” → “here’s that date in French, in the Chinese calendar, with Thai numbering”, where at each step you can let people fiddle around with the parameters.

But ICU4X is a Rust library, and doing this kind of demo in Rust requires whipping out your laptop and having people tweak the code.

A while ago I realized that Diplomat already knows how to generate a JS-Wasm wrapper for your library, and it already has a good understanding of the API at a type level — which means that Diplomat can generate a web-based “demo” for most exposed APIs by chasing down constructors until it finds primitive/enum types it can ask the user for.

You can see this in action on ICU4X’s autogenerated demo page (try playing around with DateFormatter.formatIso for the date time formatting example above).

Demo generation has proven to be very valuable for us; the demo linked above works on phones and is an easy way to show off ICU4X’s capabilities in an elevator pitch timeframe.

The diplomat book documents how to set this up.

Design notes

I like compiler design, and Diplomat is basically a compiler. It takes (syn-parsed) Rust code and transforms it through a series of intermediate representations into bindings.

Diplomat, like rustc, has a two layers of abstract syntax tree style IRs: it has an “AST” (abstract syntax tree) that is basically a simplified version of the AST we get from syn, and an “HIR” (higher level intermediate representation) which has all of the paths resolved and a bunch of typechecking done. For example, this is the Type type, which contains a bunch of different variants for the different kinds of types supported in Diplomat. The various “Path” types can all eventually be resolved via their ids.

This originates from a design constraint of the proc macro: The proc macro cannot see the whole program, just the module it is tagged on. The AST is designed to not need whole-program information. As a consequence, it is less pleasant to work with.

When doing diplomat-tool codegen, however, the AST is transformed into the HIR, which is much nicer to work with. Diplomat tries to do most of the pre-work resolving everything in the HIR, so that backends can write relatively simple transformations from the HIR into bindings.

These days the underlying C ABI model for Diplomat doesn’t change often, so the AST and proc macro rarely change. New features are usually added via attributes or minor changes to the HIR, and backends can choose to adopt them when they want.

Most Diplomat backends have been written by a different person: writing them is pretty easy, which was our goal!

Lifetimes

Diplomat supports APIs like this:

impl MyType {
    fn get_foo(&self) -> &Foo {...}
}

In Rust, this is fine: lifetimes ensure that the returned Foo isn’t persisted for too long. In C++, it’s pretty normal to manually police lifetime constraints, so we can generate an API that returns a reference there4.

We can’t do that in JS or other GC’d languages, however, that would be unsound: these languages expect all values to be valid as long as you hold on to them, and they do not restrict how long you can hold on to them.

What we do here is that when the JS-side &Foo is returned, the JS object internally contains a “lifetime edge”, a reference to the MyType that originated the borrow. If you hold the Foo longer than the parent MyType, that’s fine, Foo will keep its parent alive since the GC will see a reference to it.

This is pretty straightforward for this API, but gets complicated pretty quickly when you start having multiple lifetimes, structs with lifetimes, or strings5.

The borrowing_param module in Diplomat goes into more detail on how we handle this in Diplomat.

Since Diplomat has been developed, two other tools have entered the same space. Mozilla developed uniffi, which gives you a choice of IDLs and bridge modules, and supports Kotlin, Swift, and Python (plus some third party bindings).

There is also BoltFFI, which supports Swift, Kotlin, Java, C#, and TypeScript. It also seems to do more work in producing nice packages. I haven’t really looked closely at it, but it seems neat.

Generally I think that this model for FFI tools — where you write a single “bridge” layer and use a CLI tool to generate bindings — is a good model for libraries and I’m excited to see more of this in that space. When I started working on Diplomat, this felt like a large hole in the ecosystem.

Shoutouts

The initial idea for Diplomat was mine, but a lot of it was done by others, especially some really skilled interns, and I want to make sure they get credit.

The first version of Diplomat was basically entirely written by our intern Shadaj, who also designed the first C, C++, and JS backends.

Quinn, another intern, designed and implemented the AST/HIR split as well as lifetime handling. The HIR-based versions of the C++ and JS/TS backends were written by my colleagues Shane and Robert respectively. Robert also implemented the Dart backend.

Tyler, another intern, implemented the demo and JS backends, and has continued to maintain them. In addition, he since joined Zeromatter where he has become the primary maintainer of the Python backend and continues to add features to Python and C++.

jcrist1 wrote the initial Kotlin backend, and Ellen polished it with more features (including callback support) for use in Android.

Walter from Zeromatter implemented the nanobind-based Python backend, as well as adding several features to the C++ backend including namespacing, arithmetic operators, and extended callback support.

Thanks to Tyler, Quinn, and Walter for reviewing drafts of this blog post.