FFI & Dynamic
Nomi modules can bind ordinary Go packages. The usual shape is:
- write Go adapter code in a normal
.gofile, - declare a compile-time Go package handle with
gopkg, - bind Nomi declarations to Go symbols with
go handle.Symbol, - expose domain-shaped Nomi APIs instead of raw Go package details,
- use
nomi run,nomi check,nomi test, andnomi buildas the workflow.
For app-specific boundaries, the adapter can live next to the Nomi module in
the same codebase: a small .go file that speaks to an existing app service,
vendor SDK, or platform API, with a Nomi file exposing the shape the rest of the
app uses. Common boundaries belong in the ecosystem: SQL, SQLite, HTTP, and
other standard integrations can be ordinary Nomi modules backed by Go adapter
code.
gopkg is not a Nomi import. It is a Go package handle used only by FFI
bindings:
gopkg "urltools" pub struct ParsedURL { scheme: String host: String path: String raw_query: String query: Map<String, List<String>> } pub fn escape_query(value: String): String go urltools.EscapeQuery pub fn unescape_query(value: String): Result<String, String> go urltools.UnescapeQuery pub fn parse_query(raw: String): Result<Map<String, List<String>>, String> go urltools.ParseQuery pub fn parse(raw: String): Result<ParsedURL, String> go urltools.Parse
The corresponding Go file is regular Go:
package urltools import "net/url" type ParsedURL struct { Scheme string Host string Path string RawQuery string Query map[string][]string } func EscapeQuery(value string) string { return url.QueryEscape(value) } func UnescapeQuery(value string) (string, error) { return url.QueryUnescape(value) } func ParseQuery(raw string) (map[string][]string, error) { return url.ParseQuery(raw) } func Parse(raw string) (ParsedURL, error) { parsed, err := url.Parse(raw) if err != nil { return ParsedURL{}, err } return ParsedURL{ Scheme: parsed.Scheme, Host: parsed.Host, Path: parsed.Path, RawQuery: parsed.RawQuery, Query: map[string][]string(parsed.Query()), }, nil }
The Nomi signature is the contract. The Go function should project to that
contract through a small adapter rather than exposing raw Go package details.
At the binding boundary, Nomi generates the wrapper code that calls the Go
symbol and converts the common shapes: scalars like String, Bool, Int,
Float, Byte, and Bytes; containers like List and Map; optional and
fallible shapes like Maybe and Result; plain structs by matching exported Go
fields to Nomi fields; and opaque handles declared with opaque type.
Shapes outside that set should be normalized in the Go adapter. If Nomi only
needs to hold a Go-owned value and pass it back, declare an opaque handle. If
Nomi needs to inspect a loose value whose shape is not known at the signature,
cross it as Dynamic and decode it deliberately at the edge.
| Go boundary shape | Nomi surface | Notes |
|---|---|---|
string, bool, numbers, []byte | String, Bool, Int, Float, Bytes | Integer widths are checked at the boundary. |
[]T, map[K]V | List<T>, Map<K, V> | Keys and values must also be supported shapes. |
*T | Maybe<T> | Explicit opaque handles are passed as the handle type instead. |
(T, error) / error | Result<T, String> / Result<Unit, String> | The Err side is a string message. |
| exported Go structs | Nomi struct | Exported Go fields are matched to Nomi fields. |
| Go-owned resources | opaque type | Use when Nomi should hold and return the value, not inspect it. |
| loose runtime values | Dynamic | Use when Nomi will decode or pattern over an unknown shape. |
| channels, non-empty interfaces, named function types | Go adapter or opaque handle | Normalize to a supported shape, or keep the value opaque. |
Dynamic Values
Section titled “Dynamic Values”Some boundary values do not have a known Nomi shape: plugin messages, decoded
payloads, loosely typed configuration, or data from another runtime.
Dynamic is the escape hatch for that case.
Use it at the edge, then decode deliberately when the expected shape is known.
When a format has a typed representation, prefer that. For JSON,
std/json decodes into a Json
enum so ordinary case matching can handle the value.
Dynamic is useful for crossing messy boundaries. It should not become the
default way to model data inside Nomi.
That’s the tour. You’ve seen Nomi’s core syntax (Bindings & Expressions, Functions, Pipes), its data story (Scalars, Collections, Structs/Enums, Pattern Matching), its abstraction layer (Interfaces, Modules), its distinctive features (App Fields, Defer, Concurrency, Dates & Times, Typed Literals), and how it talks to host programs. The Standard Library reference is generated from the stdlib’s own docs.