RustQ can generate repetitive Rustler boundary code while keeping implementation logic in ordinary Rust. Use structural manifests and Rust source metadata rather than duplicating signatures across Rust and Elixir.
Atom registries
Discover atom calls structurally with RustQ.Syn.atom_references!/1, then emit
the registry with RustQ.Rustler.Atom.declaration/2. The scanner recognizes calls in
ordinary expressions and Rust macro token trees.
atoms =
"native/my_nif/src/*.rs"
|> Path.wildcard()
|> Enum.flat_map(fn path -> path |> File.read!() |> RustQ.Syn.atom_references!() end)
|> Enum.uniq()
|> Enum.sort()
rust "native/my_nif/src/generated_atoms.rs" do
RustQ.Rustler.Atom.declaration(atoms)
endExclude generated files from discovery. If another manifest introduces atom keys, derive those keys directly from that manifest so generation reaches a stable result in one pass.
Struct term encoders
term_encoder/2 generates a rustler::Encoder implementation backed by RustQ
AST. Simple fields are atoms; {key, field} renames the atom key.
RustQ.Rustler.Term.encoder(:EncodedLocation,
fields: [:start, {:end_, :end}, :line]
)Lifetime-bearing adapters use :target_lifetimes:
RustQ.Rustler.Term.encoder(:EncodedError,
target_lifetimes: [:_],
fields: [:message, code: [when_some: true, via: :as_str]]
)Field metadata supports structural transformations:
field: [:result, :code]— nested field pathvia: :as_str— zero-argument method before encodingwith: :encode_value— helper called asencode_value(env, &value)borrow: false— pass the helper value without adding&when_some: true— omit the map entry when the option isNoneoptional: [wrap: :EncodedValue]— encodeSomethrough an adapter andNoneas nilmap: [wrap: :EncodedValue]— map a collection into encoded termsmap: [convert: :EncodedValue]— map throughEncodedValue::fromfallback: [field: [:result, :code], via: :as_str]— generateunwrap_or
These are typed operations, not raw Rust expression strings.
One NIF manifest for Rust and Elixir
Keep NIF bodies as handwritten implementation functions with an _impl suffix:
fn parse_nif_impl<'a>(env: Env<'a>, source: &str) -> NifResult<Term<'a>> {
// domain implementation
}Declare boundary policy once:
nifs = [
parse_nif: [],
compile_nif: [attrs: [A.attr(:allow, [A.path([:clippy, :too_many_arguments])])]]
]
rust "native/my_nif/src/generated_nifs.rs" do
RustQ.Rustler.Nif.wrappers_from_source(
"native/my_nif/src/lib.rs",
nifs,
schedule: :dirty_cpu
)
end
generate "lib/my_app/generated_nif_stubs.ex" do
content(
RustQ.Rustler.Nif.stubs_from_source(
"native/my_nif/src/lib.rs",
nifs,
MyApp.GeneratedNifStubs
)
)
endThe Rust wrapper signatures come from RustQ.Syn; they are not repeated in the
manifest. Elixir stub arities use the same signatures and structurally exclude
Rustler's injected Env argument.
Retain human-facing specs in the native module and install generated stubs once:
defmodule MyApp.Native do
@spec parse_nif(String.t()) :: {:ok, map()} | {:error, term()}
use MyApp.GeneratedNifStubs
endRun both generation and compiler checks in CI:
lint: [
"rustq.gen --check",
"compile --warnings-as-errors"
]