mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-27 10:03:15 +00:00
refactor(codegen): shared FFIType IR + single type parser for C/C++/Rust (#104)
This commit is contained in:
parent
08509cc74f
commit
8c1343aaf2
@ -13,7 +13,7 @@
|
||||
## distinctly-named codec emitted by the cbor_helpers template.
|
||||
|
||||
import std/[os, strutils, tables, sets]
|
||||
import ./meta, ./string_helpers, ./c_cpp_common
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir
|
||||
|
||||
## Wire-format C type for any Nim `ptr T` / `pointer`. Fixed 64-bit so the CBOR
|
||||
## payload size is stable regardless of host architecture (mirrors CppPtrType).
|
||||
@ -30,81 +30,43 @@ const
|
||||
PreludeHeaderName* = "nim_ffi_prelude.h"
|
||||
CborHeaderName* = "nim_ffi_cbor.h"
|
||||
|
||||
type LeafInfo = tuple[ok: bool, cType: string, suffix: string, owns: bool]
|
||||
|
||||
func leafCType(t: string): LeafInfo =
|
||||
## Maps a Nim leaf type to its C type, codec suffix and whether a decoded
|
||||
## value owns heap memory. `ok` is false for composite types (seq/Option/
|
||||
## user structs), which are monomorphised separately.
|
||||
case t
|
||||
of "int", "int64":
|
||||
(true, "int64_t", "i64", false)
|
||||
of "int32":
|
||||
(true, "int32_t", "i32", false)
|
||||
of "int16":
|
||||
(true, "int16_t", "i16", false)
|
||||
of "int8":
|
||||
(true, "int8_t", "i8", false)
|
||||
of "uint", "uint64":
|
||||
(true, "uint64_t", "u64", false)
|
||||
of "uint32":
|
||||
(true, "uint32_t", "u32", false)
|
||||
of "uint16":
|
||||
(true, "uint16_t", "u16", false)
|
||||
of "uint8", "byte":
|
||||
(true, "uint8_t", "u8", false)
|
||||
of "bool":
|
||||
(true, "bool", "bool", false)
|
||||
of "float", "float64":
|
||||
(true, "double", "f64", false)
|
||||
of "float32":
|
||||
(true, "float", "f32", false)
|
||||
of "pointer":
|
||||
(true, CPtrType, "u64", false)
|
||||
of "string", "cstring":
|
||||
(true, "NimFfiStr", "str", true)
|
||||
else:
|
||||
(false, "", "", false)
|
||||
|
||||
func cToken(cType: string): string =
|
||||
## Short PascalCase token used to build monomorphised container names and
|
||||
## codec-adapter symbols. Composite C type names are already unique C
|
||||
## identifiers, so they pass through verbatim.
|
||||
case cType
|
||||
of "int64_t": "I64"
|
||||
of "int32_t": "I32"
|
||||
of "int16_t": "I16"
|
||||
of "int8_t": "I8"
|
||||
of "uint64_t": "U64"
|
||||
of "uint32_t": "U32"
|
||||
of "uint16_t": "U16"
|
||||
of "uint8_t": "U8"
|
||||
of "bool": "Bool"
|
||||
of "double": "F64"
|
||||
of "float": "F32"
|
||||
of "NimFfiStr": "Str"
|
||||
of "NimFfiBytes": "Bytes"
|
||||
else: cType
|
||||
const scalarCInfoTable: array[ScalarKind, tuple[cType, suffix: string]] = [
|
||||
skBool: ("bool", "bool"),
|
||||
skI8: ("int8_t", "i8"),
|
||||
skI16: ("int16_t", "i16"),
|
||||
skI32: ("int32_t", "i32"),
|
||||
skI64: ("int64_t", "i64"),
|
||||
skU8: ("uint8_t", "u8"),
|
||||
skU16: ("uint16_t", "u16"),
|
||||
skU32: ("uint32_t", "u32"),
|
||||
skU64: ("uint64_t", "u64"),
|
||||
skF32: ("float", "f32"),
|
||||
skF64: ("double", "f64"),
|
||||
]
|
||||
|
||||
func leafSuffix(cType: string): string =
|
||||
## Inverse of leafCType's cType→suffix for the leaf codecs the template
|
||||
## provides; empty string for composite types.
|
||||
## C type name → leaf codec suffix for the leaf codecs the template provides;
|
||||
## empty string for composite types. Driven off the shared scalar table so it
|
||||
## can't drift from the IR's scalar set.
|
||||
for s in ScalarKind:
|
||||
if scalarCInfoTable[s].cType == cType:
|
||||
return scalarCInfoTable[s].suffix
|
||||
case cType
|
||||
of "int64_t": "i64"
|
||||
of "int32_t": "i32"
|
||||
of "int16_t": "i16"
|
||||
of "int8_t": "i8"
|
||||
of "uint64_t": "u64"
|
||||
of "uint32_t": "u32"
|
||||
of "uint16_t": "u16"
|
||||
of "uint8_t": "u8"
|
||||
of "bool": "bool"
|
||||
of "double": "f64"
|
||||
of "float": "f32"
|
||||
of "NimFfiStr": "str"
|
||||
of "NimFfiBytes": "bytes"
|
||||
else: ""
|
||||
|
||||
func cToken(cType: string): string =
|
||||
## Short PascalCase token used to build monomorphised container names and
|
||||
## codec-adapter symbols. Leaf types reuse their codec suffix (e.g.
|
||||
## `int64_t`→`I64`); composite C type names are already unique C identifiers,
|
||||
## so they pass through verbatim.
|
||||
let suffix = leafSuffix(cType)
|
||||
if suffix.len > 0:
|
||||
capitalizeFirstLetter(suffix)
|
||||
else:
|
||||
cType
|
||||
|
||||
type CTypeReg = object
|
||||
libName: string ## snake_case symbol prefix, e.g. "my_timer"
|
||||
libType: string ## PascalCase container-name prefix, e.g. "MyTimer"
|
||||
@ -293,44 +255,48 @@ proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) =
|
||||
reg.codecs.add(body.join("\n"))
|
||||
reg.owns[t.name] = owns
|
||||
|
||||
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] =
|
||||
let t = nimType.strip()
|
||||
if t.startsWith("ptr ") or t == "pointer":
|
||||
proc ensureCType(reg: var CTypeReg, t: FFIType): tuple[cType: string, owns: bool] =
|
||||
## Walks the shared type IR into a C type, monomorphising each distinct
|
||||
## `seq[T]` / `Option[T]` into its own struct + codec triple on first sight.
|
||||
## `owns` marks a C type that carries heap-allocated payload the caller must
|
||||
## release with its generated free function (strings, byte buffers, and any
|
||||
## seq/opt/struct transitively containing one); plain scalars and pointers own
|
||||
## nothing and need no cleanup.
|
||||
case t.kind
|
||||
of ftPtr:
|
||||
return (CPtrType, false)
|
||||
let leaf = leafCType(t)
|
||||
if leaf.ok:
|
||||
return (leaf.cType, leaf.owns)
|
||||
|
||||
let seqInner = genericInnerType(t, "seq[")
|
||||
if seqInner.len > 0:
|
||||
let inner = seqInner.strip()
|
||||
if inner == "byte" or inner == "uint8":
|
||||
return ("NimFfiBytes", true)
|
||||
let (elemC, _) = ensureCType(reg, inner)
|
||||
of ftScalar:
|
||||
return (scalarCInfoTable[t.scalar].cType, false)
|
||||
of ftStr:
|
||||
return ("NimFfiStr", true)
|
||||
of ftBytes:
|
||||
return ("NimFfiBytes", true)
|
||||
of ftSeq:
|
||||
let (elemC, _) = ensureCType(reg, t.elem)
|
||||
let name = reg.libType & "Seq_" & cToken(elemC)
|
||||
if name notin reg.emitted:
|
||||
reg.emitted.incl(name)
|
||||
emitSeqType(reg, name, elemC)
|
||||
return (name, true)
|
||||
|
||||
var optInner = genericInnerType(t, "Option[")
|
||||
if optInner.len == 0:
|
||||
optInner = genericInnerType(t, "Maybe[")
|
||||
if optInner.len > 0:
|
||||
let (elemC, elemOwns) = ensureCType(reg, optInner.strip())
|
||||
of ftOpt:
|
||||
let (elemC, elemOwns) = ensureCType(reg, t.elem)
|
||||
let name = reg.libType & "Opt_" & cToken(elemC)
|
||||
if name notin reg.emitted:
|
||||
reg.emitted.incl(name)
|
||||
emitOptType(reg, name, elemC, elemOwns)
|
||||
return (name, reg.owns.getOrDefault(name, false))
|
||||
of ftStruct:
|
||||
let name = t.name
|
||||
if name notin reg.emitted:
|
||||
reg.emitted.incl(name)
|
||||
if name in reg.typeTable:
|
||||
emitStructType(reg, reg.typeTable[name])
|
||||
else:
|
||||
reg.decls.add("/* unknown type referenced: " & name & " */")
|
||||
return (name, reg.owns.getOrDefault(name, false))
|
||||
|
||||
if t notin reg.emitted:
|
||||
reg.emitted.incl(t)
|
||||
if t in reg.typeTable:
|
||||
emitStructType(reg, reg.typeTable[t])
|
||||
else:
|
||||
reg.decls.add("/* unknown type referenced: " & t & " */")
|
||||
(t, reg.owns.getOrDefault(t, false))
|
||||
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] =
|
||||
ensureCType(reg, parseFFIType(nimType))
|
||||
|
||||
proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
|
||||
## Synthesises the per-proc Req struct as an FFITypeMeta so it flows through
|
||||
@ -344,10 +310,12 @@ proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
|
||||
|
||||
func paramByValue(nimType: string, ridesAsPtr: bool): bool =
|
||||
## Scalars / opaque pointers / string views pass by value; composite
|
||||
## aggregates (seq, Option, user structs) pass by const pointer.
|
||||
## aggregates (seq, Option, user structs) pass by const pointer. Note `ptr T`
|
||||
## rides by value as the 64-bit wire int (like `pointer`); production params
|
||||
## reach here as `pointer` since handles are pre-converted upstream.
|
||||
if ridesAsPtr:
|
||||
return true
|
||||
leafCType(nimType.strip()).ok
|
||||
parseFFIType(nimType).kind in {ftScalar, ftStr, ftPtr}
|
||||
|
||||
proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string =
|
||||
if p.returnRidesAsPtr():
|
||||
|
||||
@ -6,16 +6,6 @@
|
||||
import std/strutils
|
||||
import ./meta, ./string_helpers
|
||||
|
||||
proc genericInnerType*(typeName, prefix: string): string =
|
||||
## Inner type of a single-parameter generic written `Prefix[Inner]`, e.g.
|
||||
## `genericInnerType("seq[int]", "seq[")` → `"int"`. Empty string when
|
||||
## `typeName` is not of that shape.
|
||||
if typeName.startsWith(prefix) and typeName.endsWith("]"):
|
||||
let start = prefix.len
|
||||
let lastIndex = typeName.len - 2
|
||||
return typeName[start .. lastIndex]
|
||||
return ""
|
||||
|
||||
proc stripLibPrefix*(procName, libName: string): string =
|
||||
## Drops the `<lib>_` prefix from an exported C symbol, e.g.
|
||||
## `stripLibPrefix("timer_echo", "timer")` → `"echo"`.
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
## the Nim-side cbor_serial codec on the wire — both ends speak RFC 8949).
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta, ./string_helpers, ./c_cpp_common
|
||||
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir
|
||||
|
||||
## Wire-format C++ type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so
|
||||
## the CBOR payload size is stable regardless of host architecture.
|
||||
@ -21,35 +21,37 @@ const
|
||||
ContextRuleOf5Tpl = staticRead("templates/cpp/context_rule_of_5.hpp.tpl")
|
||||
CMakeListsTpl = staticRead("templates/cpp/CMakeLists.txt.tpl")
|
||||
|
||||
func cppScalar(s: ScalarKind): string =
|
||||
case s
|
||||
of skBool: "bool"
|
||||
of skI8: "int8_t"
|
||||
of skI16: "int16_t"
|
||||
of skI32: "int32_t"
|
||||
of skI64: "int64_t"
|
||||
of skU8: "uint8_t"
|
||||
of skU16: "uint16_t"
|
||||
of skU32: "uint32_t"
|
||||
of skU64: "uint64_t"
|
||||
of skF32: "float"
|
||||
of skF64: "double"
|
||||
|
||||
func cppSeq(elem: string): string =
|
||||
"std::vector<" & elem & ">"
|
||||
|
||||
func cppOpt(elem: string): string =
|
||||
"std::optional<" & elem & ">"
|
||||
|
||||
const cppMap = NativeTypeMap(
|
||||
scalar: cppScalar,
|
||||
str: "std::string",
|
||||
bytes: "std::vector<uint8_t>",
|
||||
ptrType: CppPtrType,
|
||||
seqOf: cppSeq,
|
||||
optOf: cppOpt,
|
||||
) ## structName omitted: C++ uses the user type name verbatim
|
||||
|
||||
proc nimTypeToCpp*(typeName: string): string =
|
||||
let trimmed = typeName.strip()
|
||||
if trimmed.startsWith("ptr "):
|
||||
return CppPtrType
|
||||
else:
|
||||
let seqInner = genericInnerType(trimmed, "seq[")
|
||||
if seqInner.len > 0:
|
||||
return "std::vector<" & nimTypeToCpp(seqInner) & ">"
|
||||
let optionInner = genericInnerType(trimmed, "Option[")
|
||||
if optionInner.len > 0:
|
||||
return "std::optional<" & nimTypeToCpp(optionInner) & ">"
|
||||
let maybeInner = genericInnerType(trimmed, "Maybe[")
|
||||
if maybeInner.len > 0:
|
||||
return "std::optional<" & nimTypeToCpp(maybeInner) & ">"
|
||||
case trimmed
|
||||
of "string", "cstring": "std::string"
|
||||
of "int", "int64": "int64_t"
|
||||
of "int32": "int32_t"
|
||||
of "int16": "int16_t"
|
||||
of "int8": "int8_t"
|
||||
of "uint", "uint64": "uint64_t"
|
||||
of "uint32": "uint32_t"
|
||||
of "uint16": "uint16_t"
|
||||
of "uint8", "byte": "uint8_t"
|
||||
of "bool": "bool"
|
||||
of "float", "float32": "float"
|
||||
of "float64": "double"
|
||||
of "pointer": CppPtrType
|
||||
else: trimmed
|
||||
renderNative(cppMap, parseFFIType(typeName))
|
||||
|
||||
proc emitStructCborCodec(
|
||||
lines: var seq[string], structName: string, fields: seq[(string, string)]
|
||||
|
||||
@ -1,38 +1,47 @@
|
||||
## Rust binding generator for the nim-ffi framework.
|
||||
## Generates a complete Rust crate that uses CBOR (ciborium) on the wire.
|
||||
|
||||
import std/[os, strutils, sequtils]
|
||||
import ./meta, ./string_helpers
|
||||
import std/[os, strutils]
|
||||
import ./meta, ./string_helpers, ./types_ir
|
||||
|
||||
## Wire-format Rust type used for any Nim `ptr T` / `pointer`. Fixed 64-bit so
|
||||
## the CBOR payload size is stable regardless of host architecture (mirrors
|
||||
## CppPtrType in cpp.nim).
|
||||
const RustPtrType* = "u64"
|
||||
|
||||
func rustScalar(s: ScalarKind): string =
|
||||
case s
|
||||
of skBool: "bool"
|
||||
of skI8: "i8"
|
||||
of skI16: "i16"
|
||||
of skI32: "i32"
|
||||
of skI64: "i64"
|
||||
of skU8: "u8"
|
||||
of skU16: "u16"
|
||||
of skU32: "u32"
|
||||
of skU64: "u64"
|
||||
of skF32: "f32"
|
||||
of skF64: "f64"
|
||||
|
||||
func rustSeq(elem: string): string =
|
||||
"Vec<" & elem & ">"
|
||||
|
||||
func rustOpt(elem: string): string =
|
||||
"Option<" & elem & ">"
|
||||
|
||||
const rustMap = NativeTypeMap(
|
||||
scalar: rustScalar,
|
||||
str: "String",
|
||||
bytes: "Vec<u8>",
|
||||
ptrType: RustPtrType,
|
||||
seqOf: rustSeq,
|
||||
optOf: rustOpt,
|
||||
structName: capitalizeFirstLetter,
|
||||
)
|
||||
|
||||
proc nimTypeToRust*(typeName: string): string =
|
||||
## Maps Nim type names to Rust type names, including generics.
|
||||
let t = typeName.strip()
|
||||
if t.startsWith("seq[") and t.endsWith("]"):
|
||||
return "Vec<" & nimTypeToRust(t[4 .. ^2]) & ">"
|
||||
if t.startsWith("Option[") and t.endsWith("]"):
|
||||
return "Option<" & nimTypeToRust(t[7 .. ^2]) & ">"
|
||||
if t.startsWith("Maybe[") and t.endsWith("]"):
|
||||
return "Option<" & nimTypeToRust(t[6 .. ^2]) & ">"
|
||||
case t
|
||||
of "string", "cstring":
|
||||
"String"
|
||||
of "int", "int64":
|
||||
"i64"
|
||||
of "int32":
|
||||
"i32"
|
||||
of "bool":
|
||||
"bool"
|
||||
of "float", "float64":
|
||||
"f64"
|
||||
of "pointer":
|
||||
RustPtrType
|
||||
else:
|
||||
capitalizeFirstLetter(t)
|
||||
renderNative(rustMap, parseFFIType(typeName))
|
||||
|
||||
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
|
||||
## Extracts the common prefix before the first `_` from proc names.
|
||||
|
||||
@ -25,7 +25,7 @@ proc camelToSnakeCase*(s: string): string =
|
||||
first = false
|
||||
return snake
|
||||
|
||||
proc capitalizeFirstLetter*(s: string): string =
|
||||
func capitalizeFirstLetter*(s: string): string =
|
||||
## Returns `s` with its first rune uppercased; the rest is left unchanged.
|
||||
## e.g. "abc" → "Abc", "" → "", "Abc" → "Abc"
|
||||
if s.len == 0:
|
||||
|
||||
134
ffi/codegen/types_ir.nim
Normal file
134
ffi/codegen/types_ir.nim
Normal file
@ -0,0 +1,134 @@
|
||||
## Structured type model shared by the C / C++ / Rust binding generators: one
|
||||
## parser (`parseFFIType`) for the Nim type strings each backend used to slice
|
||||
## by hand, plus `renderNative` to walk the result into a backend's type string.
|
||||
|
||||
import std/[strutils, options]
|
||||
|
||||
type
|
||||
ScalarKind* {.pure.} = enum
|
||||
skBool
|
||||
skI8
|
||||
skI16
|
||||
skI32
|
||||
skI64
|
||||
skU8
|
||||
skU16
|
||||
skU32
|
||||
skU64
|
||||
skF32
|
||||
skF64
|
||||
|
||||
FFITypeKind* {.pure.} = enum
|
||||
ftScalar
|
||||
ftStr
|
||||
ftBytes
|
||||
ftSeq
|
||||
ftOpt
|
||||
ftPtr
|
||||
ftStruct
|
||||
|
||||
FFIType* = ref object
|
||||
case kind*: FFITypeKind
|
||||
of ftScalar:
|
||||
scalar*: ScalarKind
|
||||
of ftSeq, ftOpt:
|
||||
elem*: FFIType
|
||||
of ftStruct:
|
||||
name*: string
|
||||
else:
|
||||
discard
|
||||
|
||||
NativeTypeMap* = object
|
||||
## A backend's answer to "what do you call this kind?". `seqOf`/`optOf`
|
||||
## wrap an already-rendered element; `structName` maps a user type name.
|
||||
scalar*: proc(s: ScalarKind): string {.noSideEffect, nimcall.}
|
||||
str*: string
|
||||
bytes*: string
|
||||
ptrType*: string
|
||||
seqOf*: proc(elem: string): string {.noSideEffect, nimcall.}
|
||||
optOf*: proc(elem: string): string {.noSideEffect, nimcall.}
|
||||
structName*: proc(name: string): string {.noSideEffect, nimcall.}
|
||||
## nil ⇒ the user type name passes through unchanged
|
||||
|
||||
func genericInnerType(typeName, prefix: string): string =
|
||||
## Inner type of a single-parameter generic `Prefix[Inner]`, e.g.
|
||||
## `genericInnerType("seq[int]", "seq[")` → `"int"`; "" if not that shape.
|
||||
if typeName.startsWith(prefix) and typeName.endsWith("]"):
|
||||
return typeName[prefix.len .. ^2]
|
||||
return ""
|
||||
|
||||
func scalarKind(t: string): Option[ScalarKind] =
|
||||
## Single source of truth for the scalar leaf set every backend shares.
|
||||
case t
|
||||
of "bool":
|
||||
some(skBool)
|
||||
of "int8":
|
||||
some(skI8)
|
||||
of "int16":
|
||||
some(skI16)
|
||||
of "int32":
|
||||
some(skI32)
|
||||
of "int", "int64":
|
||||
some(skI64)
|
||||
of "uint8", "byte":
|
||||
some(skU8)
|
||||
of "uint16":
|
||||
some(skU16)
|
||||
of "uint32":
|
||||
some(skU32)
|
||||
of "uint", "uint64":
|
||||
some(skU64)
|
||||
of "float32":
|
||||
some(skF32)
|
||||
of "float", "float64":
|
||||
some(skF64)
|
||||
else:
|
||||
none(ScalarKind)
|
||||
|
||||
func parseFFIType*(typeName: string): FFIType =
|
||||
## Single source of truth for turning a Nim type string into the shared IR:
|
||||
## ptr/pointer, seq[byte]→bytes, seq/Option/Maybe, scalars, string, else struct.
|
||||
let t = typeName.strip()
|
||||
if t.startsWith("ptr ") or t == "pointer":
|
||||
return FFIType(kind: ftPtr)
|
||||
|
||||
let seqInner = genericInnerType(t, "seq[")
|
||||
if seqInner.len > 0:
|
||||
let inner = seqInner.strip()
|
||||
if inner == "byte" or inner == "uint8":
|
||||
return FFIType(kind: ftBytes)
|
||||
return FFIType(kind: ftSeq, elem: parseFFIType(inner))
|
||||
|
||||
var optInner = genericInnerType(t, "Option[")
|
||||
if optInner.len == 0:
|
||||
optInner = genericInnerType(t, "Maybe[")
|
||||
if optInner.len > 0:
|
||||
return FFIType(kind: ftOpt, elem: parseFFIType(optInner.strip()))
|
||||
|
||||
let sc = scalarKind(t)
|
||||
if sc.isSome():
|
||||
return FFIType(kind: ftScalar, scalar: sc.get())
|
||||
if t == "string" or t == "cstring":
|
||||
return FFIType(kind: ftStr)
|
||||
FFIType(kind: ftStruct, name: t)
|
||||
|
||||
func renderNative*(m: NativeTypeMap, t: FFIType): string =
|
||||
## Recursively walks `t` into a native type string for the backend `m`.
|
||||
case t.kind
|
||||
of ftScalar:
|
||||
m.scalar(t.scalar)
|
||||
of ftStr:
|
||||
m.str
|
||||
of ftBytes:
|
||||
m.bytes
|
||||
of ftPtr:
|
||||
m.ptrType
|
||||
of ftSeq:
|
||||
m.seqOf(renderNative(m, t.elem))
|
||||
of ftOpt:
|
||||
m.optOf(renderNative(m, t.elem))
|
||||
of ftStruct:
|
||||
if m.structName.isNil():
|
||||
t.name
|
||||
else:
|
||||
m.structName(t.name)
|
||||
43
tests/unit/test_rust_codegen.nim
Normal file
43
tests/unit/test_rust_codegen.nim
Normal file
@ -0,0 +1,43 @@
|
||||
## Regression tests for the Rust type mapping. `nimTypeToRust` used to carry its
|
||||
## own scalar table that had drifted from C/C++ — `int8`/`int16`/`uint8`/
|
||||
## `uint16`/`uint32`/`byte`/`float32` fell through to `capitalizeFirstLetter`
|
||||
## and emitted invalid Rust. It now renders through the shared `parseFFIType`
|
||||
## IR, so the full scalar set is pinned here.
|
||||
|
||||
import unittest2
|
||||
import ffi/codegen/rust
|
||||
|
||||
suite "nimTypeToRust: scalar set":
|
||||
test "every scalar maps to its Rust primitive (the drift that regressed)":
|
||||
check nimTypeToRust("bool") == "bool"
|
||||
check nimTypeToRust("int8") == "i8"
|
||||
check nimTypeToRust("int16") == "i16"
|
||||
check nimTypeToRust("int32") == "i32"
|
||||
check nimTypeToRust("int") == "i64"
|
||||
check nimTypeToRust("int64") == "i64"
|
||||
check nimTypeToRust("uint8") == "u8"
|
||||
check nimTypeToRust("byte") == "u8"
|
||||
check nimTypeToRust("uint16") == "u16"
|
||||
check nimTypeToRust("uint32") == "u32"
|
||||
check nimTypeToRust("uint") == "u64"
|
||||
check nimTypeToRust("uint64") == "u64"
|
||||
check nimTypeToRust("float32") == "f32"
|
||||
check nimTypeToRust("float") == "f64"
|
||||
check nimTypeToRust("float64") == "f64"
|
||||
|
||||
suite "nimTypeToRust: strings, pointers and containers":
|
||||
test "string types render to String":
|
||||
check nimTypeToRust("string") == "String"
|
||||
check nimTypeToRust("cstring") == "String"
|
||||
|
||||
test "seq[byte] collapses to Vec<u8> and ptr/pointer to the wire int":
|
||||
check nimTypeToRust("seq[byte]") == "Vec<u8>"
|
||||
check nimTypeToRust("ptr Foo") == RustPtrType
|
||||
check nimTypeToRust("pointer") == RustPtrType
|
||||
|
||||
test "generics nest and Maybe aliases Option":
|
||||
check nimTypeToRust("seq[Option[int8]]") == "Vec<Option<i8>>"
|
||||
check nimTypeToRust("Maybe[uint16]") == "Option<u16>"
|
||||
|
||||
test "an unknown user type is capitalised, not mistaken for a scalar":
|
||||
check nimTypeToRust("echoRequest") == "EchoRequest"
|
||||
122
tests/unit/test_types_ir.nim
Normal file
122
tests/unit/test_types_ir.nim
Normal file
@ -0,0 +1,122 @@
|
||||
## Unit tests for the shared type IR that the C / C++ / Rust binding generators
|
||||
## parse Nim type strings through. `parseFFIType` is the single source of truth
|
||||
## the three backends consume, so its shape mappings are pinned here directly.
|
||||
|
||||
import unittest2
|
||||
import ffi/codegen/types_ir
|
||||
|
||||
suite "parseFFIType: scalars":
|
||||
test "the full scalar set round-trips to its ScalarKind":
|
||||
let cases = {
|
||||
"bool": skBool,
|
||||
"int8": skI8,
|
||||
"int16": skI16,
|
||||
"int32": skI32,
|
||||
"int": skI64,
|
||||
"int64": skI64,
|
||||
"uint8": skU8,
|
||||
"byte": skU8,
|
||||
"uint16": skU16,
|
||||
"uint32": skU32,
|
||||
"uint": skU64,
|
||||
"uint64": skU64,
|
||||
"float32": skF32,
|
||||
"float": skF64,
|
||||
"float64": skF64,
|
||||
}
|
||||
for (name, kind) in cases:
|
||||
let t = parseFFIType(name)
|
||||
check t.kind == ftScalar
|
||||
check t.scalar == kind
|
||||
|
||||
test "surrounding whitespace is stripped":
|
||||
let t = parseFFIType(" int ")
|
||||
check t.kind == ftScalar
|
||||
check t.scalar == skI64
|
||||
|
||||
suite "parseFFIType: strings, bytes and pointers":
|
||||
test "string and cstring are ftStr":
|
||||
check parseFFIType("string").kind == ftStr
|
||||
check parseFFIType("cstring").kind == ftStr
|
||||
|
||||
test "seq[byte] and seq[uint8] collapse to ftBytes":
|
||||
check parseFFIType("seq[byte]").kind == ftBytes
|
||||
check parseFFIType("seq[uint8]").kind == ftBytes
|
||||
|
||||
test "ptr T and pointer are ftPtr":
|
||||
check parseFFIType("ptr Foo").kind == ftPtr
|
||||
check parseFFIType("pointer").kind == ftPtr
|
||||
|
||||
suite "parseFFIType: containers":
|
||||
test "seq[T] wraps the parsed element":
|
||||
let t = parseFFIType("seq[int]")
|
||||
check t.kind == ftSeq
|
||||
check t.elem.kind == ftScalar
|
||||
check t.elem.scalar == skI64
|
||||
|
||||
test "Maybe[T] is the same as Option[T]":
|
||||
let opt = parseFFIType("Option[string]")
|
||||
let maybe = parseFFIType("Maybe[string]")
|
||||
check opt.kind == ftOpt
|
||||
check maybe.kind == ftOpt
|
||||
check opt.elem.kind == ftStr
|
||||
check maybe.elem.kind == ftStr
|
||||
|
||||
test "nested containers parse all the way down":
|
||||
let t = parseFFIType("seq[Option[seq[int]]]")
|
||||
check t.kind == ftSeq
|
||||
check t.elem.kind == ftOpt
|
||||
check t.elem.elem.kind == ftSeq
|
||||
check t.elem.elem.elem.kind == ftScalar
|
||||
check t.elem.elem.elem.scalar == skI64
|
||||
|
||||
test "seq[Option[byte]] keeps the inner byte as a scalar, not bytes":
|
||||
let t = parseFFIType("seq[Option[byte]]")
|
||||
check t.kind == ftSeq
|
||||
check t.elem.kind == ftOpt
|
||||
check t.elem.elem.kind == ftScalar
|
||||
check t.elem.elem.scalar == skU8
|
||||
|
||||
suite "parseFFIType: structs":
|
||||
test "an unknown name is an ftStruct carrying the name":
|
||||
let t = parseFFIType("EchoRequest")
|
||||
check t.kind == ftStruct
|
||||
check t.name == "EchoRequest"
|
||||
|
||||
suite "renderNative: walks the IR with a backend map":
|
||||
let rustish = NativeTypeMap(
|
||||
scalar: proc(s: ScalarKind): string =
|
||||
(
|
||||
case s
|
||||
of skI64: "i64"
|
||||
of skU8: "u8"
|
||||
else: "?"
|
||||
),
|
||||
str: "String",
|
||||
bytes: "Vec<u8>",
|
||||
ptrType: "u64",
|
||||
seqOf: proc(e: string): string =
|
||||
"Vec<" & e & ">",
|
||||
optOf: proc(e: string): string =
|
||||
"Option<" & e & ">",
|
||||
structName: proc(n: string): string =
|
||||
n,
|
||||
)
|
||||
|
||||
test "scalars, strings, bytes and pointers map to the leaf entries":
|
||||
check renderNative(rustish, parseFFIType("int")) == "i64"
|
||||
check renderNative(rustish, parseFFIType("string")) == "String"
|
||||
check renderNative(rustish, parseFFIType("seq[byte]")) == "Vec<u8>"
|
||||
check renderNative(rustish, parseFFIType("ptr Foo")) == "u64"
|
||||
|
||||
test "nested containers render recursively":
|
||||
check renderNative(rustish, parseFFIType("seq[Option[int]]")) == "Vec<Option<i64>>"
|
||||
|
||||
test "a struct renders through structName when set":
|
||||
check renderNative(rustish, parseFFIType("EchoRequest")) == "EchoRequest"
|
||||
|
||||
test "a nil structName passes the user type name through unchanged":
|
||||
let noStructMap = NativeTypeMap(
|
||||
scalar: rustish.scalar, str: "String", bytes: "Vec<u8>", ptrType: "u64"
|
||||
)
|
||||
check renderNative(noStructMap, parseFFIType("EchoRequest")) == "EchoRequest"
|
||||
Loading…
x
Reference in New Issue
Block a user