From 08751bab4354610508b2bac41dcd204cc3aa38b8 Mon Sep 17 00:00:00 2001 From: treeform Date: Mon, 25 May 2026 12:42:44 -0700 Subject: [PATCH] Add owned string buffer ABI --- src/genny/internal.nim | 39 +++++++--- src/genny/languages/c.nim | 23 +++++- src/genny/languages/cpp.nim | 104 ++++++++++++++++++++++---- src/genny/languages/nim.nim | 104 ++++++++++++++++++++------ src/genny/languages/node.nim | 38 +++++++++- src/genny/languages/python.nim | 38 +++++++++- src/genny/languages/python_native.nim | 52 ++++++++++--- src/genny/languages/zig.nim | 65 ++++++++++++++-- tests/generated/internal.nim | 28 ++++++- tests/generated/test.h | 9 ++- tests/generated/test.hpp | 61 +++++++++++++-- tests/generated/test.js | 31 +++++++- tests/generated/test.nim | 27 ++++++- tests/generated/test.py | 32 +++++++- tests/generated/test.zig | 37 ++++++++- tests/test.nim | 4 + tests/test_c.c | 15 ++++ tests/test_cpp.cpp | 10 ++- tests/test_nim.nim | 13 +++- tests/test_node.js | 10 +++ tests/test_pixie_c.c | 33 ++++++-- tests/test_pixie_cpp.cpp | 12 +-- tests/test_pixie_zig.zig | 21 ++++-- tests/test_python.py | 3 + tests/test_python_native.py | 1 + tests/test_zig.zig | 21 ++++++ 26 files changed, 719 insertions(+), 112 deletions(-) diff --git a/src/genny/internal.nim b/src/genny/internal.nim index a7478cc..0247edc 100644 --- a/src/genny/internal.nim +++ b/src/genny/internal.nim @@ -47,7 +47,7 @@ proc exportProcInternal*( internal.removeSuffix ", " internal.add ")" if procReturn.kind != nnkEmpty: - internal.add &": {exportTypeNim(procReturn)}" + internal.add &": {exportReturnTypeNim(procReturn)}" internal.add &" {exportProcPragmas} =\n" if procRaises: internal.add " try:\n " @@ -68,7 +68,7 @@ proc exportProcInternal*( if procReturnsSeq: internal.add &"{procReturn.getSeqName()}(s: {callExpr})" elif procReturn.kind != nnkEmpty: - internal.add convertExportExprNim(callExpr, procReturn) + internal.add convertExportReturnExprNim(callExpr, procReturn) else: internal.add callExpr if procRaises: @@ -155,10 +155,10 @@ proc exportRefObjectInternal*( if fieldType.kind != nnkBracketExpr: internal.add "proc " internal.add &"$lib_{objNameSnaked}_get_{fieldNameSnaked}*" - internal.add &"({objNameSnaked}: {objName}): {exportTypeNim(fieldType)}" + internal.add &"({objNameSnaked}: {objName}): {exportReturnTypeNim(fieldType)}" internal.add &" {exportProcPragmas} =\n" internal.add " " - internal.add convertExportExprNim(&"{objNameSnaked}.{fieldName}", fieldType) + internal.add convertExportReturnExprNim(&"{objNameSnaked}.{fieldName}", fieldType) internal.add "\n" internal.add "\n" @@ -186,10 +186,10 @@ proc exportRefObjectInternal*( internal.add ")\n" internal.add "\n" - internal.add &"proc {prefix}_get*({objNameSnaked}: {objName}, i: int): {exportTypeNim(fieldType[1])}" + internal.add &"proc {prefix}_get*({objNameSnaked}: {objName}, i: int): {exportReturnTypeNim(fieldType[1])}" internal.add &" {exportProcPragmas} =\n" internal.add " " - internal.add convertExportExprNim(&"{objNameSnaked}.{fieldName}[i]", fieldType[1]) + internal.add convertExportReturnExprNim(&"{objNameSnaked}.{fieldName}[i]", fieldType[1]) internal.add "\n" internal.add "\n" @@ -240,11 +240,11 @@ proc generateSeqs(sym: NimNode) = internal.add ")\n" internal.add "\n" - internal.add &"proc $lib_{seqNameSnaked}_get*(s: {seqName}, i: int): {exportTypeNim(sym[1])}" + internal.add &"proc $lib_{seqNameSnaked}_get*(s: {seqName}, i: int): {exportReturnTypeNim(sym[1])}" internal.add &" {exportProcPragmas}" internal.add " =\n" internal.add " " - internal.add convertExportExprNim("s.s[i]", sym[1]) + internal.add convertExportReturnExprNim("s.s[i]", sym[1]) internal.add "\n" internal.add "\n" @@ -283,11 +283,32 @@ when not defined(gcArc) and not defined(gcOrc): when (NimMajor, NimMinor, NimPatch) == (1, 6, 2): {.error: "Nim 1.6.2 not supported with Genny due to FFI issues.".} + +type GennyBuffer* = ref object + data: string + +proc newGennyBuffer*(data: string): GennyBuffer = + result = GennyBuffer(data: data) + GC_ref(result) + +proc $lib_genny_buffer_data*(buffer: GennyBuffer): cstring {.raises: [], cdecl, exportc, dynlib.} = + if buffer == nil: + return nil + buffer.data.cstring + +proc $lib_genny_buffer_len*(buffer: GennyBuffer): int {.raises: [], cdecl, exportc, dynlib.} = + if buffer == nil: + return 0 + buffer.data.len + +proc $lib_genny_buffer_unref*(buffer: GennyBuffer) {.raises: [], cdecl, exportc, dynlib.} = + if buffer != nil: + GC_unref(buffer) """ proc writeInternal*(dir, lib: string) = createDir(dir) writeFile( &"{dir}/internal.nim", - header & internal.replace("$Lib", lib).replace("$lib", toSnakeCase(lib)) + (header & internal).replace("$Lib", lib).replace("$lib", toSnakeCase(lib)) ) diff --git a/src/genny/languages/c.nim b/src/genny/languages/c.nim index 6ee30df..5f28f5c 100644 --- a/src/genny/languages/c.nim +++ b/src/genny/languages/c.nim @@ -55,6 +55,13 @@ proc exportTypeC(sym: NimNode): string = else: typ.repr +proc exportReturnTypeC(sym: NimNode): string = + let typ = sym.stripSink + if typ.repr == "string": + "GennyBuffer" + else: + exportTypeC(typ) + proc exportTypeC(sym: NimNode, name: string): string = let typ = sym.stripSink if typ.kind == nnkBracketExpr: @@ -142,7 +149,7 @@ proc exportProcC*( var dllParams: seq[(NimNode, NimNode)] for param in procParams: dllParams.add((param[0], param[1])) - dllProc(&"$lib_{apiProcName}", dllParams, exportTypeC(procReturn)) + dllProc(&"$lib_{apiProcName}", dllParams, exportReturnTypeC(procReturn)) proc exportObjectC*(sym: NimNode, constructor: NimNode) = let objName = sym.repr @@ -175,7 +182,7 @@ proc genRefObject(objName: string) = proc genSeqProcs(objName, procPrefix, selfSuffix: string, entryType: NimNode) = let objArg = objName & " " & toSnakeCase(objName) dllProc(&"{procPrefix}_len", [objArg], "intptr_t") - dllProc(&"{procPrefix}_get", [objArg, "intptr_t index"], exportTypeC(entryType)) + dllProc(&"{procPrefix}_get", [objArg, "intptr_t index"], exportReturnTypeC(entryType)) dllProc(&"{procPrefix}_set", [objArg, "intptr_t index", exportTypeC(entryType, "value")], "void") dllProc(&"{procPrefix}_delete", [objArg, "intptr_t index"], "void") dllProc(&"{procPrefix}_add", [objArg, exportTypeC(entryType, "value")], "void") @@ -213,7 +220,7 @@ proc exportRefObjectC*( let setProcName = &"$lib_{objNameSnaked}_set_{fieldNameSnaked}" - dllProc(getProcName, [objName & " " & objNameSnaked], exportTypeC(fieldType)) + dllProc(getProcName, [objName & " " & objNameSnaked], exportReturnTypeC(fieldType)) dllProc(setProcName, [objName & " " & objNameSnaked, exportTypeC(fieldType, "value")], exportTypeC(nil)) else: var helperName = fieldName @@ -251,6 +258,14 @@ const header = """ #include +typedef struct GennyBufferHandle* GennyBuffer; + +""" + +const bufferProcs = """ +const char* $lib_genny_buffer_data(GennyBuffer buffer); +intptr_t $lib_genny_buffer_len(GennyBuffer buffer); +void $lib_genny_buffer_unref(GennyBuffer buffer); """ const externCStart = """ @@ -273,6 +288,6 @@ const footer = """ proc writeC*(dir, lib: string) = createDir(dir) - writeFile(&"{dir}/{toSnakeCase(lib)}.h", (header & types & externCStart & procs & externCEnd & footer) + writeFile(&"{dir}/{toSnakeCase(lib)}.h", (header & types & externCStart & bufferProcs & procs & externCEnd & footer) .replace("$lib", toSnakeCase(lib)).replace("$LIB", lib.toUpperAscii()) ) diff --git a/src/genny/languages/cpp.nim b/src/genny/languages/cpp.nim index 3146b28..70011f0 100644 --- a/src/genny/languages/cpp.nim +++ b/src/genny/languages/cpp.nim @@ -24,6 +24,9 @@ proc isSeqLike(sym: NimNode): bool = proc isRuneType(sym: NimNode): bool = sym.stripSink.repr == "Rune" +proc isStringType(sym: NimNode): bool = + sym.stripSink.repr == "string" + proc exportTypeCpp(sym: NimNode, abi = false): string = let typ = sym.stripSink if typ.kind == nnkBracketExpr: @@ -70,6 +73,18 @@ proc exportTypeCpp(sym: NimNode, abi = false): string = proc exportTypeCppAbi(sym: NimNode): string = exportTypeCpp(sym, abi = true) +proc exportReturnTypeCpp(sym: NimNode): string = + if sym.isStringType: + "std::string" + else: + exportTypeCpp(sym) + +proc exportReturnTypeCppAbi(sym: NimNode): string = + if sym.isStringType: + "GennyBuffer" + else: + exportTypeCppAbi(sym) + proc exportTypeCpp(sym: NimNode, name: string, abi = false): string = let typ = sym.stripSink if typ.kind == nnkBracketExpr: @@ -95,7 +110,9 @@ proc cppArgValue(argType: NimNode, argName: string): string = argName proc cppReturnValue(returnType: NimNode, call: string): string = - if returnType.isRuneType: + if returnType.isStringType: + &"gennyBufferToString({call})" + elif returnType.isRuneType: &"static_cast({call})" else: call @@ -156,11 +173,11 @@ proc exportProcCpp*( var dllParams: seq[(NimNode, NimNode)] for param in procParams: dllParams.add((param[0], param[1])) - dllProc(&"$lib_{apiProcName}", dllParams, exportTypeCppAbi(procReturn)) + dllProc(&"$lib_{apiProcName}", dllParams, exportReturnTypeCppAbi(procReturn)) if owner == nil: if procReturn.kind != nnkEmpty: - members.add exportTypeCpp(procReturn) + members.add exportReturnTypeCpp(procReturn) members.add " " members.add procName members.add "(" @@ -199,14 +216,14 @@ proc exportProcCpp*( classes.add &" * {line}\n" classes.add " */\n" - classes.add &" {exportTypeCpp(procReturn)} {procName}(" + classes.add &" {exportReturnTypeCpp(procReturn)} {procName}(" for param in procParams[1..^1]: classes.add exportTypeCpp(param[1], param[0].getName()) classes.add ", " classes.removeSuffix ", " classes.add ");\n\n" - members.add &"{exportTypeCpp(procReturn)} {owner.getName()}::{procName}(" + members.add &"{exportReturnTypeCpp(procReturn)} {owner.getName()}::{procName}(" for param in procParams[1..^1]: members.add exportTypeCpp(param[1], param[0].getName()) members.add ", " @@ -278,7 +295,7 @@ proc genRefObject(objName: string) = proc genSeqProcs(objName, procPrefix, selfSuffix: string, entryType: NimNode) = let objArg = objName & " " & toSnakeCase(objName) dllProc(&"{procPrefix}_len", [objArg], "std::intptr_t") - dllProc(&"{procPrefix}_get", [objArg, "std::intptr_t index"], exportTypeCppAbi(entryType)) + dllProc(&"{procPrefix}_get", [objArg, "std::intptr_t index"], exportReturnTypeCppAbi(entryType)) dllProc(&"{procPrefix}_set", [objArg, "std::intptr_t index", exportTypeCppAbi(entryType, "value")], "void") dllProc(&"{procPrefix}_delete", [objArg, "std::intptr_t index"], "void") dllProc(&"{procPrefix}_add", [objArg, exportTypeCppAbi(entryType, "value")], "void") @@ -287,12 +304,13 @@ proc genSeqProcs(objName, procPrefix, selfSuffix: string, entryType: NimNode) = proc genSeqClassMethods(seqName, procPrefix: string, entryType: NimNode) = let entryTypeCpp = exportTypeCpp(entryType) + entryReturnTypeCpp = exportReturnTypeCpp(entryType) entryValue = cppArgValue(entryType, "value") getCall = &"{procPrefix}_get(*this, index)" classes.add &" std::intptr_t size();\n" - classes.add &" {entryTypeCpp} get(std::intptr_t index);\n" - classes.add &" {entryTypeCpp} operator[](std::intptr_t index);\n" + classes.add &" {entryReturnTypeCpp} get(std::intptr_t index);\n" + classes.add &" {entryReturnTypeCpp} operator[](std::intptr_t index);\n" classes.add &" void set(std::intptr_t index, {entryTypeCpp} value);\n" classes.add &" void removeAt(std::intptr_t index);\n" classes.add &" void add({entryTypeCpp} value);\n" @@ -302,11 +320,11 @@ proc genSeqClassMethods(seqName, procPrefix: string, entryType: NimNode) = members.add &" return {procPrefix}_len(*this);\n" members.add "}\n\n" - members.add &"{entryTypeCpp} {seqName}::get(std::intptr_t index)" & "{\n" + members.add &"{entryReturnTypeCpp} {seqName}::get(std::intptr_t index)" & "{\n" members.add &" return {cppReturnValue(entryType, getCall)};\n" members.add "}\n\n" - members.add &"{entryTypeCpp} {seqName}::operator[](std::intptr_t index)" & "{\n" + members.add &"{entryReturnTypeCpp} {seqName}::operator[](std::intptr_t index)" & "{\n" members.add &" return get(index);\n" members.add "}\n\n" @@ -332,11 +350,12 @@ proc genSeqFieldMethods(objName, procPrefix, fieldName: string, entryType: NimNo let entryTypeCpp = exportTypeCpp(entryType) + entryReturnTypeCpp = exportReturnTypeCpp(entryType) entryValue = cppArgValue(entryType, "value") getCall = &"{procPrefix}_get(*this, index)" classes.add &" std::intptr_t {fieldName}Size();\n" - classes.add &" {entryTypeCpp} get{fieldCap}(std::intptr_t index);\n" + classes.add &" {entryReturnTypeCpp} get{fieldCap}(std::intptr_t index);\n" classes.add &" void set{fieldCap}(std::intptr_t index, {entryTypeCpp} value);\n" classes.add &" void remove{fieldCap}(std::intptr_t index);\n" classes.add &" void add{fieldCap}({entryTypeCpp} value);\n" @@ -346,7 +365,7 @@ proc genSeqFieldMethods(objName, procPrefix, fieldName: string, entryType: NimNo members.add &" return {procPrefix}_len(*this);\n" members.add "}\n\n" - members.add &"{entryTypeCpp} {objName}::get{fieldCap}(std::intptr_t index)" & "{\n" + members.add &"{entryReturnTypeCpp} {objName}::get{fieldCap}(std::intptr_t index)" & "{\n" members.add &" return {cppReturnValue(entryType, getCall)};\n" members.add "}\n\n" @@ -428,12 +447,12 @@ proc exportRefObjectCpp*( let getMemberName = &"get{fieldName.capitalizeAscii}" let setMemberName = &"set{fieldName.capitalizeAscii}" - dllProc(getProcName, [objName & " " & objNameSnaked], exportTypeCppAbi(fieldType)) + dllProc(getProcName, [objName & " " & objNameSnaked], exportReturnTypeCppAbi(fieldType)) dllProc(setProcName, [objName & " " & objNameSnaked, exportTypeCppAbi(fieldType, "value")], exportTypeCppAbi(nil)) - classes.add &" {exportTypeCpp(fieldType)} {getMemberName}();\n" + classes.add &" {exportReturnTypeCpp(fieldType)} {getMemberName}();\n" - members.add &"{exportTypeCpp(fieldType)} {objName}::{getMemberName}()" & "{\n" + members.add &"{exportReturnTypeCpp(fieldType)} {objName}::{getMemberName}()" & "{\n" let getCall = &"{getProcName}(*this)" members.add &" return {cppReturnValue(fieldType, getCall)};\n" members.add "}\n\n" @@ -529,7 +548,59 @@ const header = """ #ifndef INCLUDE_$LIB_H #define INCLUDE_$LIB_H +#include #include +#include + +""" + +const bufferClass = """ +struct GennyBuffer { + + private: + + std::uintptr_t reference; + + public: + + const char* data(); + std::intptr_t len(); + void free(); + +}; + +""" + +const bufferProcs = """ +const char* $lib_genny_buffer_data(GennyBuffer buffer); +std::intptr_t $lib_genny_buffer_len(GennyBuffer buffer); +void $lib_genny_buffer_unref(GennyBuffer buffer); + +""" + +const bufferMembers = """ +static inline std::string gennyBufferToString(GennyBuffer buffer) { + const char* data = $lib_genny_buffer_data(buffer); + std::intptr_t len = $lib_genny_buffer_len(buffer); + std::string result; + if (data != nullptr && len > 0) { + result.assign(data, static_cast(len)); + } + $lib_genny_buffer_unref(buffer); + return result; +} + +const char* GennyBuffer::data() { + return $lib_genny_buffer_data(*this); +} + +std::intptr_t GennyBuffer::len() { + return $lib_genny_buffer_len(*this); +} + +void GennyBuffer::free() { + $lib_genny_buffer_unref(*this); +} """ @@ -542,10 +613,13 @@ proc writeCpp*(dir, lib: string) = writeFile(&"{dir}/{toSnakeCase(lib)}.hpp", ( header & types & + bufferClass & classes & "extern \"C\" {\n\n" & + bufferProcs & procs & "}\n\n" & + bufferMembers & members & footer ).replace("$lib", toSnakeCase(lib)).replace("$LIB", lib.toUpperAscii()) diff --git a/src/genny/languages/nim.nim b/src/genny/languages/nim.nim index 419b5b9..9b7d944 100644 --- a/src/genny/languages/nim.nim +++ b/src/genny/languages/nim.nim @@ -59,6 +59,15 @@ proc exportTypeNim*(sym: NimNode): string = else: return typ.normalizeValueTypeName() +proc exportReturnTypeNim*(sym: NimNode): string = + ## Returns type for internal ABI return values. Returned strings cross the + ## ABI as owned GennyBuffer handles instead of borrowed cstring pointers. + let typ = sym.stripSink + if typ.repr == "string": + return "GennyBuffer" + else: + return exportTypeNim(typ) + proc exportArgTypeNim(sym: NimNode): string = ## Returns the friendly type for Nim wrapper parameters. Unlike return values, ## string parameters stay as Nim strings and are converted at the C boundary. @@ -87,6 +96,14 @@ proc exportTypeCImport*(sym: NimNode): string = else: return typ.normalizeValueTypeName() +proc exportReturnTypeCImport*(sym: NimNode): string = + ## Returns type for C import return declarations in the generated Nim wrapper. + let typ = sym.stripSink + if typ.repr == "string": + return "pointer" + else: + return exportTypeCImport(typ) + proc convertExportFromNim*(sym: NimNode): string = ## Converts Nim value to C value (used by DLL side). let typ = sym.stripSink @@ -114,6 +131,15 @@ proc convertExportExprNim*(expr: string, sym: NimNode): string = else: return expr & convertExportFromNim(typ) +proc convertExportReturnExprNim*(expr: string, sym: NimNode): string = + ## Converts a Nim expression to the ABI-facing return value used by generated + ## internal exports. + let typ = sym.stripSink + if typ.repr == "string": + return &"newGennyBuffer({expr})" + else: + return convertExportExprNim(expr, typ) + proc convertToPointer*(sym: NimNode): string = ## Converts client-side wrapper to pointer for C call. let typ = sym.stripSink @@ -157,6 +183,19 @@ proc convertImportExprNim*(expr: string, sym: NimNode): string = else: return expr & convertImportToNim(typ) +proc convertImportReturnExprNim*(expr: string, sym: NimNode): string = + ## Converts an ABI-facing return expression to the friendly Nim wrapper type. + let typ = sym.stripSink + case typ.repr + of "string": + return &"gennyBufferToString({expr})" + of "Vec2": + return &"cast[Vector2]({expr})" + of "Mat3": + return &"cast[Matrix3]({expr})" + else: + return convertImportExprNim(expr, typ) + proc exportDefaultValueNim(default, sym: NimNode): string = ## Keeps wrapper defaults in the same ABI-facing type family as the generated ## wrapper signature. Some Pixie APIs use vmath defaults like vec2()/mat3() @@ -211,6 +250,8 @@ proc exportProcNim*( # Check if return type is a ref object let returnsRefObject = procReturn.kind != nnkEmpty and (procReturn.isRefObjectLike or procReturn.isSeqLike) + let returnsString = procReturn.kind != nnkEmpty and + procReturn.stripSink.repr == "string" # C import declaration procs.add &"proc {apiProcName}(" @@ -223,7 +264,7 @@ proc exportProcNim*( procs.removeSuffix ", " procs.add ")" if procReturn.kind != nnkEmpty: - procs.add &": {exportTypeCImport(procReturn)}" + procs.add &": {exportReturnTypeCImport(procReturn)}" procs.add " {.importc: \"" procs.add &"{apiProcName}" procs.add "\", cdecl.}" @@ -248,27 +289,30 @@ proc exportProcNim*( if procReturn.kind != nnkEmpty: procs.add &": {exportArgTypeNim(procReturn)}" procs.add " {.inline.} =\n" - if returnsRefObject: - # Wrap returned pointer in ref object - procs.add &" result = {exportArgTypeNim(procReturn)}(reference: " - elif procReturn.kind != nnkEmpty: - procs.add " result = " - else: - procs.add " " - procs.add &"{apiProcName}(" + var callExpr = &"{apiProcName}(" for param in procParams: for i in 0 .. param.len - 3: - procs.add &"{param[i].repr}{convertToPointer(param[^2])}, " - procs.removeSuffix ", " - procs.add ")" + callExpr.add &"{param[i].repr}{convertToPointer(param[^2])}, " + callExpr.removeSuffix ", " + callExpr.add ")" + if returnsRefObject: - procs.add ")" # Close the TypeName(reference: ...) + procs.add &" result = {exportArgTypeNim(procReturn)}(reference: {callExpr})\n" + elif returnsString: + procs.add &" let gennyBuffer = {callExpr}\n" elif procReturn.kind != nnkEmpty: - procs.add convertImportToNim(procReturn) - procs.add "\n" + procs.add &" result = {convertImportReturnExprNim(callExpr, procReturn)}\n" + else: + procs.add &" {callExpr}\n" + if procRaises: procs.add " if checkError():\n" + if returnsString: + procs.add " if gennyBuffer != nil:\n" + procs.add " $lib_genny_buffer_unref(gennyBuffer)\n" procs.add " raise newException($LibError, $takeError())\n" + if returnsString: + procs.add " result = gennyBufferToString(gennyBuffer)\n" procs.add "\n" proc exportObjectNim*(sym: NimNode, constructor: NimNode) = @@ -351,7 +395,7 @@ proc genSeqProcs( procs.add &"proc {procPrefix}_get" procs.add &"(s: pointer, i: int)" - procs.add &": {exportTypeCImport(entryType)}" + procs.add &": {exportReturnTypeCImport(entryType)}" procs.add " {.importc: \"" procs.add &"{procPrefix}_get" procs.add "\", cdecl.}\n" @@ -362,9 +406,8 @@ proc genSeqProcs( procs.add &" {exportTypeNim(entryType)}(reference: " procs.add &"{procPrefix}_get(s{refSuffix}, i))\n" else: - procs.add &" {procPrefix}_get(s{refSuffix}, i)" - procs.add convertImportToNim(entryType) - procs.add "\n" + let getCall = &"{procPrefix}_get(s{refSuffix}, i)" + procs.add &" {convertImportReturnExprNim(getCall, entryType)}\n" procs.add "\n" procs.add &"proc {procPrefix}_set(s: pointer, " @@ -422,7 +465,7 @@ proc exportRefObjectNim*( # C import takes pointer procs.add &"proc {getProcName}(" procs.add &"{toVarCase(objName)}: pointer): " - procs.add exportTypeCImport(fieldType) + procs.add exportReturnTypeCImport(fieldType) procs.add " {.importc: \"" procs.add &"{getProcName}" procs.add "\", cdecl.}" @@ -438,8 +481,8 @@ proc exportRefObjectNim*( procs.add &" {exportTypeNim(fieldType)}(reference: " procs.add &"{getProcName}({toVarCase(objName)}.reference))" else: - procs.add &" {getProcName}({toVarCase(objName)}.reference)" - procs.add convertImportToNim(fieldType) + let getCall = &"{getProcName}({toVarCase(objName)}.reference)" + procs.add &" {convertImportReturnExprNim(getCall, fieldType)}" procs.add "\n" procs.add "\n" @@ -528,6 +571,23 @@ else: {.push dynlib: libName.} +proc $lib_genny_buffer_data(buffer: pointer): cstring {.importc: "$lib_genny_buffer_data", cdecl.} +proc $lib_genny_buffer_len(buffer: pointer): int {.importc: "$lib_genny_buffer_len", cdecl.} +proc $lib_genny_buffer_unref(buffer: pointer) {.importc: "$lib_genny_buffer_unref", cdecl.} + +proc gennyBufferToString(buffer: pointer): string = + if buffer == nil: + return "" + try: + let len = $lib_genny_buffer_len(buffer) + if len > 0: + let data = $lib_genny_buffer_data(buffer) + if data != nil: + result = newString(len) + copyMem(result[0].addr, data, len) + finally: + $lib_genny_buffer_unref(buffer) + type $LibError = object of ValueError """ diff --git a/src/genny/languages/node.nim b/src/genny/languages/node.nim index 4715156..03f1e89 100644 --- a/src/genny/languages/node.nim +++ b/src/genny/languages/node.nim @@ -19,6 +19,9 @@ proc isSeqLike(sym: NimNode): bool = let typ = sym.stripSink typ.kind == nnkBracketExpr and typ[0].repr in ["seq", "openArray"] +proc isStringType(sym: NimNode): bool = + sym.stripSink.repr == "string" + proc typeBody(sym: NimNode): NimNode = let typ = sym.stripSink if typ.kind == nnkSym: @@ -82,6 +85,12 @@ proc exportTypeNode(sym: NimNode): string = else: "'uint64'" # Treat ref objects as opaque pointers +proc exportReturnTypeNode(sym: NimNode): string = + if sym.isStringType: + "'uint64'" + else: + exportTypeNode(sym) + proc jsArgValue(argType: NimNode, argName: string): string = let typ = argType.stripSink if typ.repr == "Rune": @@ -95,6 +104,8 @@ proc jsReturnValue(returnType: NimNode, call: string): string = let typ = returnType.stripSink if typ.kind == nnkEmpty: call + elif typ.repr == "string": + &"gennyBufferToString({call})" elif typ.repr == "Rune": &"intToRune({call})" elif typ.isSeqLike: @@ -174,7 +185,7 @@ proc exportProcNode*( defaults.add((entry.repr, default)) # Declare the C function - declareFunc(apiProcName, procParams, exportTypeNode(procReturn)) + declareFunc(apiProcName, procParams, exportReturnTypeNode(procReturn)) # Create wrapper function # Only generate prototype methods for ref objects, not value objects @@ -284,7 +295,7 @@ proc genSeqProcs(objName, className, procPrefix, selfAccessor: string, entryType types.add "};\n" # get - procs.add &"const {procPrefix}_get = lib.func('{procPrefix}_get', {exportTypeNode(entryType)}, ['uint64', 'int64']);\n" + procs.add &"const {procPrefix}_get = lib.func('{procPrefix}_get', {exportReturnTypeNode(entryType)}, ['uint64', 'int64']);\n" let getCall = &"{procPrefix}_get({selfAccessor}, index)" types.add &"{className}.prototype.get = function(index) {{\n" types.add &" return {jsReturnValue(entryType, getCall)};\n" @@ -360,7 +371,7 @@ proc exportRefObjectNode*( let setProcName = &"$lib_{objNameSnaked}_set_{fieldNameSnaked}" # Declare getter/setter C functions - procs.add &"const {getProcName} = lib.func('{getProcName}', {exportTypeNode(fieldType)}, ['uint64']);\n" + procs.add &"const {getProcName} = lib.func('{getProcName}', {exportReturnTypeNode(fieldType)}, ['uint64']);\n" procs.add &"const {setProcName} = lib.func('{setProcName}', 'void', ['uint64', {exportTypeNode(fieldType)}]);\n" types.add &"Object.defineProperty({objName}.prototype, '{fieldName}', {{\n" @@ -437,6 +448,27 @@ if (process.platform === 'win32') { const lib = koffi.load(path.join(__dirname, libName)); +const $lib_genny_buffer_data = lib.func('$lib_genny_buffer_data', 'void *', ['uint64']); +const $lib_genny_buffer_len = lib.func('$lib_genny_buffer_len', 'int64', ['uint64']); +const $lib_genny_buffer_unref = lib.func('$lib_genny_buffer_unref', 'void', ['uint64']); + +function gennyBufferToString(buffer) { + if (buffer === null || buffer === 0 || buffer === 0n) { + return ''; + } + try { + const length = Number($lib_genny_buffer_len(buffer)); + const data = $lib_genny_buffer_data(buffer); + if (data === null || data === 0 || data === 0n || length <= 0) { + return ''; + } + const bytes = koffi.decode(data, 'uint8_t', length); + return Buffer.from(bytes).toString('utf8'); + } finally { + $lib_genny_buffer_unref(buffer); + } +} + class $LibException extends Error { constructor(message) { super(message); diff --git a/src/genny/languages/python.nim b/src/genny/languages/python.nim index 5d94baa..6eb5ff9 100644 --- a/src/genny/languages/python.nim +++ b/src/genny/languages/python.nim @@ -24,6 +24,9 @@ proc isSeqLike(sym: NimNode): bool = let typ = sym.stripSink typ.kind == nnkBracketExpr and typ[0].repr in ["seq", "openArray"] +proc isStringType(sym: NimNode): bool = + sym.stripSink.repr == "string" + proc exportTypePy(sym: NimNode): string = let typ = sym.stripSink if typ.kind == nnkBracketExpr: @@ -64,6 +67,12 @@ proc exportTypePy(sym: NimNode): string = else: typ.repr +proc exportReturnTypePy(sym: NimNode): string = + if sym.isStringType: + "_GennyBuffer" + else: + exportTypePy(sym) + proc convertExportFromPy*(sym: NimNode): string = let typ = sym.stripSink if typ.repr == "string": @@ -88,7 +97,7 @@ proc importExprPy(expr: string, sym: NimNode): string = let typ = sym.stripSink case typ.repr of "string": - expr & ".decode(\"utf8\")" + &"_genny_buffer_to_string({expr})" of "Rune": &"_int_to_rune({expr})" else: @@ -246,7 +255,7 @@ proc exportProcPy*( var dllParams: seq[NimNode] for param in procParams: dllParams.add(param[1]) - dllProc(&"dll.$lib_{apiProcName}", toArgTypes(dllParams), exportTypePy(procReturn)) + dllProc(&"dll.$lib_{apiProcName}", toArgTypes(dllParams), exportReturnTypePy(procReturn)) proc exportObjectPy*(sym: NimNode, constructor: NimNode) = let objName = sym.repr @@ -372,7 +381,7 @@ proc genSeqProcs(objName, procPrefix, selfSuffix: string, entryType: NimNode) = types.add "\n" dllProc(&"dll.{procPrefix}_len", [objName], "c_longlong") - dllProc(&"dll.{procPrefix}_get", [objName, "c_longlong"], exportTypePy(entryType)) + dllProc(&"dll.{procPrefix}_get", [objName, "c_longlong"], exportReturnTypePy(entryType)) dllProc(&"dll.{procPrefix}_set", [objName, "c_longlong", exportTypePy(entryType)], "None") dllProc(&"dll.{procPrefix}_delete", [objName, "c_longlong"], "None") dllProc(&"dll.{procPrefix}_add", [objName, exportTypePy(entryType)], "None") @@ -446,7 +455,7 @@ proc exportRefObjectPy*( types.add ")\n" types.add "\n" - dllProc(getProcName, toArgTypes([sym]), exportTypePy(fieldType)) + dllProc(getProcName, toArgTypes([sym]), exportReturnTypePy(fieldType)) dllProc(setProcName, toArgTypes([sym, fieldType]), exportTypePy(nil)) else: var helperName = fieldName @@ -506,6 +515,27 @@ else: libName = "lib$lib.so" dll = cdll.LoadLibrary(os.path.join(dir, libName)) +_GennyBuffer = c_void_p + +dll.$lib_genny_buffer_data.argtypes = [_GennyBuffer] +dll.$lib_genny_buffer_data.restype = c_void_p +dll.$lib_genny_buffer_len.argtypes = [_GennyBuffer] +dll.$lib_genny_buffer_len.restype = c_longlong +dll.$lib_genny_buffer_unref.argtypes = [_GennyBuffer] +dll.$lib_genny_buffer_unref.restype = None + +def _genny_buffer_to_string(buffer): + if not buffer: + return "" + try: + length = dll.$lib_genny_buffer_len(buffer) + data = dll.$lib_genny_buffer_data(buffer) + if not data or length <= 0: + return "" + return string_at(data, length).decode("utf8") + finally: + dll.$lib_genny_buffer_unref(buffer) + class $LibError(Exception): pass diff --git a/src/genny/languages/python_native.nim b/src/genny/languages/python_native.nim index 5868658..c4ed601 100644 --- a/src/genny/languages/python_native.nim +++ b/src/genny/languages/python_native.nim @@ -81,6 +81,7 @@ proc isArrayType(sym: NimNode): bool = proc isRefLikeObjectType(sym: NimNode): bool proc cBaseType(sym: NimNode): string +proc cType(sym: NimNode): string proc cArraySuffix(sym: NimNode): string = let typ = sym.stripSink @@ -117,6 +118,13 @@ proc cBaseType(sym: NimNode): string = of "", "nil", "None": "void" else: name +proc cReturnType(sym: NimNode): string = + let typ = sym.stripSink + if typ.nativeName() == "string": + "void *" + else: + cType(typ) + proc cType(sym: NimNode): string = if sym.isArrayType: error("array types need a declarator name", sym) @@ -217,7 +225,9 @@ proc pyFromC(expr: string, typ: NimNode): string = return &"GennyPy_{cIdent(seqName)}_FromRef((void *)({expr}))" case baseTyp.nativeName() - of "string", "cstring": + of "string": + &"genny_buffer_to_py((void *)({expr}))" + of "cstring": &"PyUnicode_FromString(({expr}) ? ({expr}) : \"\")" of "bool": &"PyBool_FromLong((long)({expr}))" @@ -268,7 +278,7 @@ proc convertPyToCInt(pyExpr, outExpr: string, typ: NimNode, label: string): stri convertPyToC(pyExpr, outExpr, typ, label).replace("return NULL", "return -1") proc addProto(procName: string, params: seq[(string, NimNode)], ret: NimNode) = - cProtos.add "extern " & cType(ret) & " " & procName & "(" + cProtos.add "extern " & cReturnType(ret) & " " & procName & "(" for (name, typ) in params: cProtos.add cDecl(typ, name) & ", " cProtos.removeSuffix(", ") @@ -281,8 +291,7 @@ proc addErrorCheck(procRaises: bool): string = if procRaises: needsErrorBridge = true result.add " if (pixie_native_check_error != NULL && pixie_native_check_error()) {\n" - result.add " char *genny_error = pixie_native_take_error();\n" - result.add " PyErr_SetString(GennyPy_ModuleError, genny_error ? genny_error : \"Nim exception\");\n" + result.add " genny_set_error_from_buffer(pixie_native_take_error());\n" result.add " return NULL;\n" result.add " }\n" @@ -290,8 +299,7 @@ proc addErrorCheckInt(procRaises: bool): string = if procRaises: needsErrorBridge = true result.add " if (pixie_native_check_error != NULL && pixie_native_check_error()) {\n" - result.add " char *genny_error = pixie_native_take_error();\n" - result.add " PyErr_SetString(GennyPy_ModuleError, genny_error ? genny_error : \"Nim exception\");\n" + result.add " genny_set_error_from_buffer(pixie_native_take_error());\n" result.add " return -1;\n" result.add " }\n" @@ -454,7 +462,7 @@ proc exportProcPyNative*( cWrappers.add addErrorCheck(procRaises) cWrappers.add " Py_RETURN_NONE;\n" else: - cWrappers.add &" {cType(procReturn)} genny_result = {apiName}({callArgs});\n" + cWrappers.add &" {cReturnType(procReturn)} genny_result = {apiName}({callArgs});\n" cWrappers.add addErrorCheck(procRaises) cWrappers.add &" return {pyFromC(\"genny_result\", procReturn)};\n" cWrappers.add "}\n\n" @@ -763,7 +771,7 @@ proc emitSeqMethods(objName, procPrefix, refExpr, typePrefix: string, entryType: cTypeBlocks.add &" Py_ssize_t len = {typePrefix}_len(self);\n" cTypeBlocks.add " if (index < 0) index += len;\n" cTypeBlocks.add " if (index < 0 || index >= len) { PyErr_SetString(PyExc_IndexError, \"index out of range\"); return NULL; }\n" - cTypeBlocks.add &" {cType(entryType)} value = {procPrefix}_get({refExpr}, (long long)index);\n" + cTypeBlocks.add &" {cReturnType(entryType)} value = {procPrefix}_get({refExpr}, (long long)index);\n" cTypeBlocks.add &" return {pyFromC(\"value\", entryType)};\n" cTypeBlocks.add "}\n\n" cTypeBlocks.add &"static int {typePrefix}_ass_item(PyObject *self, Py_ssize_t index, PyObject *value) {{\n" @@ -859,7 +867,7 @@ proc exportRefObjectPyNative*( getsetForwardDecls.add &"static PyObject *{getterName}({pyStruct} *self, void *closure);\n" getsetForwardDecls.add &"static int {setterName}({pyStruct} *self, PyObject *value, void *closure);\n" cTypeBlocks.add &"static PyObject *{getterName}({pyStruct} *self, void *closure) {{\n" - cTypeBlocks.add &" {cType(fieldType)} value = {getterApi}(self->ref);\n" + cTypeBlocks.add &" {cReturnType(fieldType)} value = {getterApi}(self->ref);\n" cTypeBlocks.add &" return {pyFromC(\"value\", fieldType)};\n" cTypeBlocks.add "}\n\n" cTypeBlocks.add &"static int {setterName}({pyStruct} *self, PyObject *value, void *closure) {{\n" @@ -997,11 +1005,33 @@ proc writePyNative*(dir, lib: string): NimNode = code.add " }\n" code.add " return 1;\n" code.add "}\n\n" + code.add "extern char *$lib_genny_buffer_data(void *buffer);\n" + code.add "extern long long $lib_genny_buffer_len(void *buffer);\n" + code.add "extern void $lib_genny_buffer_unref(void *buffer);\n\n" + code.add "static PyObject *genny_buffer_to_py(void *buffer) {\n" + code.add " if (buffer == NULL) return PyUnicode_FromString(\"\");\n" + code.add " PyObject *result = NULL;\n" + code.add " long long len = $lib_genny_buffer_len(buffer);\n" + code.add " char *data = $lib_genny_buffer_data(buffer);\n" + code.add " if (data == NULL || len <= 0) result = PyUnicode_FromString(\"\");\n" + code.add " else result = PyUnicode_FromStringAndSize(data, (Py_ssize_t)len);\n" + code.add " $lib_genny_buffer_unref(buffer);\n" + code.add " return result;\n" + code.add "}\n\n" + code.add "static void genny_set_error_from_buffer(void *buffer) {\n" + code.add " PyObject *message = genny_buffer_to_py(buffer);\n" + code.add " if (message != NULL) {\n" + code.add " PyErr_SetObject(GennyPy_ModuleError, message);\n" + code.add " Py_DECREF(message);\n" + code.add " } else {\n" + code.add " PyErr_SetString(GennyPy_ModuleError, \"Nim exception\");\n" + code.add " }\n" + code.add "}\n\n" if needsErrorBridge: code.add "extern char $lib_check_error(void);\n" - code.add "extern char *$lib_take_error(void);\n" + code.add "extern void *$lib_take_error(void);\n" code.add "static char (*pixie_native_check_error)(void) = $lib_check_error;\n" - code.add "static char *(*pixie_native_take_error)(void) = $lib_take_error;\n\n" + code.add "static void *(*pixie_native_take_error)(void) = $lib_take_error;\n\n" code.add cTypes code.add "\n" code.add cForwardDecls diff --git a/src/genny/languages/zig.nim b/src/genny/languages/zig.nim index 63ce90f..038b778 100644 --- a/src/genny/languages/zig.nim +++ b/src/genny/languages/zig.nim @@ -15,6 +15,9 @@ proc isSeqLike(sym: NimNode): bool = let typ = sym.stripSink typ.kind == nnkBracketExpr and typ[0].repr in ["seq", "openArray"] +proc isStringType(sym: NimNode): bool = + sym.stripSink.repr == "string" + proc exportTypeZig(sym: NimNode): string = let typ = sym.stripSink if typ.kind == nnkBracketExpr: @@ -56,6 +59,12 @@ proc exportTypeZig(sym: NimNode): string = else: typ.repr +proc exportReturnTypeZig(sym: NimNode): string = + if sym.isStringType: + "[:0]u8" + else: + exportTypeZig(sym) + proc convertExportFromZig*(inner: string, sym: string): string = if sym == "[:0]const u8": inner & ".ptr" @@ -73,7 +82,9 @@ proc convertImportToZig*(inner: string, sym: string): string = inner proc exportTypeZigAbi(sym: string): string = - if sym == "[:0]const u8": + if sym == "[:0]u8": + "?*GennyBuffer" + elif sym == "[:0]const u8": "[*:0]const u8" elif sym == "u21": "i32" @@ -108,6 +119,7 @@ proc exportProc( ) = let onClass = owner notin ["void", ""] + returnsString = procReturn == "[:0]u8" baseIndent = if onClass or indent: " " @@ -146,11 +158,20 @@ proc exportProc( code.add ": " code.add param[1] code.add &", " + if returnsString: + code.add "allocator: std.mem.Allocator, " code.removeSuffix ", " code.add ") " - if procRaises: + if returnsString: + if procRaises: + code.add "(Error || std.mem.Allocator.Error)![:0]u8" + else: + code.add "std.mem.Allocator.Error![:0]u8" + elif procRaises: code.add "Error!" - if procReturn != "": + if returnsString: + discard + elif procReturn != "": code.add procReturn; else: code.add "void" @@ -172,7 +193,17 @@ proc exportProc( call.removeSuffix ", " call.add &")" - if procRaises: + if returnsString: + code.add &"{baseIndent} const result = {call};\n" + if procRaises: + code.add &"{baseIndent} if (checkError()) " & "{\n" + code.add &"{baseIndent} if (result) |buffer| buffer.deinit();\n" + code.add &"{baseIndent} return error.$LibError;\n" + code.add &"{baseIndent} " & "}\n" + code.add &"{baseIndent} const buffer = result.?;\n" + code.add &"{baseIndent} defer buffer.deinit();\n" + code.add &"{baseIndent} return buffer.toOwnedSlice(allocator);\n" + elif procRaises: let hasReturn = procReturn notin ["", "void"] if hasReturn: code.add &"{baseIndent} const result = {call};\n" @@ -201,7 +232,7 @@ proc exportProcZig*( procNameSnaked = toSnakeCase(procName) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1].toArgSeq() - procReturn = procType[0][0].exportTypeZig() + procReturn = procType[0][0].exportReturnTypeZig() procRaises = sym.raises() comments = if sym.getImpl()[6][0].kind == nnkCommentStmt: @@ -306,7 +337,7 @@ proc genSeqProcs(objName, procPrefix, selfSuffix: string, entryType: NimNode) = &"get{selfSuffix}", &"{procPrefix}_get", @[("self", objName), ("index", "isize")], - entryType.exportTypeZig(), + entryType.exportReturnTypeZig(), indent = true ) exportProc( @@ -360,7 +391,7 @@ proc exportRefObjectZig*( "get" & fieldNameCapped, &"$lib_{objNameSnaked}_get_{fieldNameSnaked}", @[("self", &"*{objName}")], - fieldType.exportTypeZig(), + fieldType.exportReturnTypeZig(), indent = true ) exportProc( @@ -406,6 +437,26 @@ pub const Error = error{ $LibError, }; +pub const GennyBuffer = opaque { + extern fn $lib_genny_buffer_unref(self: *GennyBuffer) void; + extern fn $lib_genny_buffer_data(self: *GennyBuffer) [*:0]const u8; + extern fn $lib_genny_buffer_len(self: *GennyBuffer) isize; + + pub inline fn deinit(self: *GennyBuffer) void { + return $lib_genny_buffer_unref(self); + } + + pub inline fn toOwnedSlice(self: *GennyBuffer, allocator: std.mem.Allocator) std.mem.Allocator.Error![:0]u8 { + const len: usize = @intCast($lib_genny_buffer_len(self)); + const output = try allocator.allocSentinel(u8, len, 0); + if (len > 0) { + const data = $lib_genny_buffer_data(self); + std.mem.copyForwards(u8, output, data[0..len]); + } + return output; + } +}; + """ proc writeZig*(dir, lib: string) = diff --git a/tests/generated/internal.nim b/tests/generated/internal.nim index 27f62c8..a70eab5 100644 --- a/tests/generated/internal.nim +++ b/tests/generated/internal.nim @@ -3,6 +3,27 @@ when not defined(gcArc) and not defined(gcOrc): when (NimMajor, NimMinor, NimPatch) == (1, 6, 2): {.error: "Nim 1.6.2 not supported with Genny due to FFI issues.".} + +type GennyBuffer* = ref object + data: string + +proc newGennyBuffer*(data: string): GennyBuffer = + result = GennyBuffer(data: data) + GC_ref(result) + +proc test_genny_buffer_data*(buffer: GennyBuffer): cstring {.raises: [], cdecl, exportc, dynlib.} = + if buffer == nil: + return nil + buffer.data.cstring + +proc test_genny_buffer_len*(buffer: GennyBuffer): int {.raises: [], cdecl, exportc, dynlib.} = + if buffer == nil: + return 0 + buffer.data.len + +proc test_genny_buffer_unref*(buffer: GennyBuffer) {.raises: [], cdecl, exportc, dynlib.} = + if buffer != nil: + GC_unref(buffer) proc test_simple_call*(a: int): int {.raises: [], cdecl, exportc, dynlib.} = simpleCall(a) @@ -115,8 +136,8 @@ proc test_seq_string_len*(s: SeqString): int {.raises: [], cdecl, exportc, dynli proc test_seq_string_add*(s: SeqString, v: cstring) {.raises: [], cdecl, exportc, dynlib.} = s.s.add(v.`$`) -proc test_seq_string_get*(s: SeqString, i: int): cstring {.raises: [], cdecl, exportc, dynlib.} = - s.s[i].cstring +proc test_seq_string_get*(s: SeqString, i: int): GennyBuffer {.raises: [], cdecl, exportc, dynlib.} = + newGennyBuffer(s.s[i]) proc test_seq_string_set*(s: SeqString, i: int, v: cstring) {.raises: [], cdecl, exportc, dynlib.} = s.s[i] = v.`$` @@ -135,3 +156,6 @@ proc test_get_datas*(): SeqString {.raises: [], cdecl, exportc, dynlib.} = if result != nil: GC_ref(result) +proc test_get_message*(): GennyBuffer {.raises: [], cdecl, exportc, dynlib.} = + newGennyBuffer(getMessage()) + diff --git a/tests/generated/test.h b/tests/generated/test.h index d6e3909..32244a0 100644 --- a/tests/generated/test.h +++ b/tests/generated/test.h @@ -3,6 +3,8 @@ #include +typedef struct GennyBufferHandle* GennyBuffer; + #define SIMPLE_CONST 123 typedef char SimpleEnum; @@ -34,6 +36,9 @@ typedef struct SeqStringHandle* SeqString; extern "C" { #endif +const char* test_genny_buffer_data(GennyBuffer buffer); +intptr_t test_genny_buffer_len(GennyBuffer buffer); +void test_genny_buffer_unref(GennyBuffer buffer); /** * Returns the integer passed in. */ @@ -104,7 +109,7 @@ SeqString test_new_seq_string(); intptr_t test_seq_string_len(SeqString seq_string); -const char* test_seq_string_get(SeqString seq_string, intptr_t index); +GennyBuffer test_seq_string_get(SeqString seq_string, intptr_t index); void test_seq_string_set(SeqString seq_string, intptr_t index, const char* value); @@ -116,6 +121,8 @@ void test_seq_string_clear(SeqString seq_string); SeqString test_get_datas(); +GennyBuffer test_get_message(); + #ifdef __cplusplus } #endif diff --git a/tests/generated/test.hpp b/tests/generated/test.hpp index 31a9281..bdbc877 100644 --- a/tests/generated/test.hpp +++ b/tests/generated/test.hpp @@ -1,7 +1,9 @@ #ifndef INCLUDE_TEST_H #define INCLUDE_TEST_H +#include #include +#include static constexpr auto SIMPLE_CONST = 123; @@ -22,6 +24,20 @@ struct SimpleObjWithProc; struct SeqString; +struct GennyBuffer { + + private: + + std::uintptr_t reference; + + public: + + const char* data(); + std::intptr_t len(); + void free(); + +}; + struct SimpleObj { std::intptr_t simple_a; std::uint8_t simple_b; @@ -117,8 +133,8 @@ struct SeqString { void free(); std::intptr_t size(); - const char* get(std::intptr_t index); - const char* operator[](std::intptr_t index); + std::string get(std::intptr_t index); + std::string operator[](std::intptr_t index); void set(std::intptr_t index, const char* value); void removeAt(std::intptr_t index); void add(const char* value); @@ -128,6 +144,10 @@ struct SeqString { extern "C" { +const char* test_genny_buffer_data(GennyBuffer buffer); +std::intptr_t test_genny_buffer_len(GennyBuffer buffer); +void test_genny_buffer_unref(GennyBuffer buffer); + std::intptr_t test_simple_call(std::intptr_t a); SimpleObj test_simple_obj(std::intptr_t simple_a, std::uint8_t simple_b, bool simple_c); @@ -192,7 +212,7 @@ SeqString test_new_seq_string(); std::intptr_t test_seq_string_len(SeqString seq_string); -const char* test_seq_string_get(SeqString seq_string, std::intptr_t index); +GennyBuffer test_seq_string_get(SeqString seq_string, std::intptr_t index); void test_seq_string_set(SeqString seq_string, std::intptr_t index, const char* value); @@ -204,6 +224,31 @@ void test_seq_string_clear(SeqString seq_string); SeqString test_get_datas(); +GennyBuffer test_get_message(); + +} + +static inline std::string gennyBufferToString(GennyBuffer buffer) { + const char* data = test_genny_buffer_data(buffer); + std::intptr_t len = test_genny_buffer_len(buffer); + std::string result; + if (data != nullptr && len > 0) { + result.assign(data, static_cast(len)); + } + test_genny_buffer_unref(buffer); + return result; +} + +const char* GennyBuffer::data() { + return test_genny_buffer_data(*this); +} + +std::intptr_t GennyBuffer::len() { + return test_genny_buffer_len(*this); +} + +void GennyBuffer::free() { + test_genny_buffer_unref(*this); } std::intptr_t simpleCall(std::intptr_t a) { @@ -326,11 +371,11 @@ std::intptr_t SeqString::size(){ return test_seq_string_len(*this); } -const char* SeqString::get(std::intptr_t index){ - return test_seq_string_get(*this, index); +std::string SeqString::get(std::intptr_t index){ + return gennyBufferToString(test_seq_string_get(*this, index)); } -const char* SeqString::operator[](std::intptr_t index){ +std::string SeqString::operator[](std::intptr_t index){ return get(index); } @@ -358,4 +403,8 @@ SeqString getDatas() { return test_get_datas(); }; +std::string getMessage() { + return gennyBufferToString(test_get_message()); +}; + #endif diff --git a/tests/generated/test.js b/tests/generated/test.js index 2191a12..f534678 100644 --- a/tests/generated/test.js +++ b/tests/generated/test.js @@ -14,6 +14,27 @@ if (process.platform === 'win32') { const lib = koffi.load(path.join(__dirname, libName)); +const test_genny_buffer_data = lib.func('test_genny_buffer_data', 'void *', ['uint64']); +const test_genny_buffer_len = lib.func('test_genny_buffer_len', 'int64', ['uint64']); +const test_genny_buffer_unref = lib.func('test_genny_buffer_unref', 'void', ['uint64']); + +function gennyBufferToString(buffer) { + if (buffer === null || buffer === 0 || buffer === 0n) { + return ''; + } + try { + const length = Number(test_genny_buffer_len(buffer)); + const data = test_genny_buffer_data(buffer); + if (data === null || data === 0 || data === 0n || length <= 0) { + return ''; + } + const bytes = koffi.decode(data, 'uint8_t', length); + return Buffer.from(bytes).toString('utf8'); + } finally { + test_genny_buffer_unref(buffer); + } +} + class testException extends Error { constructor(message) { super(message); @@ -200,7 +221,7 @@ SeqString.prototype.length = function() { return test_seq_string_len(this.ref); }; SeqString.prototype.get = function(index) { - return test_seq_string_get(this.ref, index); + return gennyBufferToString(test_seq_string_get(this.ref, index)); }; SeqString.prototype.set = function(index, value) { test_seq_string_set(this.ref, index, value); @@ -218,6 +239,10 @@ function getDatas() { return new SeqString(test_get_datas()); } +function getMessage() { + return gennyBufferToString(test_get_message()); +} + const test_simple_call = lib.func('test_simple_call', 'int64', ['int64']); const test_simple_ref_obj_unref = lib.func('test_simple_ref_obj_unref', 'void', ['uint64']); @@ -247,12 +272,13 @@ const test_simple_obj_with_proc_extra_proc = lib.func('test_simple_obj_with_proc const test_seq_string_unref = lib.func('test_seq_string_unref', 'void', ['uint64']); const test_new_seq_string = lib.func('test_new_seq_string', 'uint64', []); const test_seq_string_len = lib.func('test_seq_string_len', 'int64', ['uint64']); -const test_seq_string_get = lib.func('test_seq_string_get', 'str', ['uint64', 'int64']); +const test_seq_string_get = lib.func('test_seq_string_get', 'uint64', ['uint64', 'int64']); const test_seq_string_set = lib.func('test_seq_string_set', 'void', ['uint64', 'int64', 'str']); const test_seq_string_delete = lib.func('test_seq_string_delete', 'void', ['uint64', 'int64']); const test_seq_string_add = lib.func('test_seq_string_add', 'void', ['uint64', 'str']); const test_seq_string_clear = lib.func('test_seq_string_clear', 'void', ['uint64']); const test_get_datas = lib.func('test_get_datas', 'uint64', []); +const test_get_message = lib.func('test_get_message', 'uint64', []); exports.SIMPLE_CONST = 123; exports.SimpleEnum = 'int8'; @@ -274,3 +300,4 @@ exports.simpleObjWithProcExtraProc = simpleObjWithProcExtraProc; exports.SeqString = SeqString; exports.newSeqString = newSeqString; exports.getDatas = getDatas; +exports.getMessage = getMessage; diff --git a/tests/generated/test.nim b/tests/generated/test.nim index f0c4ad0..64ca5d6 100644 --- a/tests/generated/test.nim +++ b/tests/generated/test.nim @@ -11,6 +11,23 @@ else: {.push dynlib: libName.} +proc test_genny_buffer_data(buffer: pointer): cstring {.importc: "test_genny_buffer_data", cdecl.} +proc test_genny_buffer_len(buffer: pointer): int {.importc: "test_genny_buffer_len", cdecl.} +proc test_genny_buffer_unref(buffer: pointer) {.importc: "test_genny_buffer_unref", cdecl.} + +proc gennyBufferToString(buffer: pointer): string = + if buffer == nil: + return "" + try: + let len = test_genny_buffer_len(buffer) + if len > 0: + let data = test_genny_buffer_data(buffer) + if data != nil: + result = newString(len) + copyMem(result[0].addr, data, len) + finally: + test_genny_buffer_unref(buffer) + type testError = object of ValueError const simpleConst* = 123 @@ -210,10 +227,10 @@ proc test_seq_string_add(s: pointer, v: cstring) {.importc: "test_seq_string_add proc add*(s: SeqString, v: string) = test_seq_string_add(s.reference, v.cstring) -proc test_seq_string_get(s: pointer, i: int): cstring {.importc: "test_seq_string_get", cdecl.} +proc test_seq_string_get(s: pointer, i: int): pointer {.importc: "test_seq_string_get", cdecl.} proc `[]`*(s: SeqString, i: int): string = - test_seq_string_get(s.reference, i).`$` + gennyBufferToString(test_seq_string_get(s.reference, i)) proc test_seq_string_set(s: pointer, i: int, v: cstring) {.importc: "test_seq_string_set", cdecl.} @@ -240,3 +257,9 @@ proc test_get_datas(): pointer {.importc: "test_get_datas", cdecl.} proc getDatas*(): SeqString {.inline.} = result = SeqString(reference: test_get_datas()) +proc test_get_message(): pointer {.importc: "test_get_message", cdecl.} + +proc getMessage*(): string {.inline.} = + let gennyBuffer = test_get_message() + result = gennyBufferToString(gennyBuffer) + diff --git a/tests/generated/test.py b/tests/generated/test.py index 51f7881..444158b 100644 --- a/tests/generated/test.py +++ b/tests/generated/test.py @@ -10,6 +10,27 @@ libName = "libtest.so" dll = cdll.LoadLibrary(os.path.join(dir, libName)) +_GennyBuffer = c_void_p + +dll.test_genny_buffer_data.argtypes = [_GennyBuffer] +dll.test_genny_buffer_data.restype = c_void_p +dll.test_genny_buffer_len.argtypes = [_GennyBuffer] +dll.test_genny_buffer_len.restype = c_longlong +dll.test_genny_buffer_unref.argtypes = [_GennyBuffer] +dll.test_genny_buffer_unref.restype = None + +def _genny_buffer_to_string(buffer): + if not buffer: + return "" + try: + length = dll.test_genny_buffer_len(buffer) + data = dll.test_genny_buffer_data(buffer) + if not data or length <= 0: + return "" + return string_at(data, length).decode("utf8") + finally: + dll.test_genny_buffer_unref(buffer) + class testError(Exception): pass @@ -223,7 +244,7 @@ def __len__(self): return dll.test_seq_string_len(self) def __getitem__(self, index): - return dll.test_seq_string_get(self, index).decode("utf8") + return _genny_buffer_to_string(dll.test_seq_string_get(self, index)) def __setitem__(self, index, value): dll.test_seq_string_set(self, index, value.encode("utf8")) @@ -244,6 +265,10 @@ def get_datas(): result = dll.test_get_datas() return result +def get_message(): + result = _genny_buffer_to_string(dll.test_get_message()) + return result + dll.test_simple_call.argtypes = [c_longlong] dll.test_simple_call.restype = c_longlong @@ -329,7 +354,7 @@ def get_datas(): dll.test_seq_string_len.restype = c_longlong dll.test_seq_string_get.argtypes = [SeqString, c_longlong] -dll.test_seq_string_get.restype = c_char_p +dll.test_seq_string_get.restype = _GennyBuffer dll.test_seq_string_set.argtypes = [SeqString, c_longlong, c_char_p] dll.test_seq_string_set.restype = None @@ -346,3 +371,6 @@ def get_datas(): dll.test_get_datas.argtypes = [] dll.test_get_datas.restype = SeqString +dll.test_get_message.argtypes = [] +dll.test_get_message.restype = _GennyBuffer + diff --git a/tests/generated/test.zig b/tests/generated/test.zig index 8a90dd6..7c108b5 100644 --- a/tests/generated/test.zig +++ b/tests/generated/test.zig @@ -4,6 +4,26 @@ pub const Error = error{ testError, }; +pub const GennyBuffer = opaque { + extern fn test_genny_buffer_unref(self: *GennyBuffer) void; + extern fn test_genny_buffer_data(self: *GennyBuffer) [*:0]const u8; + extern fn test_genny_buffer_len(self: *GennyBuffer) isize; + + pub inline fn deinit(self: *GennyBuffer) void { + return test_genny_buffer_unref(self); + } + + pub inline fn toOwnedSlice(self: *GennyBuffer, allocator: std.mem.Allocator) std.mem.Allocator.Error![:0]u8 { + const len: usize = @intCast(test_genny_buffer_len(self)); + const output = try allocator.allocSentinel(u8, len, 0); + if (len > 0) { + const data = test_genny_buffer_data(self); + std.mem.copyForwards(u8, output, data[0..len]); + } + return output; + } +}; + pub const simple_const = 123; pub const SimpleEnum = enum(u8) { @@ -201,9 +221,12 @@ pub const SeqString = opaque { return test_seq_string_len(self); } - extern fn test_seq_string_get(self: *SeqString, index: isize) [*:0]const u8; - pub inline fn get(self: *SeqString, index: isize) [:0]const u8 { - return std.mem.span(test_seq_string_get(self, index)); + extern fn test_seq_string_get(self: *SeqString, index: isize) ?*GennyBuffer; + pub inline fn get(self: *SeqString, index: isize, allocator: std.mem.Allocator) std.mem.Allocator.Error![:0]u8 { + const result = test_seq_string_get(self, index); + const buffer = result.?; + defer buffer.deinit(); + return buffer.toOwnedSlice(allocator); } extern fn test_seq_string_set(self: *SeqString, index: isize, value: [*:0]const u8) void; @@ -232,3 +255,11 @@ pub inline fn getDatas() *SeqString { return test_get_datas(); } +extern fn test_get_message() ?*GennyBuffer; +pub inline fn getMessage(allocator: std.mem.Allocator) std.mem.Allocator.Error![:0]u8 { + const result = test_get_message(); + const buffer = result.?; + defer buffer.deinit(); + return buffer.toOwnedSlice(allocator); +} + diff --git a/tests/test.nim b/tests/test.nim index 84c7ca5..ba8e3cb 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -107,11 +107,15 @@ exportObject SimpleObjWithProc: proc getDatas(): seq[string] = @["a", "b", "c"] +proc getMessage(): string = + "alpha\0omega" + exportSeq seq[string]: discard exportProcs: getDatas + getMessage writeFiles("tests/generated", "test") diff --git a/tests/test_c.c b/tests/test_c.c index 87e7415..6873082 100644 --- a/tests/test_c.c +++ b/tests/test_c.c @@ -1,8 +1,17 @@ /* Test C bindings. */ #include #include +#include +#include #include "test.h" +static void assert_buffer_equals(GennyBuffer buffer, const char *expected, intptr_t expected_len) { + assert(buffer != NULL); + assert(test_genny_buffer_len(buffer) == expected_len); + assert(memcmp(test_genny_buffer_data(buffer), expected, (size_t)expected_len) == 0); + test_genny_buffer_unref(buffer); +} + int main() { printf("Testing C bindings\n"); @@ -45,9 +54,15 @@ int main() { assert(test_seq_int_len(seq_int) == 0); test_seq_int_unref(seq_int); + printf("Testing test_get_message\n"); + assert_buffer_equals(test_get_message(), "alpha\0omega", 11); + printf("Testing test_get_datas\n"); SeqString datas = test_get_datas(); assert(test_seq_string_len(datas) == 3); + assert_buffer_equals(test_seq_string_get(datas, 0), "a", 1); + assert_buffer_equals(test_seq_string_get(datas, 1), "b", 1); + assert_buffer_equals(test_seq_string_get(datas, 2), "c", 1); test_seq_string_unref(datas); printf("All C tests passed!\n"); diff --git a/tests/test_cpp.cpp b/tests/test_cpp.cpp index 7d945bf..4f9796b 100644 --- a/tests/test_cpp.cpp +++ b/tests/test_cpp.cpp @@ -1,6 +1,7 @@ // Test C++ bindings. #include #include +#include #include "test.hpp" int main() { @@ -53,10 +54,17 @@ int main() { assert(refObjWithSeq.dataSize() == 0); refObjWithSeq.free(); + std::cout << "Testing getMessage" << std::endl; + std::string message = getMessage(); + assert(message.size() == 11); + assert(message == std::string("alpha\0omega", 11)); + std::cout << "Testing SeqString via getDatas" << std::endl; SeqString datas = getDatas(); assert(datas.size() == 3); - assert(datas[0][0] == 'a'); + assert(datas[0] == "a"); + assert(datas[1] == "b"); + assert(datas[2] == "c"); datas.free(); std::cout << "All C++ tests passed!" << std::endl; diff --git a/tests/test_nim.nim b/tests/test_nim.nim index 8356a63..f81e26a 100644 --- a/tests/test_nim.nim +++ b/tests/test_nim.nim @@ -61,8 +61,15 @@ let objWithProc = simpleObjWithProc(1, 2, false) doAssert objWithProc.simpleA == 1, "simpleA should be 1" objWithProc.extraProc() -# Note: SeqString and getDatas tests are skipped due to cstring lifetime -# issues across DLL boundaries. The cstring returned from the DLL points -# to internal string data that can be freed before the client uses it. +echo "Testing getMessage" +doAssert getMessage() == "alpha\0omega", "getMessage should preserve embedded NUL" + +echo "Testing SeqString via getDatas" +block: + let datas = getDatas() + doAssert datas.len == 3, "datas should have 3 elements" + doAssert datas[0] == "a", "datas[0] should be a" + doAssert datas[1] == "b", "datas[1] should be b" + doAssert datas[2] == "c", "datas[2] should be c" echo "All Nim-C-Nim sandwich tests passed!" diff --git a/tests/test_node.js b/tests/test_node.js index e0e3814..b1c90d5 100644 --- a/tests/test_node.js +++ b/tests/test_node.js @@ -33,4 +33,14 @@ seqInt.add(42); console.assert(seqInt.length() === 1, "seqInt should have 1 element"); console.assert(seqInt.get(0) === 42, "seqInt[0] should be 42"); +console.log("Testing getMessage"); +console.assert(test.getMessage() === "alpha\0omega", "getMessage should preserve embedded NUL"); + +console.log("Testing getDatas"); +const datas = test.getDatas(); +console.assert(datas.length() === 3, "datas should have 3 elements"); +console.assert(datas.get(0) === "a", "datas[0] should be a"); +console.assert(datas.get(1) === "b", "datas[1] should be b"); +console.assert(datas.get(2) === "c", "datas[2] should be c"); + console.log("All Node.js tests passed!"); diff --git a/tests/test_pixie_c.c b/tests/test_pixie_c.c index 98c6ffd..b40814a 100644 --- a/tests/test_pixie_c.c +++ b/tests/test_pixie_c.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include "pixie.h" @@ -26,6 +27,26 @@ static void assert_color(Color actual, Color expected) { approx(actual.a, expected.a); } +static void assert_buffer_len_gt(GennyBuffer buffer, intptr_t min_len) { + assert(buffer != NULL); + assert(pixie_genny_buffer_len(buffer) > min_len); + pixie_genny_buffer_unref(buffer); +} + +static void assert_buffer_contains(GennyBuffer buffer, const char *needle) { + assert(buffer != NULL); + assert(strstr(pixie_genny_buffer_data(buffer), needle) != NULL); + pixie_genny_buffer_unref(buffer); +} + +static void assert_buffer_equals(GennyBuffer buffer, const char *expected) { + assert(buffer != NULL); + intptr_t len = pixie_genny_buffer_len(buffer); + assert(len == (intptr_t)strlen(expected)); + assert(memcmp(pixie_genny_buffer_data(buffer), expected, (size_t)len) == 0); + pixie_genny_buffer_unref(buffer); +} + int main() { const char *ppm = "P3\n2 1\n255\n255 0 0 0 255 0\n"; @@ -58,7 +79,7 @@ int main() { Image image = pixie_new_image(4, 3); assert(pixie_image_get_width(image) == 4); assert(pixie_image_get_height(image) == 3); - assert(strlen(pixie_image_encode_base64(image)) > 20); + assert_buffer_len_gt(pixie_image_encode_base64(image), 20); pixie_image_fill(image, red); assert(pixie_image_is_one_color(image)); assert(pixie_image_is_opaque(image)); @@ -129,7 +150,7 @@ int main() { assert(pixie_path_stroke_overlaps(rect_path, pixie_vector2(0, 5), identity, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, solid_dashes)); Typeface typeface = pixie_read_typeface(FONT_PATH); - assert(strstr(pixie_typeface_get_file_path(typeface), "Inter-Regular.ttf") != NULL); + assert_buffer_contains(pixie_typeface_get_file_path(typeface), "Inter-Regular.ttf"); pixie_typeface_set_file_path(typeface, FONT_PATH); assert(pixie_typeface_has_glyph(typeface, 'A')); assert(pixie_typeface_get_advance(typeface, 'A') > 0); @@ -155,7 +176,7 @@ int main() { SeqSpan spans = pixie_new_seq_span(); pixie_seq_span_add(spans, span); Arrangement arrangement = pixie_seq_span_typeset(spans, pixie_vector2(100, 100), CENTER_ALIGN, BOTTOM_ALIGN, 1); - assert(strcmp(pixie_span_get_text(pixie_seq_span_get(spans, 0)), "hello") == 0); + assert_buffer_equals(pixie_span_get_text(pixie_seq_span_get(spans, 0)), "hello"); assert(pixie_arrangement_layout_bounds(arrangement).x > 0); assert(pixie_seq_span_layout_bounds(spans).y > 0); assert(pixie_arrangement_compute_bounds(arrangement, mat).x > 0); @@ -226,7 +247,9 @@ int main() { pixie_context_save_layer(ctx); pixie_context_restore(ctx); - Image decoded = pixie_decode_base64(pixie_image_encode_base64(canvas)); + GennyBuffer encoded_canvas = pixie_image_encode_base64(canvas); + Image decoded = pixie_decode_base64(pixie_genny_buffer_data(encoded_canvas)); + pixie_genny_buffer_unref(encoded_canvas); assert(pixie_image_get_width(decoded) == pixie_image_get_width(canvas)); assert(pixie_image_get_height(decoded) == pixie_image_get_height(canvas)); assert(pixie_image_get_width(pixie_decode_image((char*)ppm)) == 2); @@ -237,7 +260,7 @@ int main() { assert(pixie_path_compute_bounds(pixie_parse_path("M0 0 L10 0 L10 10 Z"), identity).w == 10); pixie_parse_color("bad"); assert(pixie_check_error()); - assert(strstr(pixie_take_error(), "bad") != NULL); + assert_buffer_contains(pixie_take_error(), "bad"); printf("All Pixie C tests passed!\n"); return 0; diff --git a/tests/test_pixie_cpp.cpp b/tests/test_pixie_cpp.cpp index cac8cf9..298d6df 100644 --- a/tests/test_pixie_cpp.cpp +++ b/tests/test_pixie_cpp.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "pixie.hpp" #ifndef PIXIE_ROOT @@ -54,7 +55,7 @@ int main() { Image image(4, 3); assert(image.getWidth() == 4); assert(image.getHeight() == 3); - assert(std::strlen(image.encodeBase64()) > 20); + assert(image.encodeBase64().size() > 20); image.fill(red); assert(image.isOneColor()); assert(image.isOpaque()); @@ -124,7 +125,7 @@ int main() { assert(rectPath.strokeOverlaps(vector2(0, 5), identity, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, solidDashes)); Typeface typeface = readTypeface(FONT_PATH); - assert(std::strstr(typeface.getFilePath(), "Inter-Regular.ttf") != nullptr); + assert(typeface.getFilePath().find("Inter-Regular.ttf") != std::string::npos); typeface.setFilePath(FONT_PATH); assert(typeface.hasGlyph(U'A')); assert(typeface.getAdvance(U'A') > 0); @@ -150,7 +151,7 @@ int main() { SeqSpan spans; spans.add(span); Arrangement arrangement = spans.typeset(vector2(100, 100), CENTER_ALIGN, BOTTOM_ALIGN, true); - assert(std::strcmp(spans[0].getText(), "hello") == 0); + assert(spans[0].getText() == "hello"); assert(arrangement.layoutBounds().x > 0); assert(spans.layoutBounds().y > 0); assert(arrangement.computeBounds(mat).x > 0); @@ -222,7 +223,8 @@ int main() { ctx.saveLayer(); ctx.restore(); - Image decoded = decodeBase64(canvas.encodeBase64()); + std::string encodedCanvas = canvas.encodeBase64(); + Image decoded = decodeBase64(encodedCanvas.c_str()); assert(decoded.getWidth() == canvas.getWidth()); assert(decoded.getHeight() == canvas.getHeight()); assert(decodeImage(ppm).getWidth() == 2); @@ -233,7 +235,7 @@ int main() { assert(parsePath("M0 0 L10 0 L10 10 Z").computeBounds(identity).w == 10); parseColor("bad"); assert(checkError()); - assert(std::strstr(takeError(), "bad") != nullptr); + assert(takeError().find("bad") != std::string::npos); std::cout << "All Pixie C++ tests passed!" << std::endl; return 0; diff --git a/tests/test_pixie_zig.zig b/tests/test_pixie_zig.zig index 1406f2b..ece8973 100644 --- a/tests/test_pixie_zig.zig +++ b/tests/test_pixie_zig.zig @@ -16,6 +16,7 @@ fn approxEps(value: f32, expected: f32, eps: f32) !void { pub fn main() !void { @setEvalBranchQuota(10000); + const allocator = std.heap.page_allocator; const font_path = "../../pixie/tests/fonts/Inter-Regular.ttf"; const image_path = "../../pixie/tests/images/turtle.png"; const ppm = "P3\n2 1\n255\n255 0 0 0 255 0\n"; @@ -51,7 +52,9 @@ pub fn main() !void { defer image.deinit(); try expect(image.getWidth() == 4); try expect(image.getHeight() == 3); - try expect((try image.encodeBase64()).len > 20); + const image_base64 = try image.encodeBase64(allocator); + defer allocator.free(image_base64); + try expect(image_base64.len > 20); image.fill(red); try expect(image.isOneColor()); try expect(image.isOpaque()); @@ -138,7 +141,9 @@ pub fn main() !void { const typeface = try pixie.readTypeface(font_path); defer typeface.deinit(); - try expect(std.mem.endsWith(u8, typeface.getFilePath(), "Inter-Regular.ttf")); + const typeface_file_path = try typeface.getFilePath(allocator); + defer allocator.free(typeface_file_path); + try expect(std.mem.endsWith(u8, typeface_file_path, "Inter-Regular.ttf")); typeface.setFilePath(font_path); try expect(typeface.hasGlyph('A')); try expect(typeface.getAdvance('A') > 0); @@ -172,7 +177,9 @@ pub fn main() !void { spans.append(span); const arrangement = spans.typeset(pixie.Vector2.init(100, 100), .center_align, .bottom_align, true); defer arrangement.deinit(); - try expect(std.mem.eql(u8, spans.get(0).getText(), "hello")); + const span_text = try spans.get(0).getText(allocator); + defer allocator.free(span_text); + try expect(std.mem.eql(u8, span_text, "hello")); try expect(arrangement.layoutBounds().x > 0); try expect(spans.layoutBounds().y > 0); try expect((try arrangement.computeBounds(mat)).x > 0); @@ -247,7 +254,9 @@ pub fn main() !void { try ctx.saveLayer(); try ctx.restore(); - const decoded = try pixie.decodeBase64(try canvas.encodeBase64()); + const canvas_base64 = try canvas.encodeBase64(allocator); + defer allocator.free(canvas_base64); + const decoded = try pixie.decodeBase64(canvas_base64); defer decoded.deinit(); try expect(decoded.getWidth() == canvas.getWidth()); try expect(decoded.getHeight() == canvas.getHeight()); @@ -271,7 +280,9 @@ pub fn main() !void { try expect(err == error.PixieError); } try expect(pixie.checkError()); - try expect(std.mem.indexOf(u8, pixie.takeError(), "bad") != null); + const message = try pixie.takeError(allocator); + defer allocator.free(message); + try expect(std.mem.indexOf(u8, message, "bad") != null); std.debug.print("All Pixie Zig tests passed!\n", .{}); } diff --git a/tests/test_python.py b/tests/test_python.py index 3deeeda..dbbb568 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -42,6 +42,9 @@ seq_int.clear() assert len(seq_int) == 0, "seq_int should be empty after clear" +print("Testing get_message") +assert test.get_message() == "alpha\0omega", "get_message should preserve embedded NUL" + print("Testing get_datas") datas = test.get_datas() assert len(datas) == 3, "datas should have 3 elements" diff --git a/tests/test_python_native.py b/tests/test_python_native.py index 5ab7ad8..da41817 100644 --- a/tests/test_python_native.py +++ b/tests/test_python_native.py @@ -17,6 +17,7 @@ def test_module_values(): assert test.SECOND == 1 assert test.THIRD == 2 assert test.SimpleEnum is int + assert test.get_message() == "alpha\0omega" def test_value_objects(): diff --git a/tests/test_zig.zig b/tests/test_zig.zig index 73e64dd..aa59d59 100644 --- a/tests/test_zig.zig +++ b/tests/test_zig.zig @@ -2,6 +2,8 @@ const std = @import("std"); const test_lib = @import("generated/test.zig"); pub fn main() !void { + const allocator = std.heap.page_allocator; + std.debug.print("Testing Zig bindings\n", .{}); std.debug.print("Testing simpleCall\n", .{}); @@ -19,5 +21,24 @@ pub fn main() !void { if (obj.simple_b != 20) return error.TestFailed; if (obj.simple_c != true) return error.TestFailed; + std.debug.print("Testing getMessage\n", .{}); + const message = try test_lib.getMessage(allocator); + defer allocator.free(message); + if (!std.mem.eql(u8, message, "alpha\x00omega")) return error.TestFailed; + + std.debug.print("Testing SeqString\n", .{}); + const datas = test_lib.getDatas(); + defer datas.deinit(); + if (datas.len() != 3) return error.TestFailed; + const data0 = try datas.get(0, allocator); + defer allocator.free(data0); + const data1 = try datas.get(1, allocator); + defer allocator.free(data1); + const data2 = try datas.get(2, allocator); + defer allocator.free(data2); + if (!std.mem.eql(u8, data0, "a")) return error.TestFailed; + if (!std.mem.eql(u8, data1, "b")) return error.TestFailed; + if (!std.mem.eql(u8, data2, "c")) return error.TestFailed; + std.debug.print("All Zig tests passed!\n", .{}); }