Skip to content

Commit 8bff5df

Browse files
add cddl generator
1 parent 7a1a9c9 commit 8bff5df

5 files changed

Lines changed: 338 additions & 1 deletion

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
; CDDL schema for `nimtimer` — auto-generated from ../nim_timer.nim
2+
; Wire format: CBOR (RFC 8949). Errors return raw UTF-8 (not CBOR) and
3+
; are intentionally absent from this schema.
4+
5+
; ─── User-declared FFI types ──────────────────────────────────────
6+
TimerConfig = { name: tstr }
7+
EchoRequest = { message: tstr, delayMs: int }
8+
EchoResponse = { echoed: tstr, timerName: tstr }
9+
ComplexRequest = { messages: [* EchoRequest], tags: [* tstr], note: tstr / nil, retries: int / nil }
10+
ComplexResponse = { summary: tstr, itemCount: int, hasNote: bool }
11+
12+
; ─── Request envelopes (one CBOR blob per request) ────────────────
13+
NimtimerCreateCtorReq = { config: TimerConfig }
14+
NimtimerEchoReq = { req: EchoRequest }
15+
NimtimerVersionReq = { }
16+
NimtimerComplexReq = { req: ComplexRequest }
17+
18+
; ─── Procs ─────────────────────────────────────────────────────────
19+
; nimtimer_create (ctor)
20+
nimtimer_create-request = NimtimerCreateCtorReq
21+
nimtimer_create-response = tstr
22+
23+
; nimtimer_echo (async)
24+
nimtimer_echo-request = NimtimerEchoReq
25+
nimtimer_echo-response = EchoResponse
26+
27+
; nimtimer_version (sync)
28+
nimtimer_version-request = NimtimerVersionReq
29+
nimtimer_version-response = tstr
30+
31+
; nimtimer_complex (sync)
32+
nimtimer_complex-request = NimtimerComplexReq
33+
nimtimer_complex-response = ComplexResponse
34+
35+
; nimtimer_destroy (dtor)
36+
nimtimer_destroy-response = nil

ffi.nimble

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ task test, "Run all tests under --mm:orc and --mm:refc":
2626
exec "nim c -r " & flags & " tests/test_cbor_serial.nim"
2727
exec "nim c -r " & flags & " tests/test_ctx_validation.nim"
2828
exec "nim c -r " & flags & " tests/test_nim_native_api.nim"
29+
exec "nim c -r " & flags & " tests/test_cddl_codegen.nim"
2930

3031
task test_alloc, "Run alloc unit tests under --mm:orc and --mm:refc":
3132
exec "nim c -r " & nimFlagsOrc & " tests/test_alloc.nim"
@@ -57,6 +58,14 @@ task genbindings_rust, "Generate Rust bindings for the nim_timer example":
5758
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
5859
" -o:/dev/null examples/nim_timer/nim_timer.nim"
5960

61+
task genbindings_cddl, "Generate CDDL schema for the nim_timer example":
62+
exec "nim c " & nimFlagsOrc &
63+
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
64+
" -d:ffiGenBindings -d:targetLang=cddl" &
65+
" -d:ffiOutputDir=examples/nim_timer/cddl_bindings" &
66+
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
67+
" -o:/dev/null examples/nim_timer/nim_timer.nim"
68+
6069
task genbindings_cpp, "Generate C++ bindings for the nim_timer example":
6170
exec "nim c " & nimFlagsOrc &
6271
" --app:lib --noMain --nimMainPrefix:libnimtimer" &

