Diplomat

repository·main·Indexed 21 days ago

https://github.com/rust-diplomat/diplomat

A Rust tool that automates the generation of high-level FFI (Foreign Function Interface) bindings from Rust APIs for languages including C, C++, Dart, JavaScript/TypeScript, .NET (C#), Kotlin, and Python. It provides macros and attributes like #[diplomat::bridge], #[diplomat::abi_rename], and #[diplomat::attr] to customize code generation, manage ABI stability, and implement language-specific features such as getters, setters, and operator overloading.

Tokens
43.4K
Snippets
139
Records
206
Agent score
74%

What's inside Diplomat

  1. Understand the Diplomat JS Proof of Concept

    main

    This project serves as a proof of concept for the tool/src/demo_gen tool. It demonstrates how to generate a web-based demonstration (using pure HTML and JavaScript) from a library's API.

    In this specific example, the implementation assumes the project already uses JavaScript. The example is structured by copying the API definitions from example/lib/js/api to treat them as a unified package for the demonstration.

  2. What is demo_gen?

    main

    Overview

    demo_gen is a Diplomat Backend that leverages Diplomat's JavaScript backend to automatically generate usage examples for your library's FFI (Foreign Function Interface) bindings in JavaScript.

    Key Characteristics

    • Purpose: It creates a "sample platter" of your library's functions to help users learn how to use them via examples.
    • Setup: Designed for minimal configuration and short setup times.
    • Customization: Highly configurable, allowing you to adjust the appearance and functionality of the generated output to suit different front-ends or design requirements.

    Use Case Example

    For a complex library like ICU4X, instead of requiring users to compile packages to test functionality, demo_gen can read functions (e.g., FixedDecimalFormatter.formatDecimal) and automatically generate an HTML page with the necessary inputs (like a number field and locale selector) and the underlying logic to demonstrate the function in action.

  3. Understand Wasm parameter and return type limitations

    main

    At the lowest level, WebAssembly (Wasm) only supports two parameter and return types: i32 and i64. The JS-Wasm interface maps Number to i32 and BigInt to i64.

    This means all other types are converted to these integers during FFI:

    • Pointers: Integer indices into the Wasm memory buffer.
    • Slices: A pair of integers.
    • Structs: A collection of integers.
    • Booleans/Chars: Converted to integers.

    Warning: Current Diplomat code may have issues with large u32 values (which may turn into negative numbers across FFI) and u64 values (which require BigInt conversion).

  4. How Diplomat handles multi-language FFI

    main

    Diplomat is designed to bridge Rust projects to multiple target languages by using a central, stable C API as an intermediary. Instead of manually writing wrappers for every language (e.g., C++, Java, JavaScript), developers define their public FFI API using tagged "bridge blocks" in Rust.

    Key architectural principles include:

    • Bridge Blocks: Users write one or more cxx-style tagged bridge blocks to define the API surface.
    • C-Layer Intermediary: All generated bindings go through an underlying C layer, ensuring that the Rust-to-C interface remains the single source of truth.
    • Plugin-Based Language Targets: New language bindings are implemented as plugins, making it easy to add support for different ecosystems.
    • Automated Conversions: The tool aims to autogenerate conversions between the raw C types and the idiomatic types of the target language.
  5. Use Feature Gates to selectively disable bindings

    main

    Diplomat allows you to selectively disable or enable bindings based on a list of features. These features are independent of Cargo features. You can use the #[diplomat::attr(not(feature=some_feature), disable)] attribute to ensure a module or item only appears for backends where some_feature is enabled.

    To enable these features globally across all backends, use the features_enabled configuration key.

    #[diplomat::attr(not(feature=some_feature), disable)]
    mod ffi {}
    
    #[diplomat::config(features_enabled=["this_feature", "some_feature"])]
    struct Config;
  6. Use Option<T> for FFI-safe function arguments and return types

    main

    In Diplomat, you can use the standard Rust Option<T> for function parameters and return values. Diplomat automatically converts these into the idiomatic equivalent for the target language across the FFI boundary.

    Supported Types for Option<T>:

    • Reference types (e.g., Box<OpaqueType> or &OpaqueType>)
    • Structs, enums, or primitives
    • Slices of the above types

    Language Mapping Examples:

    • C++: Option<Box<T>> becomes std::optional<std::unique_ptr<T>>. Primitives like Option<u8> become std::optional<uint8_t>.
    • JavaScript: Option<T> returns a potentially-null object or an integer-or-null.
    #[diplomat::bridge]
    mod ffi {
        #[diplomat::opaque]
        pub struct Thingy;
    
        impl Thingy {
            pub fn maybe_create() -> Option<Box<Thingy>> {
                Some(Box::new(Thingy))
            }
    
            pub fn increment_option(x: Option<u8>) -> Option<u8> {
                x.map(|inner| inner + 1)
            }
        }
    }
  7. Understand ownership and memory safety in .NET

    main

    Diplomat ensures memory safety in .NET through several mechanisms:

    • RustHandle<T>: Every opaque type is backed by a RustHandle<T>.
      • Owned handles: Carry the Rust destructor and run it on release.
      • Borrowed handles: Do not carry a destructor; releasing them is a no-op because Rust still owns the memory.
    • GC Rooting: Borrowed returns include a _edges array that roots the source object, preventing the .NET GC from collecting the source while a reference is still live.
    • Cleanup: While consumers can call Dispose(), a finalizer serves as a last-resort cleanup path for owned handles. Native calls use GC.KeepAlive(this) to prevent premature finalization during P/Invoke execution.
  8. Configure demo_gen output with #[diplomat::attr]

    main
    The demo_gen backend supports all standard #[diplomat::attr] attributes. A common use case is using the disable attribute to prevent specific functions from appearing in the generated demonstrations. Because demo_gen is built on the JS backend, any methods disabled for JS will also be disabled in the demo_gen output.
  9. Expose Rust types as opaque types over FFI

    main

    When you want to expose a Rust type over FFI without revealing its internal fields, use an opaque type. Opaque types can contain any data, but they can only be passed over FFI behind pointers (they cannot be passed on the stack). The consumer (e.g., C++, JS) can only interact with the type by calling explicitly defined methods on it.

    To create an opaque type, wrap the original Rust type in a new struct within a #[diplomat::bridge] block and annotate it with #[diplomat::opaque].

    struct Person {
        name: String,
        age: u8,
    }
    
    impl Person {
        pub fn new(name: String, age: u8) -> Self {
            Self { name, age }
        }
        pub fn get_age(&self) -> u8 {
            self.age
        }
    }
    
    #[diplomat::bridge]
    mod ffi {
        use super::Person as RustPerson;
    
        #[diplomat::opaque]
        pub struct Person(RustPerson);
    
        impl Person {
            pub fn create(name: String, age: u8) -> Box<Self> {
                Box::new(Person(RustPerson::new(name, age)))
            }
    
            pub fn get_age(&self) -> u8 {
                self.0.get_age()
            }
        }
    }
  10. C Backend: Implementing Traits

    main

    For each Rust trait, Diplomat generates a Trait struct and a VTable struct. The Trait struct contains a pointer to the data and a pointer to the VTable. The VTable includes the destructor, SIZE, and ALIGNMENT of the data, along with function pointers for the trait methods.

    typedef struct DiplomatTraitStruct_TraitName {
        void *data;
        TraitName_VTable vtable;
    } DiplomatTraitStruct_TraitName;
    
    typedef struct TraitName_VTable {
        void (*destructor)(const void*);
        size_t SIZE; size_t ALIGNMENT;
        /* ... */
    } TraitName_VTable;