.NET Backend
The .NET backend wraps Diplomat’s C ABI in a C# library, generating two layers per type:
a Raw layer of [LibraryImport] P/Invoke declarations and unsafe pointer types, and an
idiomatic layer of safe, GC-friendly classes built on top of it. Consumers only interact
with the idiomatic layer.
To run the .NET backend you need to provide some configuration:
diplomat-tool -e {PATH_TO_LIB.RS} -c {CONFIG_FILE} --config {CONFIG_OVERRIDE_1} dotnet {OUTPUT_PATH}
The configuration consists of these options:
namespace- the root .NET namespace for the generated bindings (e.g.Icu4x). Defaults to the crate’slib_name, upper-camel-cased.dylib_name(ornative_lib) - the native library name passed toLibraryImport. Defaults to the crate’slib_name.exception_trim_suffix(orexceptions.trim_suffix) - suffix trimmed when deriving exception class names from error types, e.g. trimmingErrorsoFooErrorbecomesFooException.exception_message_method(orexceptions.error_message_method) - the method on an error type used to populate the generated exception’s message, e.g.ToDisplay.scaffold- an optional binary value. If set totrue,diplomat-toolwill emit a.csprojscaffold next to the generated sources.
Properties
#[diplomat::attr(auto, getter)] and #[diplomat::attr(auto, setter = "name")] render as
C# properties. The accessor is the property — its body is generated inline, so no
separate method is emitted:
#![allow(unused)]
fn main() {
#[diplomat::attr(auto, getter)]
pub fn width(&self) -> u32 { self.0.width }
}
public uint Width
{
get
{
unsafe
{
/* ... */
}
}
}
A getter and a setter that resolve to the same property name are merged into one property with both accessors. C# cannot hold two members of the same name, so this merging is required — unlike Dart or JS, where a getter and setter are separate members.
Two cases do not merge:
- A setter with no matching getter becomes a write-only property.
- A getter and setter whose types disagree are rejected with a diagnostic naming both
types, both Rust methods, and the marshal each side chose. A C# property has one type,
and silently dropping the setter would hide the mistake — make the two agree. A
byte-slice pair is the case you are most likely to hit: a
&'a [u8]getter hands out aDiplomatBorrowedSpan<byte>view over Rust-owned memory, while a&[u8]setter takes a managedbyte[]Rust only reads during the call. No single C# type serves both.
A getter must take &self. A &mut self getter is rejected, because reading it could
change the value: a property is read more than once — by a debugger watch, a serializer,
or just twice in a row — and a one-shot self.field.take() behind a property would drain
to null on the second read. Setters keep &mut self; assigning is the point.
Names must not collide either. A property that would share its name with a method, a
struct field, the type that contains it, or one of the members Diplomat always generates
(AsFFI, FromFFI, and opt-in Dispose on opaques) is rejected, because C# would not
compile the result.
A getter that returns an owned Box<[u8]> (RustVec) hands back a value you own, so
dispose it — using var x = thing.Data;. A getter returning an owned opaque can be used
the same way only when that opaque is opted into
#[diplomat::attr(dotnet, manually_disposable)].
In accessor position a string-shaped parameter is always string, even for
&DiplomatStr (which is byte[] everywhere else, zero-copy and unvalidated). A property
cannot have one type for reading and another for writing, and every other backend already
maps &DiplomatStr to its string type — std::string_view in C++/Nanobind, String in
Dart, string in JS. Parameters outside accessors keep the zero-copy byte[] shape.
#[diplomat::rename] is applied after case conversion, so its value is used verbatim
(#[rename = "UTCTime"] stays UTCTime, not UtcTime) — matching how this backend names
methods and types. Static accessors are not supported.
Ownership and memory safety
Every opaque type is backed by a RustHandle<T> rather than a bare pointer. A handle
remembers who owns the underlying memory: an owned handle carries the Rust destructor
and runs it on release; a borrowed handle carries none, so releasing it is a no-op
because Rust still owns (and will free) that memory. This means methods returning &T or
Option<&T> are safe to wrap without risking a double-free.
Borrowed returns also carry a _edges array on the object rooting whatever it borrowed
from, so the GC can’t collect the source object out from under a still-live borrowed
reference. object[] _edges is Array.Empty<object>() (no allocation) when a type has
no lifetime-carrying returns.
By default, generated opaques are finalizer-only: no public Dispose(), cleanup runs
through a private idempotent path invoked by the finalizer. Add
#[diplomat::attr(dotnet, manually_disposable)] on an opaque type declaration to generate
: IDisposable plus a public Dispose() that runs the same cleanup and
GC.SuppressFinalize(this). In both modes, native calls are followed by
GC.KeepAlive(this) to prevent finalization while P/Invoke is still using the pointer.
String encoding
The backend supports both UTF-8 and UTF-16 strings, zero-copy wherever the C# and Rust representations line up:
&DiplomatStr16params and returns: a C#stringis already a flat UTF-16 buffer, so these are always zero-copy — pinned directly withfixed(or, if the return value borrows it, viaReadOnlyMemory<char>+ the same pinning holder slices use).&DiplomatStrparams and returns (unvalidated UTF-8 — Rust places no validity requirement on the caller): treated exactly like&[u8], so these are also zero-copy —byte[]/ReadOnlyMemory<byte>pinned directly, no transcoding.&strparams (validated UTF-8 — Rust requires the caller to guarantee well-formed UTF-8, undefined behavior otherwise): a transcode from the UTF-16stringis unavoidable here. That copy is always routed through the explicitly-namedDiplomat.Utf8.Clone(...)helper rather than inlined, so it stays visible in the generated source instead of hiding inside generic marshalling.
A borrowed string or slice return (&'a str / &'a DiplomatStr / &'a DiplomatStr16 /
&'a [u8] / &'a [u32]) surfaces as DiplomatBorrowedSpan<T> — a zero-copy view over
memory Rust still owns, rooted with the same keep-alive-edge mechanism as a borrowed
opaque return. It intentionally does not expose a Span-returning property (nothing
would keep the view rooted once the span escaped it); call WithSpan(...) for scoped,
zero-copy, read-only access instead — the same pattern RustVec uses for owned returns
(see below). Producing an independent T[] is a separate, explicit step: call Clone().
An owned Box<[u8]> return surfaces as RustVec — it owns the native allocation, is
IDisposable, and offers the same WithSpan(...) / Clone() shape as
DiplomatBorrowedSpan<T> (it deliberately avoids MemoryManager<T> for the same reason:
GetSpan()’s result wouldn’t keep the owner alive). Other owned string/slice returns
(Box<str>, Box<[T]> for T other than u8) and &[&str] (&[DiplomatStrSlice])
parameters aren’t supported yet.
Examples
The best way to learn to use the .NET backend is to first understand Diplomat generally
by reading this book. Then look at the example and feature_tests
directories in the Diplomat project.
- Feature tests: rust source, .NET usage
- Example: rust source, .NET generated bindings
Supports
-
Memory Sharing
-
Non Exhaustive Structs
-
Method Overloading
-
UTF8 Strings
-
UTF16 Strings
-
Named Constructors
-
Fallible Constructors
-
Static Getters/Setters
-
Custom Errors
-
Traits are Send
-
Traits are Sync
-
Generate Mocking Interface
-
Struct References. See References for more.
-
Mutable Struct References. See References for more.
-
Free Functions
-
Owned Slices
-
Mutable Slices (
&mut [T]) -
Tuples. See tuples for more