From a0c3a2d181d7427b59e269b14941df396cf5d6e0 Mon Sep 17 00:00:00 2001 From: Tests Date: Sat, 30 May 2026 07:18:22 -0700 Subject: [PATCH] Add Node and C++ exception wrappers --- src/genny/languages/cpp.nim | 77 +++++++++++++++++++++++++++++------- src/genny/languages/node.nim | 44 ++++++++++++++++++--- tests/generated/internal.nim | 18 +++++++++ tests/generated/test.h | 8 ++++ tests/generated/test.hpp | 52 +++++++++++++++++++++++- tests/generated/test.js | 39 ++++++++++++++++++ tests/generated/test.nim | 28 +++++++++++++ tests/generated/test.py | 32 +++++++++++++++ tests/generated/test.zig | 32 +++++++++++++++ tests/test.nim | 27 +++++++++++++ tests/test_cpp.cpp | 18 +++++++++ tests/test_node.js | 15 +++++++ tests/test_pixie_cpp.cpp | 10 +++-- tests/test_pixie_node.js | 8 ++-- 14 files changed, 381 insertions(+), 27 deletions(-) diff --git a/src/genny/languages/cpp.nim b/src/genny/languages/cpp.nim index 87fc4a6..b3bd848 100644 --- a/src/genny/languages/cpp.nim +++ b/src/genny/languages/cpp.nim @@ -7,6 +7,7 @@ var procs {.compiletime.}: string classes {.compiletime.}: string members {.compiletime.}: string + hasRaisingProcs {.compiletime.}: bool proc unCapitalize(s: string): string = s[0].toLowerAscii() & s[1 .. ^1] @@ -121,6 +122,31 @@ proc cppReturnValue(returnType: NimNode, call: string): string = else: call +proc addCppCall( + returnType: NimNode, + call: string, + raises: bool +) = + if not raises: + members.add " " + if returnType.kind != nnkEmpty: + members.add "return " + members.add cppReturnValue(returnType, call) + members.add ";\n" + return + + hasRaisingProcs = true + if returnType.kind == nnkEmpty: + members.add &" {call};\n" + members.add " throwIfError();\n" + else: + members.add &" auto result = {call};\n" + if returnType.isStringType: + members.add " throwIfError(result);\n" + else: + members.add " throwIfError();\n" + members.add &" return {cppReturnValue(returnType, \"result\")};\n" + proc dllProc*(procName: string, args: openarray[string], restype: string) = var argStr = "" for arg in args: @@ -160,6 +186,7 @@ proc exportProcCpp*( procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] + procRaises = sym.raises() var apiProcName = "" if owner != nil: @@ -190,17 +217,13 @@ proc exportProcCpp*( members.add ", " members.removeSuffix ", " members.add ") {\n" - members.add " " - if procReturn.kind != nnkEmpty: - members.add "return " var call = &"$lib_{apiProcName}(" for param in procParams: call.add cppArgValue(param[1], param[0].getParamName()) call.add ", " call.removeSuffix ", " call.add ")" - members.add cppReturnValue(procReturn, call) - members.add ";\n" + addCppCall(procReturn, call, procRaises) members.add "};\n\n" else: @@ -240,18 +263,13 @@ proc exportProcCpp*( members.removeSuffix ", " members.add ") " members.add "{\n" - if procReturn.kind == nnkEmpty: - members.add &" " - else: - members.add &" return " var call = &"$lib_{apiProcName}(*this, " for param in procParams[1..^1]: call.add cppArgValue(param[1], param[0].getParamName()) call.add ", " call.removeSuffix ", " call.add ")" - members.add cppReturnValue(procReturn, call) - members.add ";\n" + addCppCall(procReturn, call, procRaises) members.add "};\n\n" proc exportObjectCpp*( @@ -435,13 +453,17 @@ proc exportRefObjectCpp*( members.removeSuffix ", " members.add ")" members.add " {\n" - members.add &" this->reference = " + members.add &" auto result = " members.add &"{constructorLibProc}(" for param in constructorParams: members.add cppArgValue(param[1], param[0].getParamName()) members.add ", " members.removeSuffix ", " - members.add ").reference;\n" + members.add ");\n" + if constructorRaises: + hasRaisingProcs = true + members.add " throwIfError();\n" + members.add " this->reference = result.reference;\n" members.add "}\n\n" var dllParams: seq[(NimNode, NimNode)] @@ -562,6 +584,7 @@ const header = """ #include #include +#include #include """ @@ -616,11 +639,36 @@ void GennyBuffer::free() { """ +const errorMembers = """ +struct $LibException : public std::runtime_error { + explicit $LibException(const std::string& message) : std::runtime_error(message) {} +}; + +static inline void throwIfError() { + if ($lib_check_error()) { + throw $LibException(gennyBufferToString($lib_take_error())); + } +} + +static inline void throwIfError(GennyBuffer buffer) { + if ($lib_check_error()) { + $lib_genny_buffer_unref(buffer); + throw $LibException(gennyBufferToString($lib_take_error())); + } +} + +""" + const footer = """ #endif """ proc writeCpp*(dir, lib: string) = + let errorBlock = + if hasRaisingProcs: + errorMembers + else: + "" createDir(dir) writeFile(&"{dir}/{toSnakeCase(lib)}.hpp", ( header & @@ -632,7 +680,8 @@ proc writeCpp*(dir, lib: string) = procs & "}\n\n" & bufferMembers & + errorBlock & members & footer - ).replace("$lib", toSnakeCase(lib)).replace("$LIB", lib.toUpperAscii()) + ).replace("$Lib", lib).replace("$lib", toSnakeCase(lib)).replace("$LIB", lib.toUpperAscii()) ) diff --git a/src/genny/languages/node.nim b/src/genny/languages/node.nim index 82abee6..c0cebc2 100644 --- a/src/genny/languages/node.nim +++ b/src/genny/languages/node.nim @@ -116,6 +116,30 @@ proc jsReturnValue(returnType: NimNode, call: string): string = else: call +proc addNodeCall( + returnType: NimNode, + call: string, + raises: bool +) = + if not raises: + types.add " " + if returnType.kind != nnkEmpty: + types.add "return " + types.add jsReturnValue(returnType, call) + types.add ";\n" + return + + if returnType.kind == nnkEmpty: + types.add &" {call};\n" + types.add " throwIfError();\n" + else: + types.add &" const result = {call};\n" + if returnType.isStringType: + types.add " throwIfError(result);\n" + else: + types.add " throwIfError();\n" + types.add &" return {jsReturnValue(returnType, \"result\")};\n" + proc convertExportFromNode*(sym: NimNode): string = discard @@ -219,9 +243,6 @@ proc exportProcNode*( types.add &", " types.removeSuffix ", " types.add ") {\n" - types.add " " - if procReturn.kind != nnkEmpty: - types.add "return " var call = &"{apiProcName}(" for i, param in procParams[0 .. ^1]: if isRefObject and i == 0: @@ -232,8 +253,7 @@ proc exportProcNode*( call.add &", " call.removeSuffix ", " call.add ")" - types.add jsReturnValue(procReturn, call) - types.add ";\n" + addNodeCall(procReturn, call, procRaises) types.add "}\n\n" proc exportObjectNode*( @@ -343,6 +363,7 @@ proc exportRefObjectNode*( constructorLibProc = &"$lib_{toSnakeCase(constructor.repr)}" constructorType = constructor.getTypeInst() constructorParams = constructorType[0][1 .. ^1] + constructorRaises = constructor.raises() # Declare constructor C function declareFunc(constructorLibProc, constructorParams, "'uint64'") @@ -361,6 +382,8 @@ proc exportRefObjectNode*( types.add ", " types.removeSuffix ", " types.add ");\n" + if constructorRaises: + types.add " throwIfError();\n" types.add &" return new {objName}(ref);\n" types.add "}\n\n" @@ -477,6 +500,17 @@ class $LibException extends Error { } } +exports.$LibException = $LibException; + +function throwIfError(buffer = null) { + if (checkError()) { + if (buffer !== null && buffer !== 0 && buffer !== 0n) { + $lib_genny_buffer_unref(buffer); + } + throw new $LibException(takeError()); + } +} + function runeToInt(value) { assert.strictEqual(typeof value, 'string', 'expected rune string'); const chars = Array.from(value); diff --git a/tests/generated/internal.nim b/tests/generated/internal.nim index 272e703..172d1d4 100644 --- a/tests/generated/internal.nim +++ b/tests/generated/internal.nim @@ -27,6 +27,24 @@ proc test_genny_buffer_unref*(buffer: GennyBuffer) {.raises: [], cdecl, exportc, proc test_simple_call*(a: int): int {.raises: [], cdecl, exportc, dynlib.} = simpleCall(a) +proc test_check_error*(): bool {.raises: [], cdecl, exportc, dynlib.} = + checkError() + +proc test_take_error*(): GennyBuffer {.raises: [], cdecl, exportc, dynlib.} = + newGennyBuffer(takeError()) + +proc test_maybe_message*(message: cstring, fail: bool): GennyBuffer {.raises: [], cdecl, exportc, dynlib.} = + try: + result = newGennyBuffer(maybeMessage(message.`$`, fail)) + except testError as e: + lastError = e + +proc test_maybe_number*(value: int, fail: bool): int {.raises: [], cdecl, exportc, dynlib.} = + try: + result = maybeNumber(value, fail) + except testError as e: + lastError = e + proc test_simple_obj*(simple_a: int, simple_b: byte, simple_c: bool): SimpleObj {.raises: [], cdecl, exportc, dynlib.} = result.simple_a = simple_a result.simple_b = simple_b diff --git a/tests/generated/test.h b/tests/generated/test.h index fc37e98..da5df03 100644 --- a/tests/generated/test.h +++ b/tests/generated/test.h @@ -49,6 +49,14 @@ void test_genny_buffer_unref(GennyBuffer buffer); */ intptr_t test_simple_call(intptr_t a); +char test_check_error(); + +GennyBuffer test_take_error(); + +GennyBuffer test_maybe_message(const char* message, char fail); + +intptr_t test_maybe_number(intptr_t value, char fail); + SimpleObj test_simple_obj(intptr_t simple_a, uint8_t simple_b, char simple_c); char test_simple_obj_eq(SimpleObj a, SimpleObj b); diff --git a/tests/generated/test.hpp b/tests/generated/test.hpp index 8920046..621741e 100644 --- a/tests/generated/test.hpp +++ b/tests/generated/test.hpp @@ -3,6 +3,7 @@ #include #include +#include #include static constexpr auto SIMPLE_CONST = 123; @@ -157,6 +158,14 @@ void test_genny_buffer_unref(GennyBuffer buffer); std::intptr_t test_simple_call(std::intptr_t a); +bool test_check_error(); + +GennyBuffer test_take_error(); + +GennyBuffer test_maybe_message(const char* message, bool fail); + +std::intptr_t test_maybe_number(std::intptr_t value, bool fail); + SimpleObj test_simple_obj(std::intptr_t simple_a, std::uint8_t simple_b, bool simple_c); bool test_simple_obj_eq(SimpleObj a, SimpleObj b); @@ -262,16 +271,54 @@ void GennyBuffer::free() { test_genny_buffer_unref(*this); } +struct testException : public std::runtime_error { + explicit testException(const std::string& message) : std::runtime_error(message) {} +}; + +static inline void throwIfError() { + if (test_check_error()) { + throw testException(gennyBufferToString(test_take_error())); + } +} + +static inline void throwIfError(GennyBuffer buffer) { + if (test_check_error()) { + test_genny_buffer_unref(buffer); + throw testException(gennyBufferToString(test_take_error())); + } +} + std::intptr_t simpleCall(std::intptr_t a) { return test_simple_call(a); }; +bool checkError() { + return test_check_error(); +}; + +std::string takeError() { + return gennyBufferToString(test_take_error()); +}; + +std::string maybeMessage(const char* message, bool fail) { + auto result = test_maybe_message(message, fail); + throwIfError(result); + return gennyBufferToString(result); +}; + +std::intptr_t maybeNumber(std::intptr_t value, bool fail) { + auto result = test_maybe_number(value, fail); + throwIfError(); + return result; +}; + SimpleObj simpleObj(std::intptr_t simpleA, std::uint8_t simpleB, bool simpleC) { return test_simple_obj(simpleA, simpleB, simpleC); }; SimpleRefObj::SimpleRefObj() { - this->reference = test_new_simple_ref_obj().reference; + auto result = test_new_simple_ref_obj(); + this->reference = result.reference; } std::intptr_t SimpleRefObj::getSimpleRefA(){ @@ -335,7 +382,8 @@ void SeqInt::free(){ } RefObjWithSeq::RefObjWithSeq() { - this->reference = test_new_ref_obj_with_seq().reference; + auto result = test_new_ref_obj_with_seq(); + this->reference = result.reference; } std::intptr_t RefObjWithSeq::dataSize(){ diff --git a/tests/generated/test.js b/tests/generated/test.js index 95eea7f..35e6095 100644 --- a/tests/generated/test.js +++ b/tests/generated/test.js @@ -42,6 +42,17 @@ class testException extends Error { } } +exports.testException = testException; + +function throwIfError(buffer = null) { + if (checkError()) { + if (buffer !== null && buffer !== 0 && buffer !== 0n) { + test_genny_buffer_unref(buffer); + } + throw new testException(takeError()); + } +} + function runeToInt(value) { assert.strictEqual(typeof value, 'string', 'expected rune string'); const chars = Array.from(value); @@ -62,6 +73,26 @@ function simpleCall(a) { return test_simple_call(a); } +function checkError() { + return test_check_error(); +} + +function takeError() { + return gennyBufferToString(test_take_error()); +} + +function maybeMessage(message, fail) { + const result = test_maybe_message(message, fail); + throwIfError(result); + return gennyBufferToString(result); +} + +function maybeNumber(value, fail) { + const result = test_maybe_number(value, fail); + throwIfError(); + return result; +} + const SimpleObj = koffi.struct('SimpleObj', { simpleA: 'int64', simpleB: 'uint8', @@ -257,6 +288,10 @@ function getMessage() { const test_simple_call = lib.func('test_simple_call', 'int64', ['int64']); +const test_check_error = lib.func('test_check_error', 'bool', []); +const test_take_error = lib.func('test_take_error', 'uint64', []); +const test_maybe_message = lib.func('test_maybe_message', 'uint64', ['str', 'bool']); +const test_maybe_number = lib.func('test_maybe_number', 'int64', ['int64', 'bool']); const test_simple_ref_obj_unref = lib.func('test_simple_ref_obj_unref', 'void', ['uint64']); const test_new_simple_ref_obj = lib.func('test_new_simple_ref_obj', 'uint64', []); const test_simple_ref_obj_get_simple_ref_a = lib.func('test_simple_ref_obj_get_simple_ref_a', 'int64', ['uint64']); @@ -298,6 +333,10 @@ exports.FIRST = 0; exports.SECOND = 1; exports.THIRD = 2; exports.simpleCall = simpleCall; +exports.checkError = checkError; +exports.takeError = takeError; +exports.maybeMessage = maybeMessage; +exports.maybeNumber = maybeNumber; exports.SimpleObj = SimpleObj; exports.simpleObj = simpleObj; exports.SimpleRefObj = SimpleRefObj; diff --git a/tests/generated/test.nim b/tests/generated/test.nim index fc8e81d..e3c5f3b 100644 --- a/tests/generated/test.nim +++ b/tests/generated/test.nim @@ -106,6 +106,34 @@ proc test_simple_call(a: int): int {.importc: "test_simple_call", cdecl.} proc simpleCall*(a: int): int {.inline.} = result = test_simple_call(a) +proc test_check_error(): bool {.importc: "test_check_error", cdecl.} + +proc checkError*(): bool {.inline.} = + result = test_check_error() + +proc test_take_error(): pointer {.importc: "test_take_error", cdecl.} + +proc takeError*(): string {.inline.} = + let gennyBuffer = test_take_error() + result = gennyBufferToString(gennyBuffer) + +proc test_maybe_message(message: cstring, fail: bool): pointer {.importc: "test_maybe_message", cdecl.} + +proc maybeMessage*(message: string, fail: bool): string {.inline.} = + let gennyBuffer = test_maybe_message(message.cstring, fail) + if checkError(): + if gennyBuffer != nil: + test_genny_buffer_unref(gennyBuffer) + raise newException(testError, $takeError()) + result = gennyBufferToString(gennyBuffer) + +proc test_maybe_number(value: int, fail: bool): int {.importc: "test_maybe_number", cdecl.} + +proc maybeNumber*(value: int, fail: bool): int {.inline.} = + result = test_maybe_number(value, fail) + if checkError(): + raise newException(testError, $takeError()) + proc test_new_simple_ref_obj(): pointer {.importc: "test_new_simple_ref_obj", cdecl.} proc newSimpleRefObj*(): SimpleRefObj {.inline.} = diff --git a/tests/generated/test.py b/tests/generated/test.py index abb01cb..dacbb9e 100644 --- a/tests/generated/test.py +++ b/tests/generated/test.py @@ -72,6 +72,26 @@ def simple_call(a): result = dll.test_simple_call(a) return result +def check_error(): + result = dll.test_check_error() + return result + +def take_error(): + result = _genny_buffer_to_string(dll.test_take_error()) + return result + +def maybe_message(message, fail): + result = _genny_buffer_to_string(dll.test_maybe_message(message.encode("utf8"), fail)) + if check_error(): + raise testError(take_error()) + return result + +def maybe_number(value, fail): + result = dll.test_maybe_number(value, fail) + if check_error(): + raise testError(take_error()) + return result + class SimpleObj(Structure): _fields_ = [ ("simple_a", c_longlong), @@ -285,6 +305,18 @@ def get_message(): dll.test_simple_call.argtypes = [c_longlong] dll.test_simple_call.restype = c_longlong +dll.test_check_error.argtypes = [] +dll.test_check_error.restype = c_bool + +dll.test_take_error.argtypes = [] +dll.test_take_error.restype = _GennyBuffer + +dll.test_maybe_message.argtypes = [c_char_p, c_bool] +dll.test_maybe_message.restype = _GennyBuffer + +dll.test_maybe_number.argtypes = [c_longlong, c_bool] +dll.test_maybe_number.restype = c_longlong + dll.test_simple_ref_obj_unref.argtypes = [SimpleRefObj] dll.test_simple_ref_obj_unref.restype = None diff --git a/tests/generated/test.zig b/tests/generated/test.zig index 5f827a3..758cb81 100644 --- a/tests/generated/test.zig +++ b/tests/generated/test.zig @@ -38,6 +38,38 @@ pub inline fn simpleCall(a: isize) isize { return test_simple_call(a); } +extern fn test_check_error() bool; +pub inline fn checkError() bool { + return test_check_error(); +} + +extern fn test_take_error() ?*GennyBuffer; +pub inline fn takeError(allocator: std.mem.Allocator) std.mem.Allocator.Error![:0]u8 { + const result = test_take_error(); + const buffer = result.?; + defer buffer.deinit(); + return buffer.toOwnedSlice(allocator); +} + +extern fn test_maybe_message(message: [*:0]const u8, fail: bool) ?*GennyBuffer; +pub inline fn maybeMessage(message: [:0]const u8, fail: bool, allocator: std.mem.Allocator) (Error || std.mem.Allocator.Error)![:0]u8 { + const result = test_maybe_message(message.ptr, fail); + if (checkError()) { + if (result) |buffer| buffer.deinit(); + return error.testError; + } + const buffer = result.?; + defer buffer.deinit(); + return buffer.toOwnedSlice(allocator); +} + +extern fn test_maybe_number(value: isize, fail: bool) isize; +pub inline fn maybeNumber(value: isize, fail: bool) Error!isize { + const result = test_maybe_number(value, fail); + if (checkError()) return error.testError; + return result; +} + pub const SimpleObj = extern struct { simple_a: isize, simple_b: u8, diff --git a/tests/test.nim b/tests/test.nim index 36a4b5a..d777b62 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -17,8 +17,35 @@ proc simpleCall(a: int): int = ## Returns the integer passed in. return a +type testError = object of ValueError + +var lastError: ref testError + +proc takeError(): string = + if lastError == nil: + return "" + result = lastError.msg + lastError = nil + +proc checkError(): bool = + lastError != nil + +proc maybeMessage(message: string, fail: bool): string {.raises: [testError].} = + if fail: + raise newException(testError, message) + "ok:" & message + +proc maybeNumber(value: int, fail: bool): int {.raises: [testError].} = + if fail: + raise newException(testError, "bad number " & $value) + value + exportProcs: simpleCall + checkError + takeError + maybeMessage + maybeNumber type SimpleObj = object simpleA*: int diff --git a/tests/test_cpp.cpp b/tests/test_cpp.cpp index 4f9796b..7b30f51 100644 --- a/tests/test_cpp.cpp +++ b/tests/test_cpp.cpp @@ -11,6 +11,24 @@ int main() { assert(simpleCall(42) == 42); assert(simpleCall(0) == 0); + std::cout << "Testing exceptions" << std::endl; + assert(maybeMessage("hello", false) == "ok:hello"); + assert(maybeNumber(7, false) == 7); + assert(!checkError()); + try { + maybeMessage("bad message", true); + assert(false); + } catch (const testException& e) { + assert(std::string(e.what()).find("bad message") != std::string::npos); + } + assert(!checkError()); + try { + maybeNumber(9, true); + assert(false); + } catch (const testException& e) { + assert(std::string(e.what()).find("bad number 9") != std::string::npos); + } + std::cout << "Testing SIMPLE_CONST" << std::endl; assert(SIMPLE_CONST == 123); diff --git a/tests/test_node.js b/tests/test_node.js index b1c90d5..d52d80d 100644 --- a/tests/test_node.js +++ b/tests/test_node.js @@ -1,4 +1,5 @@ // Test Node.js bindings. +const assert = require('assert'); const test = require('./generated/test.js'); console.log("Testing Node.js bindings"); @@ -7,6 +8,20 @@ console.log("Testing simpleCall"); console.assert(test.simpleCall(42) === 42, "simpleCall should return input"); console.assert(test.simpleCall(0) === 0, "simpleCall should return 0"); +console.log("Testing exceptions"); +console.assert(test.maybeMessage("hello", false) === "ok:hello", "maybeMessage should return on success"); +console.assert(test.maybeNumber(7, false) === 7, "maybeNumber should return on success"); +console.assert(test.checkError() === false, "checkError should start false"); +assert.throws( + () => test.maybeMessage("bad message", true), + (err) => err instanceof test.testException && err.message.includes("bad message") +); +console.assert(test.checkError() === false, "thrown exception should consume the pending error"); +assert.throws( + () => test.maybeNumber(9, true), + (err) => err instanceof test.testException && err.message.includes("bad number 9") +); + console.log("Testing SIMPLE_CONST"); console.assert(test.SIMPLE_CONST === 123, "SIMPLE_CONST should be 123"); diff --git a/tests/test_pixie_cpp.cpp b/tests/test_pixie_cpp.cpp index 286dc3c..bde49fa 100644 --- a/tests/test_pixie_cpp.cpp +++ b/tests/test_pixie_cpp.cpp @@ -306,9 +306,13 @@ int main() { approx(readFont(FONT_PATH).getSize(), 12); assert(parsePath("M0 0 L10 0 L10 10 Z").computeBounds(identity).w == 10); writeRenderImages("cpp"); - parseColor("bad"); - assert(checkError()); - assert(takeError().find("bad") != std::string::npos); + try { + parseColor("bad"); + assert(false); + } catch (const PixieException& e) { + assert(std::string(e.what()).find("bad") != std::string::npos); + } + assert(!checkError()); std::cout << "All Pixie C++ tests passed!" << std::endl; return 0; diff --git a/tests/test_pixie_node.js b/tests/test_pixie_node.js index 24bdcf8..df9b972 100644 --- a/tests/test_pixie_node.js +++ b/tests/test_pixie_node.js @@ -287,8 +287,10 @@ assert.strictEqual(pixie.readImageDimensions(imagePath).height, 40); assert.strictEqual(pixie.readFont(fontPath).size, 12); assert.strictEqual(pixie.parsePath('M0 0 L10 0 L10 10 Z').computeBounds(identity).w, 10); writeRenderImages('node'); -pixie.parseColor('bad'); -assert(pixie.checkError()); -assert(pixie.takeError().includes('bad')); +assert.throws( + () => pixie.parseColor('bad'), + (err) => err instanceof pixie.PixieException && err.message.includes('bad') +); +assert.strictEqual(pixie.checkError(), false); console.log('All Pixie Node tests passed!');