Skip to content

Using the native library (DLL)

The engine also ships as a native shared library (.dll / .so / .dylib) with a small C ABI, so you can drive it from C#, Python, C/C++, Go, or any language with a foreign-function interface ,without a Node or JavaScript runtime. The library reproduces the TypeScript engine’s behaviour bit-for-bit, including seeded randomness.

The wire format is JSON in, JSON out: you pass the same Grammar / Rule / Graph shapes documented in the API reference, and get JSON back. Inputs are validated against the engine’s schema at the boundary.

Grab the archive for your platform from the project’s GitHub Releases (assets named graph-grammar-native-<version>-<platform>):

PlatformAssetLibrary file
Windows x64…-windows-x64.zipgraph_grammar.dll
Linux x64…-linux-x64.tar.gzlibgraph_grammar.so
macOS (Apple silicon)…-macos-arm64.tar.gzlibgraph_grammar.dylib

Each archive contains the library, the generated C header (graph_grammar.h), the JSON Schema (graph-grammar.schema.json), and the README. To build from source instead, see Building from source.

All strings are NUL-terminated UTF-8.

FunctionPurpose
const char* gg_version(void)Library version. The pointer is owned by the library ,do not free it.
int gg_apply_rule(const char* rule_json, const char* graph_json, char** out_json)Apply one rule to one graph, deterministically.
int gg_apply_rule_seeded(const char* rule_json, const char* graph_json, uint32_t seed, char** out_json)Same, but with a seeded RNG (stochastic match selection + random property expressions).
int gg_string_free(char* s)Release a string returned via an out_json parameter.

For multi-step runs, create a stateful engine handle (seeded from the grammar’s config.seed):

FunctionPurpose
Engine* gg_engine_new(const char* grammar_json, const char* start_graph_json, char** err_out)Build an engine. start_graph_json may be NULL to use the grammar’s own start. Returns NULL and writes *err_out on failure.
int gg_engine_step(Engine*, char** out_json)Advance one step.
int gg_engine_run(Engine*, int32_t max_steps, char** out_json)Run to a fixpoint/bound. Pass -1 for max_steps to use config.maxSteps.
int gg_engine_graph(Engine*, char** out_json)Snapshot the current graph.
void gg_engine_free(Engine*)Release the engine handle.

Functions return 0 on success. On error they return a non-zero code and write { "error": { "code", "detail" } } to the out-parameter:

CodeMeaning
0Success
1Invalid input JSON, or input that fails schema validation
3Unsupported feature (e.g. a random property expression without a seed)
-1Null argument or an internal error

gg_apply_rule[_seeded] writes an envelope:

{
"applied": true,
"graph": { "nodes": [], "edges": [] },
"createdNodes": [],
"createdEdges": [],
"deletedNodes": [],
"deletedEdges": []
}

gg_engine_step writes { "applied", "ruleId"?, "createdNodes", … }, gg_engine_run writes { "applied": <count> }, and gg_engine_graph writes { "nodes", "edges" }.

Using the standard-library ctypes ,no third-party packages:

import ctypes, json
lib = ctypes.CDLL("./libgraph_grammar.so") # .dll / .dylib on Win/macOS
lib.gg_apply_rule_seeded.restype = ctypes.c_int
lib.gg_apply_rule_seeded.argtypes = [
ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_void_p),
]
lib.gg_string_free.argtypes = [ctypes.c_void_p]
def apply_rule(rule: dict, graph: dict, seed: int) -> tuple[int, dict]:
out = ctypes.c_void_p()
code = lib.gg_apply_rule_seeded(
json.dumps(rule).encode(), json.dumps(graph).encode(), seed, ctypes.byref(out),
)
try:
return code, json.loads(ctypes.cast(out, ctypes.c_char_p).value or b"{}")
finally:
lib.gg_string_free(out)
rule = {
"id": "r", "name": "A→B", "enabled": True, "weight": 1, "probability": 1,
"priority": 0, "maxApplications": 0, "morphism": [], "embedding": [],
"lhs": {"nodes": [{"id": "L0", "label": "A", "props": {}}], "edges": []},
"rhs": {"nodes": [{"id": "R0", "label": "B", "props": {}, "mapFrom": "L0"}], "edges": []},
}
graph = {"nodes": [{"id": "n1", "label": "A", "props": {}}], "edges": []}
code, result = apply_rule(rule, graph, seed=42)
print(result["graph"]) # n1 is now labelled "B"

