This target supports an escape hatch (__rust__) for emitting raw Rust, but the proposed v1.0
policy is:
- Application code should stay “pure Haxe” (no raw
__rust__calls). - Rust interop belongs in framework code (
std/,runtime/) behind typed APIs.
This doc describes the recommended, documented pattern for binding to Rust.
For APIs whose Rust shape needs explicit lifetimes, HRTB, const generics, or macro-heavy setup, use the Extern and lifetime-island cookbook as the practical template.
@:rustExtraSrc remains in the generated application crate. It cannot contain unsafe code when the
application forbids unsafe. The planned
typed support-crate facility reserves @:rustSupportCrate for a
separate, content-bound crate. The compiler rejects that metadata until the complete facility lands.
Compiler-generated native wrappers are not a shipped user feature yet. The M94
native wrapper facility spike reserves @:rustNativeWrapper for
future simple value-wrapper generation, and the compiler rejects that metadata today. Use
@:rustExtraSrc plus the native facade manifest for current handwritten helper islands.
Put a .rs file in a directory and include it via -D rust_extra_src=...:
- Haxe:
-D rust_extra_src=native(directory relative to thehaxeworking directory) - Rust file:
native/my_module.rs
The compiler copies it into the generated crate and emits mod my_module; automatically.
@:native("crate::my_module")
extern class MyModule {
@:native("some_fn")
public static function someFn(x:Int): Int;
}Notes:
@:native("crate::my_module")maps the extern class to a Rust module path.@:native("some_fn")maps the Haxe field to the Rust function name.- Keep the extern surface tiny and then wrap it with more idiomatic Haxe APIs if needed.
- For small APIs, add a snapshot under
test/snapshot/*that compiles + builds the generated crate. - For richer behavior, add
native/*.rstests and runcargo testin CI (seeexamples/tui_todo).
Prefer declarative dependencies:
- Put
@:rustCargo({ name: "crate_name", version: "x.y" })metadata on the extern type that needs it.
This keeps Cargo wiring centralized and avoids ad-hoc Cargo.toml edits in app repos.
Two forms are supported:
- Raw TOML line:
@:rustCargo("ratatui = \"0.26\"")
- Structured object (recommended; deterministic + mergeable):
@:rustCargo({ name: "serde", version: "1", features: ["derive"] })
Supported fields:
name(required): crate nameversion: Cargo version requirement (e.g."1","0.26","^1.2")features: array of feature stringsdefaultFeatures: boolean (falseto emitdefault-features = false)optional: booleanpath: local path dependencygit: git URL dependencybranch/tag/rev: optional git selectorspackage: override the package name (Cargo’spackage = "..."field)
If multiple modules declare @:rustCargo for the same crate:
featuresare unioned + de-duped (stable order)- most other fields must match (conflicts produce a compile-time error)
Sometimes you want to implement a Rust trait for a Haxe-emitted type without dropping down to raw
__rust__ in app code (for example Display, or a small marker trait).
Use @:rustImpl(...) metadata on the Haxe class/enum:
@:rustImpl("std::marker::Unpin")
@:rustImpl("std::fmt::Display",
"fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n" +
" write!(f, \"Foo({})\", self.x)\n" +
"}")
class Foo {
public var x:Int;
public function new(x:Int) this.x = x;
}Supported forms:
@:rustImpl("path::Trait")emits an empty impl block:impl path::Trait for Type { }@:rustImpl("path::Trait", "fn ...")emits the provided string as the inner body of the impl block@:rustImpl({ trait: "path::Trait", body: "fn ...", forType: "SomeType" })(advanced)forTypeoverrides the Rust type name used on the right-hand side offor ...
The strings that identify the trait and optional target type are parsed at the metadata boundary.
After that, the compiler keeps the impl header as typed Rust structure, so analysis can see its paths
and generic arguments. When a body string is supplied, only that inner body remains raw metadata;
the surrounding impl Trait for Type { ... } syntax is still compiler-owned and structurally printed.
The positional form is intentionally strict: use exactly one string for a marker impl or exactly two
strings for an impl with a body. Extra arguments and non-string bodies are reported at compile time
instead of being ignored or silently reinterpreted as marker impls.
Limitations:
- Rust orphan rules still apply. In practice, this is primarily useful for implementing external traits for local types (types emitted by this compiler). If both the trait and the target type are external, Rust will reject the impl.
- Trait paths use the compiler's closed structural metadata grammar. It accepts stable path forms used
by current fixtures, including trailing commas in generic/function-trait argument lists and signed
decimal const arguments such as
Marker<-1,>. Braced const expressions such asMarker<{ N + 1 }>are not modeled yet; use a named const path or a typed extern/native island instead of hiding that expression in compiler-owned syntax. - Impl body strings are a narrow metadata escape hatch, not the long-term app authoring model for common Rust trait patterns. See Metal trait, impl, and bound model for the current contract and planned typed surfaces. The compiler-wide raw-authority inventory proves that the body is the only metadata-owned raw producer and that no compiler-owned raw lowering remains.
If a binding is awkward to express as an extern (generics/closures, tricky lifetimes, etc.), you can
use __rust__ inside framework code as a last resort.
Two ways exist:
untyped __rust__("...{0}...", arg0)— works in normal (non-macro) modulesreflaxe.rust.macros.RustInjection.__rust__("...{0}...", arg0, arg1, ...)— macro shim that provides a typed callable surface (and helps in files that also define macros)
Important:
- Examples/snapshots are guarded by
-D reflaxe_rust_strict_examplesand will fail if__rust__leaks into user code via inlining. - Both injection branches are listed explicitly in the generated raw-authority inventory; adding another producer requires a reviewed inventory update.
If a narrow low-level abstraction module genuinely needs raw __rust__, you can mark the owning
type/module with @:rustAllowRaw.
Use this sparingly:
- good fit: a small user-owned bridge module that cannot be expressed cleanly as an extern yet
- bad fit: application business logic, examples, or general "Rust everywhere" authoring
Important limits:
@:rustAllowRawonly relaxes strict boundary enforcement (reflaxe_rust_strictandreflaxe_rust_strict_examples) for the tagged module.- It does not bypass
metalor@:rustMetalraw-fallback restrictions. - If the same module is compiled as
metalor tagged@:rustMetal, raw fallback still errors.
Practical rule:
- Prefer typed externs and metadata first.
- If that still cannot express the boundary, use
@:rustAllowRawin one small wrapper module. - Keep the rest of the codebase on typed Haxe APIs.
Some Haxe std APIs are intentionally dynamic across all targets. For those, this backend keeps
Dynamic only at the API boundary, then converts immediately to typed data.
Current important example:
haxe.Json.parse(text):Dynamic- Required by upstream std API contract.
- Rust runtime parses with typed
serde_json::Value. - The
Dynamicvalue is only the outward compatibility layer. haxe.Json.parseValue(text):haxe.json.Valueis the typed path and should be preferred in compiler/runtime/example code.
Rule of thumb:
- If upstream API requires
Dynamic, keep it only at that seam and document the conversion path. - Otherwise, prefer typed externs/typedefs/classes end-to-end.