ffi/codegen/cddl.nim

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
## CDDL (RFC 8610) schema generator for the nim-ffi framework.
2+
## Mirrors the CBOR wire format produced by ffi/cbor_serial.nim: every
3+
## user-declared {.ffi.} type becomes a CDDL rule, every {.ffi.} / {.ffiCtor.}
4+
## proc gets a request envelope rule plus a response shape rule.
5+
6+
import std/[os, strutils]
7+
import ./meta
8+
9+
proc innerOf(typeName, prefix: string): string =
10+
if typeName.startsWith(prefix) and typeName.endsWith("]"):
11+
return typeName[prefix.len .. ^2]
12+
return ""
13+
14+
proc capitalizeFirstLetter(s: string): string =
15+
if s.len == 0: return s
16+
result = s
17+
result[0] = s[0].toUpperAscii()
18+
19+
proc toCamelCase(s: string): string =
20+
## "testlib_create" → "TestlibCreate"
21+
var parts = s.split('_')
22+
result = ""
23+
for p in parts:
24+
result.add capitalizeFirstLetter(p)
25+
26+
proc nimTypeToCddl*(typeName: string): string =
27+
## Maps a Nim type name (as recorded in the compile-time registries) to its
28+
## CDDL equivalent. Unknown PascalCase names are passed through as references
29+
## to other CDDL rules in the same document.
30+
let t = typeName.strip()
31+
if t.startsWith("ptr "):
32+
return "uint" # in-process pointer; documented inline at the call site
33+
let seqI = innerOf(t, "seq[")
34+
if seqI.len > 0:
35+
return "[* " & nimTypeToCddl(seqI) & "]"
36+
let arrI = innerOf(t, "array[")
37+
if arrI.len > 0:
38+
# CDDL has no fixed-length array literal as ergonomic as Nim's array; emit
39+
# an unbounded array whose element type is the user-declared element type.
40+
let commaIdx = arrI.find(',')
41+
let elemT = if commaIdx >= 0: arrI[commaIdx + 1 .. ^1].strip() else: arrI
42+
return "[* " & nimTypeToCddl(elemT) & "]"
43+
let optI = innerOf(t, "Option[")
44+
if optI.len > 0:
45+
return nimTypeToCddl(optI) & " / nil"
46+
let mayI = innerOf(t, "Maybe[")
47+
if mayI.len > 0:
48+
return nimTypeToCddl(mayI) & " / nil"
49+
case t
50+
of "bool": "bool"
51+
of "int", "int64", "int32", "int16", "int8": "int"
52+
of "uint", "uint64", "uint32", "uint16", "uint8": "uint"
53+
of "string", "cstring": "tstr"
54+
of "float", "float64": "float64"
55+
of "float32": "float32"
56+
of "pointer": "uint"
57+
else: t # reference to another rule in this CDDL document
58+
59+
proc reqStructName(p: FFIProcMeta): string =
60+
## Mirrors the Nim macro: <CamelCase(procName)>{Ctor}Req.
61+
let camel = toCamelCase(p.procName)
62+
if p.kind == ffiCtorKind: camel & "CtorReq" else: camel & "Req"
63+
64+
proc emitMap(fields: openArray[tuple[name: string, typeName: string, isPtr: bool]]): string =
65+
if fields.len == 0:
66+
return "{ }"
67+
var parts: seq[string] = @[]
68+
for f in fields:
69+
let cddlType =
70+
if f.isPtr: "uint"
71+
else: nimTypeToCddl(f.typeName)
72+
parts.add(f.name & ": " & cddlType)
73+
"{ " & parts.join(", ") & " }"
74+
75+
proc emitObjectFields(t: FFITypeMeta): string =
76+
var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[]
77+
for f in t.fields:
78+
let isPtr = f.typeName.startsWith("ptr ")
79+
let tn = if isPtr: f.typeName[4 .. ^1] else: f.typeName
80+
fields.add((name: f.name, typeName: tn, isPtr: isPtr))
81+
emitMap(fields)
82+
83+
proc emitReqFields(p: FFIProcMeta): string =
84+
var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[]
85+
for ep in p.extraParams:
86+
fields.add((name: ep.name, typeName: ep.typeName, isPtr: ep.isPtr))
87+
emitMap(fields)
88+
89+
proc responseRule(p: FFIProcMeta): string =
90+
## CDDL shape of the success payload returned by the FFI callback.
91+
## Error payloads stay as raw UTF-8 and are intentionally absent from the schema.
92+
case p.kind
93+
of ffiCtorKind:
94+
# The ctor returns the FFI context address as a CBOR-encoded decimal string.
95+
"tstr"
96+
of ffiDtorKind:
97+
# The dtor has no meaningful payload — handleRes sends a CBOR null sentinel.
98+
"nil"
99+
of ffiFfiKind:
100+
if p.returnIsPtr: "uint"
101+
else: nimTypeToCddl(p.returnTypeName)
102+
103+
proc generateCddlSchema*(
104+
procs: seq[FFIProcMeta],
105+
types: seq[FFITypeMeta],
106+
libName: string,
107+
nimSrcRelPath: string,
108+
): string =
109+
var L: seq[string] = @[]
110+
L.add("; CDDL schema for `" & libName & "` — auto-generated from " & nimSrcRelPath)
111+
L.add("; Wire format: CBOR (RFC 8949). Errors return raw UTF-8 (not CBOR) and")
112+
L.add("; are intentionally absent from this schema.")
113+
L.add("")
114+
115+
if types.len > 0:
116+
L.add("; ─── User-declared FFI types ──────────────────────────────────────")
117+
for t in types:
118+
L.add(t.name & " = " & emitObjectFields(t))
119+
L.add("")
120+
121+
# Per-proc request envelopes (one CBOR blob per request).
122+
let nonDtor = block:
123+
var r: seq[FFIProcMeta] = @[]
124+
for p in procs:
125+
if p.kind != ffiDtorKind: r.add(p)
126+
r
127+
if nonDtor.len > 0:
128+
L.add("; ─── Request envelopes (one CBOR blob per request) ────────────────")
129+
for p in nonDtor:
130+
L.add(reqStructName(p) & " = " & emitReqFields(p))
131+
L.add("")
132+
133+
# Per-proc request/response rules.
134+
L.add("; ─── Procs ─────────────────────────────────────────────────────────")
135+
for p in procs:
136+
let kindTag =
137+
case p.kind
138+
of ffiCtorKind: "ctor"
139+
of ffiDtorKind: "dtor"
140+
of ffiFfiKind:
141+
if p.isAsync: "async" else: "sync"
142+
L.add("; " & p.procName & " (" & kindTag & ")")
143+
if p.kind != ffiDtorKind:
144+
L.add(p.procName & "-request = " & reqStructName(p))
145+
L.add(p.procName & "-response = " & responseRule(p))
146+
L.add("")
147+
148+
result = L.join("\n")
149+
150+
proc generateCddlBindings*(
151+
procs: seq[FFIProcMeta],
152+
types: seq[FFITypeMeta],
153+
libName: string,
154+
outputDir: string,
155+
nimSrcRelPath: string,
156+
) =
157+
createDir(outputDir)
158+
writeFile(
159+
outputDir / (libName & ".cddl"),
160+
generateCddlSchema(procs, types, libName, nimSrcRelPath),
161+
)