Using DllImport. Marshal JSON as NUL-terminated UTF-8 bytes and read results back with Marshal.PtrToStringUTF8:

using System.Runtime.InteropServices;
using System.Text;
static class Gg
{
const string Lib = "graph_grammar"; // resolves to .dll/.so/.dylib
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
static extern int gg_apply_rule_seeded(byte[] rule, byte[] graph, uint seed, out IntPtr outJson);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
static extern void gg_string_free(IntPtr s);
static byte[] Utf8z(string s)
{
var b = Encoding.UTF8.GetBytes(s);
Array.Resize(ref b, b.Length + 1); // trailing NUL
return b;
}
public static (int Code, string Json) ApplyRule(string ruleJson, string graphJson, uint seed)
{
var code = gg_apply_rule_seeded(Utf8z(ruleJson), Utf8z(graphJson), seed, out var ptr);
try { return (code, Marshal.PtrToStringUTF8(ptr) ?? "{}"); }
finally { gg_string_free(ptr); }
}
}

Using cgo. The import "C" block declares the functions (or #include "graph_grammar.h" from the release archive); LDFLAGS links the shared library. cgo requires a C compiler and CGO_ENABLED=1.

package main
/*
#cgo LDFLAGS: -L. -lgraph_grammar
#include <stdlib.h>
int gg_apply_rule_seeded(const char* rule_json, const char* graph_json, unsigned int seed, char** out_json);
void gg_string_free(char* s);
*/
import "C"
import (
"fmt"
"unsafe"
)
// applyRule returns the status code and the result JSON. The library owns the
// returned string until gg_string_free, so we copy it out with C.GoString first.
func applyRule(ruleJSON, graphJSON string, seed uint32) (int, string) {
cRule := C.CString(ruleJSON)
cGraph := C.CString(graphJSON)
defer C.free(unsafe.Pointer(cRule))
defer C.free(unsafe.Pointer(cGraph))
var out *C.char
code := C.gg_apply_rule_seeded(cRule, cGraph, C.uint(seed), &out)
defer C.gg_string_free(out)
return int(code), C.GoString(out)
}
func main() {
const ruleJSON = `{
"id":"r","name":"A->B","enabled":true,"weight":1,"probability":1,
"priority":0,"maxApplications":0,"morphism":[],"embedding":[],
"lhs":{"nodes":[{"id":"L0","label":"A","props":{}}],"edges":[]},
"rhs":{"nodes":[{"id":"R0","label":"B","props":{},"mapFrom":"L0"}],"edges":[]}
}`
const graphJSON = `{"nodes":[{"id":"n1","label":"A","props":{}}],"edges":[]}`
code, result := applyRule(ruleJSON, graphJSON, 42)
fmt.Println(code, result) // 0 {"applied":true,"graph":{...n1 now labelled "B"...},...}
}

Use the stateful engine for multi-step runs. In Python:

lib.gg_engine_new.restype = ctypes.c_void_p
lib.gg_engine_new.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)]
lib.gg_engine_run.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_void_p)]
lib.gg_engine_graph.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]
lib.gg_engine_free.argtypes = [ctypes.c_void_p]
def run_grammar(grammar: dict):
err = ctypes.c_void_p()
eng = lib.gg_engine_new(json.dumps(grammar).encode(), None, ctypes.byref(err))
if not eng:
raise RuntimeError(ctypes.cast(err, ctypes.c_char_p).value)
try:
out = ctypes.c_void_p()
lib.gg_engine_run(eng, -1, ctypes.byref(out)) # -1 → config.maxSteps
applied = json.loads(ctypes.cast(out, ctypes.c_char_p).value)["applied"]
lib.gg_string_free(out)
g = ctypes.c_void_p()
lib.gg_engine_graph(eng, ctypes.byref(g))
graph = json.loads(ctypes.cast(g, ctypes.c_char_p).value)
lib.gg_string_free(g)
return applied, graph
finally:
lib.gg_engine_free(eng)

The grammar’s config.strategy (random, priority, sequential, maximal), config.seed, maxSteps, and maxNodes all behave exactly as in the strategies guide.

The repository ships thin wrappers you can copy or learn from, each exercising the full ABI against a conformance suite:

You need the Rust toolchain. On Windows, the default stable-msvc toolchain links against the Visual Studio C++ build tools.

Terminal window
cargo build --release --manifest-path packages/graph-grammar-native/Cargo.toml
# → target/release/<lib> and the generated include/graph_grammar.h

Built by Kiberon Labs