Generate FFI bindings and data models from a single TypeScript source of truth.
Describe your API surface and data model once, in TypeScript. genffi extracts it and emits
matching code across languages: a JSON Schema, Java DTOs, a Go data model, Go interfaces, a Go
CGo shared-library wrapper (//exported C ABI), Go implementation stubs, a Java FFM caller,
and TypeScript (Ajv) validators.
The architecture it is built for: implement a core in Go, compile it to a C shared library through the generated wrapper, and call into it from Java, Node or C — with every side's types staying in sync with the one TypeScript definition.
Status. The Go, Java and JSON-Schema targets are mature and load-bearing. The JS / N-API target is experimental and incomplete — see known limitations.
| Target | Output | Maturity |
|---|---|---|
| Go CGo wrapper | package main with //exported C-ABI functions, C typedefs, handle tables |
✅ the centerpiece |
| Go api / impl | Go interface definitions, plus implementation skeletons to fill in |
✅ stable |
| Go model | Structs, interfaces, constructors with defaults, consts, enums, JSON marshalers | ✅ stable |
| Shared library | A Makefile linking the wrapper with -buildmode=c-shared, and a .gitignore |
✅ stable |
| Java binding | An FFM (Panama) caller: records, enums, handles, folds, futures, listeners | ✅ compiled and run, needs Java 22+ |
| Java model | Records and interfaces with Jackson @JsonSubTypes polymorphism, Identifiers |
✅ most complete data target |
| JSON Schema | schema.json — the backbone of the model pipeline |
✅ stable |
| TS validators | Ajv-based validate.ts |
✅ runs; ajv is yours to install |
| JS / N-API | currently a copy of the Go CGo wrapper | ❌ incomplete — do not use |
Everything you want generated lives under one of two namespaces: GenApi for behavior and
GenModel for data. Anything else in the file is ignored.
// defs.ts
namespace GenApi {
/** @singleton */
export interface Stores {
open: (cfg: StoreCfg) => Store;
close: (store: Store) => void;
}
/** @handle */
export interface Store {
path: () => string;
/** @throws */
sync: (target: string) => Promise<string>;
}
export type StoreCfg = { src: string; readOnly: boolean };
}Each interface declares its lifetime (@singleton, @handle, @fold, @listener); each
method's tags declare how it fails and whether it blocks (@throws, @optional, @scoped, or
a Promise<T> return for async). Point a config at the file and run it:
// genffi.config.ts
import type { Config } from "genffi";
export default {
source: "./defs.ts",
outDir: "./gen",
schema: {},
java: { pkg: "com.example.model", api: {} },
go: {
module: "github.com/example/blobstore",
mod: {},
model: {},
api: {},
cgo: {}, // the C shared-library wrapper
impl: {}, // stubs to implement
},
} satisfies Config;npx genffi # reads ./genffi.config.ts
npx genffi --dry-run --verbose # what would change, without writingA target's presence in the config is what enables it — {} means "on, with the defaults" —
and each one writes into gen/<key>. --strict is on by default in the CLI, so a problem
with your input stops the run rather than generating from it.
From there: fill in the Go stubs, run make shared next to the generated wrapper, and call the
library through the generated Java binding. The walkthrough with every step spelled out is
getting started.
generate() is exported, and is the right call when the config has to be computed or when
generation is one step of a larger script. Same object, two differences: source is required,
and strict defaults to false so that a new rule can never break an existing build.
import { generate } from "genffi";
const { diagnostics, files } = generate({
source: "./defs.ts",
strict: true /* … */,
});Both are conventions over the same pipeline — you choose by how you write your definitions.
- Typed service. Model the surface as
GenApiinterfaces and let every method become its own C export, with structs, enums, handles and errors marshaled across the boundary. Reach for it when you control both sides and want the boundary itself typed and granular. - Command bus. Collapse the FFI surface to one function taking and returning a
GenModeldiscriminated union carried as JSON. Reach for it when you want a minimal, stable ABI and would rather version the protocol than the C signatures.
Handles, fold chains, interface inheritance, async and callbacks compose on top of either. The full catalogue, with the example that shows each one, is use cases.
Five runnable examples, each owning exactly one story — indexed by
examples/README.md:
npm run example # typed service → examples/gen
npm run example:command # command bus → examples/gen-command
npm run example:fold # handles + folds → examples/gen-fold (committed)
npm run example:data # model only, no FFI → examples/gen-data
npm run example:callback # listeners (callbacks) → examples/gen-callbackAll of them run with strict: true, so they double as a calibration check on the rules.
| If you want to… | Read |
|---|---|
| use genffi | docs/guide/ — what it can do, and how to get it to do that |
| know the exact rules for your input | GenApi · GenModel |
| bind the library from C, Node or Java | memory ownership · Java binding |
| change genffi itself | docs/design/ — architecture, decisions, invariants |
docs/README.md is the full index.
Requires Node >= 22.18 (engines.node; CI pins .nvmrc). A Go toolchain, make, a C compiler
and a JDK 22+ are optional: the suites that need them skip when they are absent, so npm test
works on a bare machine.
npm run build # tsdown: ESM bundle + rolled-up d.ts
npm test # vitest
node --run test:compilers # only the suites that compile and run generated code
npm run lint # oxlint --fix, then oxfmt (writes)
npm run lint:check # oxlint --type-aware + oxfmt --check (CI, read-only)
npm run type:check # tsc --noEmit
npm run snap # update snapshotslefthook runs the fast gates before a commit and the slow ones before a
push. Commit messages must be conventional commits,
because semantic-release derives the version bump and the changelog from them — npm run cz
walks you through a conforming message.
CONTRIBUTING.md has the rest: the repo layout, the hooks in detail, and
what else to update when you change what genffi accepts or emits.
The JS/N-API target is the incomplete one. The accepted signature grammar is also narrow by
design — (a: T, b: U) => R over basic and declared types, with anything else reported rather
than emitted. See known limitations for the list with
symptoms and workarounds.
MIT © Andreas Zahnen