ffi/internal/ffi_macro.nim

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ../codegen/meta
55
when defined(ffiGenBindings):
66
import ../codegen/rust
77
import ../codegen/cpp
8+
import ../codegen/cddl
89

910
# ---------------------------------------------------------------------------
1011
# String helpers used by multiple macros
@@ -1432,7 +1433,14 @@ macro genBindings*(
14321433
generateCppBindings(
14331434
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath
14341435
)
1436+
of "cddl":
1437+
generateCddlBindings(
1438+
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath
1439+
)
14351440
else:
1436-
error("genBindings: unknown targetLang '" & lang & "'. Use 'rust' or 'cpp'.")
1441+
error(
1442+
"genBindings: unknown targetLang '" & lang &
1443+
"'. Use 'rust', 'cpp', or 'cddl'."
1444+
)
14371445

14381446
result = newEmptyNode()

tests/test_cddl_codegen.nim

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
## Unit-tests for the CDDL schema generator. Drives it directly against a
2+
## synthetic `ffiProcRegistry` / `ffiTypeRegistry` so we don't need to invoke
3+
## the macro pipeline (and thus don't write any files).
4+
5+
import std/strutils
6+
import unittest2
7+
import ../ffi/codegen/[meta, cddl]
8+
9+
proc fieldsOf(pairs: openArray[(string, string)]): seq[FFIFieldMeta] =
10+
result = @[]
11+
for p in pairs:
12+
result.add(FFIFieldMeta(name: p[0], typeName: p[1]))
13+
14+
proc paramsOf(triples: openArray[(string, string, bool)]): seq[FFIParamMeta] =
15+
result = @[]
16+
for t in triples:
17+
result.add(FFIParamMeta(name: t[0], typeName: t[1], isPtr: t[2]))
18+
19+
proc field(n, t: string): FFIFieldMeta =
20+
FFIFieldMeta(name: n, typeName: t)
21+
22+
proc param(n, t: string, isPtr = false): FFIParamMeta =
23+
FFIParamMeta(name: n, typeName: t, isPtr: isPtr)
24+
25+
suite "nimTypeToCddl primitive mapping":
26+
test "primitives map to CDDL builtins":
27+
check nimTypeToCddl("bool") == "bool"
28+
check nimTypeToCddl("int") == "int"
29+
check nimTypeToCddl("int32") == "int"
30+
check nimTypeToCddl("uint64") == "uint"
31+
check nimTypeToCddl("float64") == "float64"
32+
check nimTypeToCddl("string") == "tstr"
33+
check nimTypeToCddl("cstring") == "tstr"
34+
check nimTypeToCddl("pointer") == "uint"
35+
36+
test "pointer types map to uint":
37+
check nimTypeToCddl("ptr Foo") == "uint"
38+
39+
test "seq[T] becomes [* T]":
40+
check nimTypeToCddl("seq[int]") == "[* int]"
41+
check nimTypeToCddl("seq[string]") == "[* tstr]"
42+
43+
test "Option[T] becomes T / nil":
44+
check nimTypeToCddl("Option[int]") == "int / nil"
45+
check nimTypeToCddl("Maybe[string]") == "tstr / nil"
46+
47+
test "nested generics":
48+
check nimTypeToCddl("seq[Option[int]]") == "[* int / nil]"
49+
50+
test "unknown PascalCase is passed through as a rule reference":
51+
check nimTypeToCddl("EchoRequest") == "EchoRequest"
52+
53+
suite "generateCddlSchema":
54+
setup:
55+
let types = @[
56+
FFITypeMeta(
57+
name: "EchoRequest",
58+
fields: @[field("message", "string"), field("delayMs", "int")],
59+
),
60+
FFITypeMeta(name: "EchoResponse", fields: @[field("echoed", "string")]),
61+
]
62+
63+
let procs = @[
64+
FFIProcMeta(
65+
procName: "nimtimer_create",
66+
libName: "nimtimer",
67+
kind: ffiCtorKind,
68+
libTypeName: "NimTimer",
69+
extraParams: @[param("config", "TimerConfig")],
70+
returnTypeName: "NimTimer",
71+
returnIsPtr: false,
72+
isAsync: true,
73+
),
74+
FFIProcMeta(
75+
procName: "nimtimer_echo",
76+
libName: "nimtimer",
77+
kind: ffiFfiKind,
78+
libTypeName: "NimTimer",
79+
extraParams: @[param("req", "EchoRequest")],
80+
returnTypeName: "EchoResponse",
81+
returnIsPtr: false,
82+
isAsync: true,
83+
),
84+
FFIProcMeta(
85+
procName: "nimtimer_destroy",
86+
libName: "nimtimer",
87+
kind: ffiDtorKind,
88+
libTypeName: "NimTimer",
89+
extraParams: @[],
90+
returnTypeName: "",
91+
returnIsPtr: false,
92+
isAsync: false,
93+
),
94+
]
95+
96+
let cddl = generateCddlSchema(procs, types, "nimtimer", "../nim_timer.nim")
97+
98+
test "header references the source file":
99+
check "../nim_timer.nim" in cddl
100+
check "CBOR (RFC 8949)" in cddl
101+
102+
test "user-declared types become CDDL map rules":
103+
check "EchoRequest = { message: tstr, delayMs: int }" in cddl
104+
check "EchoResponse = { echoed: tstr }" in cddl
105+
106+
test "per-proc request envelopes are emitted":
107+
check "NimtimerCreateCtorReq = { config: TimerConfig }" in cddl
108+
check "NimtimerEchoReq = { req: EchoRequest }" in cddl
109+
110+
test "proc request/response rules":
111+
check "nimtimer_create-request = NimtimerCreateCtorReq" in cddl
112+
check "nimtimer_create-response = tstr" in cddl
113+
check "nimtimer_echo-request = NimtimerEchoReq" in cddl
114+
check "nimtimer_echo-response = EchoResponse" in cddl
115+
116+
test "dtor has no request envelope and a nil response":
117+
check "nimtimer_destroy-request" notin cddl
118+
check "nimtimer_destroy-response = nil" in cddl
119+
120+
test "kind tags appear in proc comments":
121+
check "; nimtimer_create (ctor)" in cddl
122+
check "; nimtimer_echo (async)" in cddl
123+
check "; nimtimer_destroy (dtor)" in cddl

0 commit comments

Comments
 (0)