From 43023570d65dd5fd5837a6757f48ba0a57ec9a09 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 13:28:54 +0000 Subject: [PATCH 01/11] feat(ipc-codegen): add standalone codegen package + wire-compat examples Add top-level ipc-codegen/ containing: - A schema-driven multi-language code generator (TS/C++/Rust/Zig) - Committed JSON schemas for BB API, WSDB, CDB, AVM - A self-contained 4-language echo example harness under ipc-codegen/examples/ with cross-language wire-compatibility tests The codegen ships ungated by any existing consumer. Only the echo examples invoke it via ipc-codegen/bootstrap.sh. Existing bb.js codegen (barretenberg/ts/src/cbind/) is untouched in this PR. Generated outputs are .gitignored under barretenberg/.gitignore so producers can regenerate without committing artifacts. Echo example generated files remain tracked as a frozen reference for the codegen. Verify: - cd ipc-codegen && ./bootstrap.sh generate # regenerate echo bindings - cd ipc-codegen && ./bootstrap.sh test # 4x4 wire-compat matrix --- barretenberg/.gitignore | 5 +- ipc-codegen/.gitignore | 7 + ipc-codegen/.rebuild_patterns | 5 + ipc-codegen/bootstrap.sh | 48 + ipc-codegen/examples/cpp/echo/echo_client.cpp | 91 ++ ipc-codegen/examples/cpp/echo/echo_server.cpp | 61 + .../cpp/echo/generated/echo_ipc_client.cpp | 88 ++ .../cpp/echo/generated/echo_ipc_client.hpp | 43 + .../cpp/echo/generated/echo_ipc_server.hpp | 165 +++ .../cpp/echo/generated/echo_types.hpp | 134 ++ .../cpp/echo/generated/ipc_client.hpp | 112 ++ .../cpp/echo/generated/ipc_server.hpp | 252 ++++ ipc-codegen/examples/echo-schema/generate.sh | 20 + .../golden/echo_bytes_request.msgpack | 1 + .../golden/echo_bytes_response.msgpack | 1 + .../golden/echo_fields_request.msgpack | Bin 0 -> 47 bytes .../golden/echo_fields_response.msgpack | Bin 0 -> 54 bytes .../golden/echo_nested_request.msgpack | 1 + .../golden/echo_nested_response.msgpack | 1 + ipc-codegen/examples/echo-schema/schema.json | 52 + ipc-codegen/examples/rust/echo/Cargo.lock | 131 ++ ipc-codegen/examples/rust/echo/Cargo.toml | 17 + .../rust/echo/src/bin/generate_golden.rs | 65 + .../examples/rust/echo/src/bin/golden_test.rs | 109 ++ .../examples/rust/echo/src/echo_client.rs | 47 + .../examples/rust/echo/src/echo_server.rs | 71 ++ .../rust/echo/src/generated/backend.rs | 44 + .../rust/echo/src/generated/echo_client.rs | 84 ++ .../rust/echo/src/generated/echo_server.rs | 40 + .../rust/echo/src/generated/echo_types.rs | 401 ++++++ .../examples/rust/echo/src/generated/error.rs | 35 + .../rust/echo/src/generated/ipc_server.rs | 51 + .../rust/echo/src/generated/uds_backend.rs | 78 ++ ipc-codegen/examples/rust/echo/src/lib.rs | 16 + ipc-codegen/examples/rust/wsdb/Cargo.toml | 11 + ipc-codegen/examples/rust/wsdb/README.md | 45 + ipc-codegen/examples/rust/wsdb/generate.sh | 19 + ipc-codegen/examples/rust/wsdb/src/lib.rs | 21 + .../scripts/run_cross_language_tests.sh | 162 +++ ipc-codegen/examples/ts/echo/echo_client.ts | 64 + ipc-codegen/examples/ts/echo/echo_server.ts | 35 + .../examples/ts/echo/generated/api_types.ts | 259 ++++ .../examples/ts/echo/generated/async.ts | 72 ++ .../examples/ts/echo/generated/ipc_client.ts | 58 + .../examples/ts/echo/generated/ipc_server.ts | 82 ++ .../examples/ts/echo/generated/server.ts | 41 + .../examples/ts/echo/generated/sync.ts | 68 + ipc-codegen/examples/ts/echo/golden_test.ts | 116 ++ ipc-codegen/examples/ts/echo/package.json | 8 + ipc-codegen/examples/zig/echo/build.zig | 36 + ipc-codegen/examples/zig/echo/build.zig.zon | 19 + ipc-codegen/examples/zig/echo/echo_client.zig | 69 + ipc-codegen/examples/zig/echo/echo_server.zig | 63 + .../examples/zig/echo/generated/backend.zig | 27 + .../zig/echo/generated/echo_client.zig | 94 ++ .../zig/echo/generated/echo_server.zig | 72 ++ .../zig/echo/generated/echo_types.zig | 239 ++++ .../zig/echo/generated/ipc_server.zig | 112 ++ .../zig/echo/generated/uds_backend.zig | 62 + ipc-codegen/examples/zig/wsdb/.gitignore | 3 + ipc-codegen/examples/zig/wsdb/README.md | 93 ++ ipc-codegen/examples/zig/wsdb/build.zig | 31 + ipc-codegen/examples/zig/wsdb/build.zig.zon | 17 + ipc-codegen/examples/zig/wsdb/generate.sh | 16 + ipc-codegen/examples/zig/wsdb/src/main.zig | 24 + ipc-codegen/schemas/avm_schema.json | 2 + ipc-codegen/schemas/bb_curve_constants.json | 36 + ipc-codegen/schemas/bb_schema.json | 2 + ipc-codegen/schemas/cdb_schema.json | 2 + ipc-codegen/schemas/wsdb_schema.json | 2 + ipc-codegen/scripts/update_schemas.sh | 54 + ipc-codegen/scripts/validate_schemas.sh | 56 + ipc-codegen/src/README.md | 86 ++ ipc-codegen/src/SCHEMA_SPEC.md | 242 ++++ ipc-codegen/src/cpp_codegen.ts | 1109 +++++++++++++++++ ipc-codegen/src/generate.ts | 448 +++++++ ipc-codegen/src/naming.ts | 27 + ipc-codegen/src/rust_codegen.ts | 783 ++++++++++++ ipc-codegen/src/schema_visitor.ts | 234 ++++ ipc-codegen/src/typescript_codegen.ts | 662 ++++++++++ ipc-codegen/src/zig_codegen.ts | 651 ++++++++++ ipc-codegen/templates/cpp/ipc_client.hpp | 112 ++ ipc-codegen/templates/cpp/ipc_server.hpp | 252 ++++ ipc-codegen/templates/rust/backend.rs | 44 + ipc-codegen/templates/rust/error.rs | 32 + ipc-codegen/templates/rust/ffi_backend.rs | 163 +++ ipc-codegen/templates/rust/ipc_client.rs | 42 + ipc-codegen/templates/rust/ipc_server.rs | 51 + ipc-codegen/templates/rust/uds_backend.rs | 78 ++ ipc-codegen/templates/ts/ipc_client.ts | 58 + ipc-codegen/templates/ts/ipc_server.ts | 82 ++ ipc-codegen/templates/zig/backend.zig | 27 + ipc-codegen/templates/zig/ffi_backend.zig | 22 + ipc-codegen/templates/zig/ffi_client.zig | 21 + ipc-codegen/templates/zig/ipc_client.zig | 93 ++ ipc-codegen/templates/zig/ipc_server.zig | 112 ++ ipc-codegen/templates/zig/uds_backend.zig | 62 + 97 files changed, 9959 insertions(+), 2 deletions(-) create mode 100644 ipc-codegen/.gitignore create mode 100644 ipc-codegen/.rebuild_patterns create mode 100755 ipc-codegen/bootstrap.sh create mode 100644 ipc-codegen/examples/cpp/echo/echo_client.cpp create mode 100644 ipc-codegen/examples/cpp/echo/echo_server.cpp create mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp create mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp create mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp create mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_types.hpp create mode 100644 ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp create mode 100644 ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp create mode 100755 ipc-codegen/examples/echo-schema/generate.sh create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_bytes_request.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_bytes_response.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_fields_request.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_fields_response.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_nested_request.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_nested_response.msgpack create mode 100644 ipc-codegen/examples/echo-schema/schema.json create mode 100644 ipc-codegen/examples/rust/echo/Cargo.lock create mode 100644 ipc-codegen/examples/rust/echo/Cargo.toml create mode 100644 ipc-codegen/examples/rust/echo/src/bin/generate_golden.rs create mode 100644 ipc-codegen/examples/rust/echo/src/bin/golden_test.rs create mode 100644 ipc-codegen/examples/rust/echo/src/echo_client.rs create mode 100644 ipc-codegen/examples/rust/echo/src/echo_server.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/backend.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/echo_client.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/echo_server.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/echo_types.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/error.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs create mode 100644 ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs create mode 100644 ipc-codegen/examples/rust/echo/src/lib.rs create mode 100644 ipc-codegen/examples/rust/wsdb/Cargo.toml create mode 100644 ipc-codegen/examples/rust/wsdb/README.md create mode 100755 ipc-codegen/examples/rust/wsdb/generate.sh create mode 100644 ipc-codegen/examples/rust/wsdb/src/lib.rs create mode 100755 ipc-codegen/examples/scripts/run_cross_language_tests.sh create mode 100644 ipc-codegen/examples/ts/echo/echo_client.ts create mode 100644 ipc-codegen/examples/ts/echo/echo_server.ts create mode 100644 ipc-codegen/examples/ts/echo/generated/api_types.ts create mode 100644 ipc-codegen/examples/ts/echo/generated/async.ts create mode 100644 ipc-codegen/examples/ts/echo/generated/ipc_client.ts create mode 100644 ipc-codegen/examples/ts/echo/generated/ipc_server.ts create mode 100644 ipc-codegen/examples/ts/echo/generated/server.ts create mode 100644 ipc-codegen/examples/ts/echo/generated/sync.ts create mode 100644 ipc-codegen/examples/ts/echo/golden_test.ts create mode 100644 ipc-codegen/examples/ts/echo/package.json create mode 100644 ipc-codegen/examples/zig/echo/build.zig create mode 100644 ipc-codegen/examples/zig/echo/build.zig.zon create mode 100644 ipc-codegen/examples/zig/echo/echo_client.zig create mode 100644 ipc-codegen/examples/zig/echo/echo_server.zig create mode 100644 ipc-codegen/examples/zig/echo/generated/backend.zig create mode 100644 ipc-codegen/examples/zig/echo/generated/echo_client.zig create mode 100644 ipc-codegen/examples/zig/echo/generated/echo_server.zig create mode 100644 ipc-codegen/examples/zig/echo/generated/echo_types.zig create mode 100644 ipc-codegen/examples/zig/echo/generated/ipc_server.zig create mode 100644 ipc-codegen/examples/zig/echo/generated/uds_backend.zig create mode 100644 ipc-codegen/examples/zig/wsdb/.gitignore create mode 100644 ipc-codegen/examples/zig/wsdb/README.md create mode 100644 ipc-codegen/examples/zig/wsdb/build.zig create mode 100644 ipc-codegen/examples/zig/wsdb/build.zig.zon create mode 100755 ipc-codegen/examples/zig/wsdb/generate.sh create mode 100644 ipc-codegen/examples/zig/wsdb/src/main.zig create mode 100644 ipc-codegen/schemas/avm_schema.json create mode 100644 ipc-codegen/schemas/bb_curve_constants.json create mode 100644 ipc-codegen/schemas/bb_schema.json create mode 100644 ipc-codegen/schemas/cdb_schema.json create mode 100644 ipc-codegen/schemas/wsdb_schema.json create mode 100755 ipc-codegen/scripts/update_schemas.sh create mode 100755 ipc-codegen/scripts/validate_schemas.sh create mode 100644 ipc-codegen/src/README.md create mode 100644 ipc-codegen/src/SCHEMA_SPEC.md create mode 100644 ipc-codegen/src/cpp_codegen.ts create mode 100644 ipc-codegen/src/generate.ts create mode 100644 ipc-codegen/src/naming.ts create mode 100644 ipc-codegen/src/rust_codegen.ts create mode 100644 ipc-codegen/src/schema_visitor.ts create mode 100644 ipc-codegen/src/typescript_codegen.ts create mode 100644 ipc-codegen/src/zig_codegen.ts create mode 100644 ipc-codegen/templates/cpp/ipc_client.hpp create mode 100644 ipc-codegen/templates/cpp/ipc_server.hpp create mode 100644 ipc-codegen/templates/rust/backend.rs create mode 100644 ipc-codegen/templates/rust/error.rs create mode 100644 ipc-codegen/templates/rust/ffi_backend.rs create mode 100644 ipc-codegen/templates/rust/ipc_client.rs create mode 100644 ipc-codegen/templates/rust/ipc_server.rs create mode 100644 ipc-codegen/templates/rust/uds_backend.rs create mode 100644 ipc-codegen/templates/ts/ipc_client.ts create mode 100644 ipc-codegen/templates/ts/ipc_server.ts create mode 100644 ipc-codegen/templates/zig/backend.zig create mode 100644 ipc-codegen/templates/zig/ffi_backend.zig create mode 100644 ipc-codegen/templates/zig/ffi_client.zig create mode 100644 ipc-codegen/templates/zig/ipc_client.zig create mode 100644 ipc-codegen/templates/zig/ipc_server.zig create mode 100644 ipc-codegen/templates/zig/uds_backend.zig diff --git a/barretenberg/.gitignore b/barretenberg/.gitignore index ba04309b3eac..e7d2a9f1bcc7 100644 --- a/barretenberg/.gitignore +++ b/barretenberg/.gitignore @@ -9,7 +9,8 @@ cmake-build-debug *_opt.pil bench-out -# Generated code from msgpack schema (run `yarn generate` in ts/) +# Generated code (produced by ipc-codegen, not committed) +**/generated/ +# Legacy generated paths from the cbind pipeline (kept for any leftover from migration) rust/barretenberg-rs/src/generated_types.rs rust/barretenberg-rs/src/api.rs -ts/src/cbind/generated/ diff --git a/ipc-codegen/.gitignore b/ipc-codegen/.gitignore new file mode 100644 index 000000000000..fa3c40f8e2be --- /dev/null +++ b/ipc-codegen/.gitignore @@ -0,0 +1,7 @@ +# Local build artifacts produced by the echo wire-compat examples. +examples/cpp/*/echo_client +examples/cpp/*/echo_server +examples/rust/*/target/ +examples/zig/*/.zig-cache/ +examples/zig/*/zig-out/ +examples/ts/*/node_modules/ diff --git a/ipc-codegen/.rebuild_patterns b/ipc-codegen/.rebuild_patterns new file mode 100644 index 000000000000..010254c52d4a --- /dev/null +++ b/ipc-codegen/.rebuild_patterns @@ -0,0 +1,5 @@ +^ipc-codegen/src/.*\.ts$ +^ipc-codegen/schemas/.*\.(json|msgpack)$ +^ipc-codegen/templates/ +^ipc-codegen/package\.json$ +^ipc-codegen/bootstrap\.sh$ diff --git a/ipc-codegen/bootstrap.sh b/ipc-codegen/bootstrap.sh new file mode 100755 index 000000000000..892d84f01299 --- /dev/null +++ b/ipc-codegen/bootstrap.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Codegen tool: generates bindings from committed JSON schemas. +# Zero npm dependencies — runs with just Node.js (v22+). +# +# Usage: +# ./bootstrap.sh # Run codegen (generate all bindings) +# ./bootstrap.sh generate # Same +# ./bootstrap.sh test # Run the cross-language wire-compat test matrix +# ./bootstrap.sh hash # Print content hash + +source $(git rev-parse --show-toplevel)/ci3/source_bootstrap + +# Hash includes codegen source AND committed schema files. +export hash=$(cache_content_hash .rebuild_patterns) + +NODE_FLAGS="--experimental-strip-types --experimental-transform-types --no-warnings" + +gen() { node $NODE_FLAGS src/generate.ts "$@"; } + +function generate { + echo_header "codegen generate" + + # In this PR, only the echo example wires up codegen output. + # Service consumers (bb, wsdb, cdb, avm) are added by later PRs as they migrate + # from the legacy cbind generator. Until then, schemas live committed under + # schemas/ and the only consumer of codegen output is the echo test harness. + examples/echo-schema/generate.sh +} + +function test { + echo_header "codegen test" + examples/scripts/run_cross_language_tests.sh +} + +case "$cmd" in + ""|generate) + generate + ;; + test) + test + ;; + hash) + echo $hash + ;; + *) + default_cmd_handler "$@" + ;; +esac diff --git a/ipc-codegen/examples/cpp/echo/echo_client.cpp b/ipc-codegen/examples/cpp/echo/echo_client.cpp new file mode 100644 index 000000000000..2a7ef59434c7 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/echo_client.cpp @@ -0,0 +1,91 @@ +/** + * Echo IPC client (C++) — uses GENERATED types + IPC client template. + * Usage: echo_client --socket /tmp/echo.sock + * + * Note: The generated EchoIpcClient (.hpp/.cpp) depends on barretenberg + * msgpack headers which are not available in this standalone test context. + * Instead we build a thin client directly on the generated types + ipc_client. + */ + +#include "generated/echo_types.hpp" +#include "generated/ipc_client.hpp" + +// Need msgpack for serialization + barretenberg's SERIALIZATION_FIELDS adaptor +#ifndef THROW +#define THROW throw +#endif +#ifndef RETHROW +#define RETHROW throw +#endif +#include +#include "struct_map_impl.hpp" + +#include +#include +#include + +using namespace echo::wire; + +template +Resp call(ipc::IpcClient& client, Cmd&& cmd) { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); pk.pack_array(2); + pk.pack(std::string(Cmd::MSGPACK_SCHEMA_NAME)); + pk.pack(std::forward(cmd)); + + auto resp = client.call(std::vector(buf.data(), buf.data() + buf.size())); + auto oh = msgpack::unpack(reinterpret_cast(resp.data()), resp.size()); + auto obj = oh.get(); + std::string resp_name(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); + if (resp_name == "EchoErrorResponse") throw std::runtime_error("server error"); + Resp result; obj.via.array.ptr[1].convert(result); + return result; +} + +int main(int argc, char** argv) { + const char* socket_path = nullptr; + for (int i = 1; i < argc - 1; i++) { + if (std::string_view(argv[i]) == "--socket") socket_path = argv[i + 1]; + } + if (!socket_path) { std::cerr << "Usage: echo_client --socket \n"; return 1; } + + ipc::IpcClient client(socket_path); + + // EchoBytes + { + EchoBytes cmd{ .data = { 0xDE, 0xAD, 0xBE, 0xEF, 0x42 } }; + auto resp = call(client, std::move(cmd)); + assert((resp.data == std::vector{ 0xDE, 0xAD, 0xBE, 0xEF, 0x42 })); + std::cerr << "echo_client(cpp): EchoBytes OK\n"; + } + + // EchoFields + { + EchoFields cmd{ .a = 42, .b = 999999, .name = "hello wire compat" }; + auto resp = call(client, std::move(cmd)); + assert(resp.a == 42 && resp.b == 999999 && resp.name == "hello wire compat"); + std::cerr << "echo_client(cpp): EchoFields OK\n"; + } + + // EchoNested + { + EchoNested cmd{ .inner = { .values = { {1, 2, 3}, {4, 5} }, .flag = true } }; + auto resp = call(client, std::move(cmd)); + assert((resp.inner.values == std::vector>{ {1, 2, 3}, {4, 5} })); + assert(resp.inner.flag == true); + std::cerr << "echo_client(cpp): EchoNested OK\n"; + } + + // Shutdown + { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); pk.pack_array(2); + pk.pack(std::string("EchoShutdown")); pk.pack_map(0); + client.call(std::vector(buf.data(), buf.data() + buf.size())); + } + + std::cerr << "echo_client(cpp): all tests passed\n"; + return 0; +} diff --git a/ipc-codegen/examples/cpp/echo/echo_server.cpp b/ipc-codegen/examples/cpp/echo/echo_server.cpp new file mode 100644 index 000000000000..db60c102d7d5 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/echo_server.cpp @@ -0,0 +1,61 @@ +/** + * Echo IPC server (C++) — uses GENERATED dispatch + template Ctx. + * Usage: echo_server --socket /tmp/echo.sock + */ + +// barretenberg's custom msgpack adaptor for SERIALIZATION_FIELDS — +// enables msgpack::object::convert() to work with the generated types. +// Must be included before echo_ipc_server.hpp which uses convert()/pack(). +#include "generated/echo_types.hpp" +#ifndef THROW +#define THROW throw +#endif +#ifndef RETHROW +#define RETHROW throw +#endif +#include +#include "struct_map_impl.hpp" + +// The generated server header declares template handler functions. +// We need to see those declarations before providing specializations. +// Importantly, make_echo_handler() is defined inline but only instantiated +// when serve() is called in main() — so specializations defined after +// the header but before main() are visible at instantiation time. +#include "generated/echo_ipc_server.hpp" + +#include +#include + +namespace echo { + +struct EchoCtx {}; // empty context for the echo service + +// Template specializations — echo input fields back in response. +template <> +wire::EchoBytesResponse handle_bytes(EchoCtx& /*ctx*/, wire::EchoBytes&& cmd) { + return { .data = std::move(cmd.data) }; +} + +template <> +wire::EchoFieldsResponse handle_fields(EchoCtx& /*ctx*/, wire::EchoFields&& cmd) { + return { .a = cmd.a, .b = cmd.b, .name = std::move(cmd.name) }; +} + +template <> +wire::EchoNestedResponse handle_nested(EchoCtx& /*ctx*/, wire::EchoNested&& cmd) { + return { .inner = std::move(cmd.inner) }; +} + +} // namespace echo + +int main(int argc, char** argv) { + const char* socket_path = nullptr; + for (int i = 1; i < argc - 1; i++) { + if (std::string_view(argv[i]) == "--socket") socket_path = argv[i + 1]; + } + if (!socket_path) { std::cerr << "Usage: echo_server --socket \n"; return 1; } + + echo::EchoCtx ctx; + echo::serve(socket_path, ctx); + return 0; +} diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp new file mode 100644 index 000000000000..c12d0571d2f8 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp @@ -0,0 +1,88 @@ +// AUTOGENERATED FILE - DO NOT EDIT + +#include "echo/echo_ipc_client.hpp" +#include "barretenberg/serialize/msgpack.hpp" +#include "barretenberg/serialize/msgpack_impl.hpp" + +#include +#include + +namespace echo { + +EchoIpcClient::EchoIpcClient(const std::string &socket_path) + : client_(std::make_unique<::ipc::IpcClient>(socket_path.c_str())) {} + +EchoIpcClient::~EchoIpcClient() = default; + +template +Resp EchoIpcClient::send(Cmd &&cmd) const { + // Serialize as [[CommandName, {payload}]] + msgpack::sbuffer send_buffer; + msgpack::packer pk(send_buffer); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(Cmd::MSGPACK_SCHEMA_NAME)); + pk.pack(std::forward(cmd)); + + // Send request, receive response + std::vector request_bytes(send_buffer.data(), + send_buffer.data() + send_buffer.size()); + auto response_bytes = client_->call(request_bytes); + + if (response_bytes.empty()) { + throw std::runtime_error("Empty response from server"); + } + + // Parse response: [ResponseName, {payload}] + auto unpacked = + msgpack::unpack(reinterpret_cast(response_bytes.data()), + response_bytes.size()); + auto obj = unpacked.get(); + + if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 2 || + obj.via.array.ptr[0].type != msgpack::type::STR) { + throw std::runtime_error("Invalid response format from server"); + } + + std::string resp_name(obj.via.array.ptr[0].via.str.ptr, + obj.via.array.ptr[0].via.str.size); + if (resp_name == "EchoErrorResponse") { + std::string message; + auto &payload = obj.via.array.ptr[1]; + // Extract message field from the error map + if (payload.type == msgpack::type::MAP) { + for (uint32_t i = 0; i < payload.via.map.size; ++i) { + auto &kv = payload.via.map.ptr[i]; + if (kv.key.type == msgpack::type::STR) { + std::string key(kv.key.via.str.ptr, kv.key.via.str.size); + if (key == "message" && kv.val.type == msgpack::type::STR) { + message = std::string(kv.val.via.str.ptr, kv.val.via.str.size); + } + } + } + } + throw std::runtime_error("Server error: " + message); + } + + Resp result; + obj.via.array.ptr[1].convert(result); + return result; +} + +EchoBytesResponse EchoIpcClient::bytes(EchoBytes cmd) const { + return send(std::move(cmd)); +} + +EchoFieldsResponse EchoIpcClient::fields(EchoFields cmd) const { + return send(std::move(cmd)); +} + +EchoNestedResponse EchoIpcClient::nested(EchoNested cmd) const { + return send(std::move(cmd)); +} + +void EchoIpcClient::shutdown() { + send(EchoShutdown{}); +} + +} // namespace echo diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp new file mode 100644 index 000000000000..28d680093425 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp @@ -0,0 +1,43 @@ +// AUTOGENERATED FILE - DO NOT EDIT +#pragma once + +#include "echo/echo_types.hpp" +#include "echo/ipc_client.hpp" +// clang-format on + +#include +#include + +namespace echo { +using namespace wire; + +/** Schema version hash for compatibility checking */ +static constexpr const char SCHEMA_HASH[] = + "bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc"; + +/** + * @brief Auto-generated IPC client. + * + * Each method sends a msgpack-serialized command to the server over UDS + * and returns the typed response. All methods block until the response arrives. + */ +class EchoIpcClient { +public: + explicit EchoIpcClient(const std::string &socket_path); + ~EchoIpcClient(); + + EchoIpcClient(const EchoIpcClient &) = delete; + EchoIpcClient &operator=(const EchoIpcClient &) = delete; + + EchoBytesResponse bytes(EchoBytes cmd) const; + EchoFieldsResponse fields(EchoFields cmd) const; + EchoNestedResponse nested(EchoNested cmd) const; + void shutdown(); + +private: + template Resp send(Cmd &&cmd) const; + + mutable std::unique_ptr<::ipc::IpcClient> client_; +}; + +} // namespace echo diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp new file mode 100644 index 000000000000..d24464543fb9 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp @@ -0,0 +1,165 @@ +// AUTOGENERATED FILE - DO NOT EDIT +// Header-only server dispatch — template for service context. +#pragma once + +#include "echo_types.hpp" +#include "ipc_server.hpp" + +// msgpack headers needed for dispatch implementation. +// Includers within barretenberg must include try_catch_shim.hpp before this +// header. +#ifndef THROW +#define THROW throw +#define RETHROW throw +#endif +#include + +#include +#include +#include +#include +#include +#include + +namespace echo { + +// Wire types are in the 'wire' sub-namespace (from echo_types.hpp) +// Handler declarations — implement these in your handler file. +// Template specializations must be visible before make_handler() is +// instantiated. + +template +wire::EchoBytesResponse handle_bytes(Ctx &ctx, wire::EchoBytes &&cmd); + +template +wire::EchoFieldsResponse handle_fields(Ctx &ctx, wire::EchoFields &&cmd); + +template +wire::EchoNestedResponse handle_nested(Ctx &ctx, wire::EchoNested &&cmd); + +// --------------------------------------------------------------------------- +// Dispatch — template on service context type +// --------------------------------------------------------------------------- + +namespace detail { + +inline std::vector make_error(const std::string &message) { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("EchoErrorResponse")); + pk.pack_map(1); + pk.pack(std::string("message")); + pk.pack(message); + return std::vector(buf.data(), buf.data() + buf.size()); +} + +} // namespace detail + +template ::ipc::Handler make_echo_handler(Ctx &ctx) { + using HandlerFn = + std::function(Ctx &, const msgpack::object &)>; + static const std::unordered_map table = { + {"EchoBytes", + [](Ctx &ctx, [[maybe_unused]] const msgpack::object &payload) + -> std::vector { + wire::EchoBytes wire_cmd; + payload.convert(wire_cmd); + auto wire_resp = handle_bytes(ctx, std::move(wire_cmd)); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("EchoBytesResponse")); + pk.pack(wire_resp); + return std::vector(buf.data(), buf.data() + buf.size()); + }}, + {"EchoFields", + [](Ctx &ctx, [[maybe_unused]] const msgpack::object &payload) + -> std::vector { + wire::EchoFields wire_cmd; + payload.convert(wire_cmd); + auto wire_resp = handle_fields(ctx, std::move(wire_cmd)); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("EchoFieldsResponse")); + pk.pack(wire_resp); + return std::vector(buf.data(), buf.data() + buf.size()); + }}, + {"EchoNested", + [](Ctx &ctx, [[maybe_unused]] const msgpack::object &payload) + -> std::vector { + wire::EchoNested wire_cmd; + payload.convert(wire_cmd); + auto wire_resp = handle_nested(ctx, std::move(wire_cmd)); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("EchoNestedResponse")); + pk.pack(wire_resp); + return std::vector(buf.data(), buf.data() + buf.size()); + }}, + {"EchoShutdown", + []([[maybe_unused]] Ctx &ctx, + [[maybe_unused]] const msgpack::object &payload) + -> std::vector { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("EchoShutdownResponse")); + pk.pack_map(0); + THROW ::ipc::ShutdownRequested( + std::vector(buf.data(), buf.data() + buf.size())); + }}, + }; + + return + [&ctx](const std::vector &raw_request) -> std::vector { + auto unpacked = + msgpack::unpack(reinterpret_cast(raw_request.data()), + raw_request.size()); + auto obj = unpacked.get(); + + if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 1) { + std::cerr << "Error: Expected array of size 1\n"; + return {}; + } + + auto &inner = obj.via.array.ptr[0]; + if (inner.type != msgpack::type::ARRAY || inner.via.array.size != 2 || + inner.via.array.ptr[0].type != msgpack::type::STR) { + std::cerr << "Error: Expected [CommandName, {payload}]\n"; + return {}; + } + + std::string cmd_name(inner.via.array.ptr[0].via.str.ptr, + inner.via.array.ptr[0].via.str.size); + auto &cmd_payload = inner.via.array.ptr[1]; + + auto it = table.find(cmd_name); + if (it == table.end()) { + return detail::make_error("unknown command: " + cmd_name); + } +#ifdef BB_NO_EXCEPTIONS + return it->second(ctx, cmd_payload); +#else + try { + return it->second(ctx, cmd_payload); + } catch (const ::ipc::ShutdownRequested &) { + throw; + } catch (const std::exception &e) { + std::cerr << "Error processing " << cmd_name << ": " << e.what() + << '\n'; + return detail::make_error(e.what()); + } +#endif + }; +} + +template +void serve(const char *socket_path, Ctx &ctx, + std::atomic *shutdown_flag = nullptr) { + ::ipc::serve(socket_path, make_echo_handler(ctx), shutdown_flag); +} + +} // namespace echo diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_types.hpp b/ipc-codegen/examples/cpp/echo/generated/echo_types.hpp new file mode 100644 index 000000000000..6e67fd1218a9 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/generated/echo_types.hpp @@ -0,0 +1,134 @@ +// AUTOGENERATED FILE - DO NOT EDIT +// Standalone types for Echo service. +#pragma once + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Self-contained serialization macro. +// Defines a msgpack() method that enumerates field name/value pairs. +// Works with msgpack packers (serialization) and schema reflectors. +// If barretenberg's SERIALIZATION_FIELDS is already available, use it instead. +// --------------------------------------------------------------------------- +#ifndef SERIALIZATION_FIELDS +#define _SF_E1(x) #x, x +#define _SF_E2(x, ...) #x, x, _SF_E1(__VA_ARGS__) +#define _SF_E3(x, ...) #x, x, _SF_E2(__VA_ARGS__) +#define _SF_E4(x, ...) #x, x, _SF_E3(__VA_ARGS__) +#define _SF_E5(x, ...) #x, x, _SF_E4(__VA_ARGS__) +#define _SF_E6(x, ...) #x, x, _SF_E5(__VA_ARGS__) +#define _SF_E7(x, ...) #x, x, _SF_E6(__VA_ARGS__) +#define _SF_E8(x, ...) #x, x, _SF_E7(__VA_ARGS__) +#define _SF_E9(x, ...) #x, x, _SF_E8(__VA_ARGS__) +#define _SF_E10(x, ...) #x, x, _SF_E9(__VA_ARGS__) +#define _SF_E11(x, ...) #x, x, _SF_E10(__VA_ARGS__) +#define _SF_E12(x, ...) #x, x, _SF_E11(__VA_ARGS__) +#define _SF_E13(x, ...) #x, x, _SF_E12(__VA_ARGS__) +#define _SF_E14(x, ...) #x, x, _SF_E13(__VA_ARGS__) +#define _SF_E15(x, ...) #x, x, _SF_E14(__VA_ARGS__) +#define _SF_E16(x, ...) #x, x, _SF_E15(__VA_ARGS__) +#define _SF_E17(x, ...) #x, x, _SF_E16(__VA_ARGS__) +#define _SF_E18(x, ...) #x, x, _SF_E17(__VA_ARGS__) +#define _SF_E19(x, ...) #x, x, _SF_E18(__VA_ARGS__) +#define _SF_E20(x, ...) #x, x, _SF_E19(__VA_ARGS__) +#define _SF_CNT(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, \ + _15, _16, _17, _18, _19, _20, N, ...) \ + N +#define _SF_NUM(...) \ + _SF_CNT(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, \ + 5, 4, 3, 2, 1) +#define _SF_CAT(a, b) a##b +#define _SF_SEL(n) _SF_CAT(_SF_E, n) +#define _SF_NVP(...) _SF_SEL(_SF_NUM(__VA_ARGS__))(__VA_ARGS__) +#define SERIALIZATION_FIELDS(...) \ + template void msgpack(_PackFn pack_fn) { \ + pack_fn(_SF_NVP(__VA_ARGS__)); \ + } +#endif + +/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. +using Fr = std::array; + +namespace echo::wire { + +struct EchoInner { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoInner"; + std::vector> values; + std::optional flag; + SERIALIZATION_FIELDS(values, flag) + bool operator==(const EchoInner &) const = default; +}; + +struct EchoBytes { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoBytes"; + std::vector data; + SERIALIZATION_FIELDS(data) + bool operator==(const EchoBytes &) const = default; +}; + +struct EchoFields { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoFields"; + uint32_t a; + uint64_t b; + std::string name; + SERIALIZATION_FIELDS(a, b, name) + bool operator==(const EchoFields &) const = default; +}; + +struct EchoNested { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoNested"; + EchoInner inner; + SERIALIZATION_FIELDS(inner) + bool operator==(const EchoNested &) const = default; +}; + +struct EchoShutdown { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoShutdown"; + + template void msgpack(_PackFn &&pack_fn) { pack_fn(); } + bool operator==(const EchoShutdown &) const = default; +}; + +struct EchoBytesResponse { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoBytesResponse"; + std::vector data; + SERIALIZATION_FIELDS(data) + bool operator==(const EchoBytesResponse &) const = default; +}; + +struct EchoFieldsResponse { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoFieldsResponse"; + uint32_t a; + uint64_t b; + std::string name; + SERIALIZATION_FIELDS(a, b, name) + bool operator==(const EchoFieldsResponse &) const = default; +}; + +struct EchoNestedResponse { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoNestedResponse"; + EchoInner inner; + SERIALIZATION_FIELDS(inner) + bool operator==(const EchoNestedResponse &) const = default; +}; + +struct EchoShutdownResponse { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoShutdownResponse"; + + template void msgpack(_PackFn &&pack_fn) { pack_fn(); } + bool operator==(const EchoShutdownResponse &) const = default; +}; + +struct EchoErrorResponse { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoErrorResponse"; + std::string message; + SERIALIZATION_FIELDS(message) + bool operator==(const EchoErrorResponse &) const = default; +}; + +} // namespace echo::wire diff --git a/ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp b/ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp new file mode 100644 index 000000000000..4e3db6c204c6 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp @@ -0,0 +1,112 @@ +/** + * Generic IPC client over Unix Domain Sockets. + * Handles: socket connect, length-prefixed framing, send/receive raw bytes. + * Header-only. + */ +#pragma once +#ifndef IPC_CLIENT_HPP_INCLUDED +#define IPC_CLIENT_HPP_INCLUDED + +#ifndef THROW +#define THROW throw +#endif +#ifndef RETHROW +#define RETHROW throw +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace ipc { + +class IpcClient { + public: + explicit IpcClient(const char* socket_path) + { + fd_ = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (fd_ < 0) { + throw std::runtime_error(std::string("socket() failed: ") + strerror(errno)); + } + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + if (::connect(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(fd_); + fd_ = -1; + throw std::runtime_error(std::string("connect() failed: ") + strerror(errno)); + } + } + + ~IpcClient() + { + if (fd_ >= 0) { + ::close(fd_); + } + } + + IpcClient(const IpcClient&) = delete; + IpcClient& operator=(const IpcClient&) = delete; + + std::vector call(const std::vector& request) + { + // Send length-prefixed request + uint32_t len = static_cast(request.size()); + uint8_t header[4] = { + static_cast(len & 0xFF), + static_cast((len >> 8) & 0xFF), + static_cast((len >> 16) & 0xFF), + static_cast((len >> 24) & 0xFF), + }; + write_all(header, 4); + write_all(request.data(), request.size()); + + // Receive length-prefixed response + uint8_t resp_hdr[4]; + read_all(resp_hdr, 4); + uint32_t resp_len = static_cast(resp_hdr[0]) | (static_cast(resp_hdr[1]) << 8) | + (static_cast(resp_hdr[2]) << 16) | (static_cast(resp_hdr[3]) << 24); + std::vector resp(resp_len); + read_all(resp.data(), resp_len); + return resp; + } + + private: + void write_all(const void* data, size_t len) + { + const auto* ptr = static_cast(data); + size_t written = 0; + while (written < len) { + auto n = ::write(fd_, ptr + written, len - written); + if (n <= 0) { + throw std::runtime_error("write failed"); + } + written += static_cast(n); + } + } + + void read_all(void* data, size_t len) + { + auto* ptr = static_cast(data); + size_t got = 0; + while (got < len) { + auto n = ::read(fd_, ptr + got, len - got); + if (n <= 0) { + throw std::runtime_error("read failed"); + } + got += static_cast(n); + } + } + + int fd_ = -1; +}; + +} // namespace ipc +#endif // IPC_CLIENT_HPP_INCLUDED diff --git a/ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp b/ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp new file mode 100644 index 000000000000..d57ad50e7b64 --- /dev/null +++ b/ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp @@ -0,0 +1,252 @@ +/** + * Generic IPC server over Unix Domain Sockets. + * Handles: socket setup, multi-client accept via poll(), length-prefixed framing. + * Service-specific dispatch is injected via the handler function parameter. + * + * Does NOT handle signal handling or parent death monitoring — those are + * the responsibility of the binary that calls serve(). + * + * Header-only — no separate .cpp needed. + */ +#pragma once +#ifndef IPC_SERVER_HPP_INCLUDED +#define IPC_SERVER_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include + +#if defined(__wasm__) +// UDS not available in WASM — provide stub types only +namespace ipc { +struct ShutdownRequested : std::exception { + std::vector final_response; + explicit ShutdownRequested(std::vector resp) : final_response(std::move(resp)) {} + const char* what() const noexcept override { return "shutdown requested"; } +}; +using Handler = std::function(const std::vector&)>; +inline void serve(const char*, Handler, std::atomic* = nullptr, int = 5) {} +} // namespace ipc +#else + +#ifndef THROW +#define THROW throw +#endif +#ifndef RETHROW +#define RETHROW throw +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ipc { + +/// Exception thrown by handlers to trigger graceful shutdown. +/// Carries the final response to send before closing. +struct ShutdownRequested : std::exception { + std::vector final_response; + explicit ShutdownRequested(std::vector resp) + : final_response(std::move(resp)) + {} + const char* what() const noexcept override { return "shutdown requested"; } +}; + +using Handler = std::function(const std::vector&)>; + +// --------------------------------------------------------------------------- +// Framing: 4-byte little-endian length prefix +// --------------------------------------------------------------------------- + +inline bool send_frame(int fd, const std::vector& data) +{ + uint32_t len = static_cast(data.size()); + uint8_t header[4] = { + static_cast(len & 0xFF), + static_cast((len >> 8) & 0xFF), + static_cast((len >> 16) & 0xFF), + static_cast((len >> 24) & 0xFF), + }; + // Write header + size_t written = 0; + while (written < 4) { + auto n = ::write(fd, header + written, 4 - written); + if (n <= 0) { + return false; + } + written += static_cast(n); + } + // Write payload + written = 0; + while (written < data.size()) { + auto n = ::write(fd, data.data() + written, data.size() - written); + if (n <= 0) { + return false; + } + written += static_cast(n); + } + return true; +} + +/// Returns empty vector on EOF/error. +inline std::vector recv_frame(int fd) +{ + uint8_t header[4]; + size_t got = 0; + while (got < 4) { + auto n = ::read(fd, header + got, 4 - got); + if (n <= 0) { + return {}; + } + got += static_cast(n); + } + uint32_t len = static_cast(header[0]) | (static_cast(header[1]) << 8) | + (static_cast(header[2]) << 16) | (static_cast(header[3]) << 24); + std::vector buf(len); + got = 0; + while (got < len) { + auto n = ::read(fd, buf.data() + got, len - got); + if (n <= 0) { + return {}; + } + got += static_cast(n); + } + return buf; +} + +// --------------------------------------------------------------------------- +// Multi-client UDS server +// --------------------------------------------------------------------------- + +/** + * @brief Run a multi-client UDS server. + * + * Accepts multiple client connections via poll(). Handles one request at a time + * (sequential, not concurrent). When a handler throws ShutdownRequested, the + * final response is sent and the server exits cleanly. + * + * The caller should set up signal handlers and parent death monitoring before + * calling this function. To request external shutdown, store true into the + * provided shutdown_flag (or pass nullptr to disable external shutdown). + * + * @param socket_path Path for the Unix domain socket + * @param handler Function that processes a request and returns a response + * @param shutdown_flag Atomic flag checked each poll cycle; serve() exits when true. + * May be nullptr if only ShutdownRequested is used. + * @param backlog listen() backlog (max pending connections) + */ +inline void serve(const char* socket_path, + Handler handler, + std::atomic* shutdown_flag = nullptr, + int backlog = 5) +{ + unlink(socket_path); + int server_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd < 0) { + throw std::runtime_error(std::string("socket() failed: ") + strerror(errno)); + } + + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(server_fd); + throw std::runtime_error(std::string("bind() failed: ") + strerror(errno)); + } + if (::listen(server_fd, backlog) < 0) { + ::close(server_fd); + throw std::runtime_error(std::string("listen() failed: ") + strerror(errno)); + } + + // Poll set: [0] = server_fd, [1..N] = client fds + std::vector fds; + fds.push_back({ server_fd, POLLIN, 0 }); + + auto remove_client = [&](size_t idx) { + ::close(fds[idx].fd); + fds.erase(fds.begin() + static_cast(idx)); + }; + + auto should_shutdown = [&]() { + return shutdown_flag != nullptr && shutdown_flag->load(std::memory_order_acquire); + }; + + while (!should_shutdown()) { + int ready = ::poll(fds.data(), static_cast(fds.size()), 100 /* ms */); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (ready == 0) { + continue; + } + + // Check server fd for new connections + if (fds[0].revents & POLLIN) { + int client_fd = ::accept(server_fd, nullptr, nullptr); + if (client_fd >= 0) { + fds.push_back({ client_fd, POLLIN, 0 }); + } + } + + // Check client fds for data + for (size_t i = 1; i < fds.size(); /* incremented below */) { + if (!(fds[i].revents & POLLIN)) { + ++i; + continue; + } + + auto payload = recv_frame(fds[i].fd); + if (payload.empty()) { + // Client disconnected + remove_client(i); + continue; + } + + try { + auto response = handler(payload); + if (!send_frame(fds[i].fd, response)) { + remove_client(i); + continue; + } + } catch (const ShutdownRequested& shutdown) { + send_frame(fds[i].fd, shutdown.final_response); + goto done; + } catch (const std::exception& e) { + std::cerr << "ipc-server: handler error: " << e.what() << "\n"; + remove_client(i); + continue; + } + ++i; + } + } + +done: + // Close all client connections + for (size_t i = 1; i < fds.size(); ++i) { + ::close(fds[i].fd); + } + ::close(server_fd); + unlink(socket_path); +} + +} // namespace ipc +#endif // !defined(__wasm__) +#endif // IPC_SERVER_HPP_INCLUDED diff --git a/ipc-codegen/examples/echo-schema/generate.sh b/ipc-codegen/examples/echo-schema/generate.sh new file mode 100755 index 000000000000..31ae2bf2996c --- /dev/null +++ b/ipc-codegen/examples/echo-schema/generate.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Generate echo service types for all 4 languages from schema.json. +# Uses the codegen CLI — same as any service developer would. +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +EXAMPLES="$(cd "$DIR/.." && pwd)" +CODEGEN="$(cd "$DIR/../.." && pwd)" +NODE="node --experimental-strip-types --experimental-transform-types --no-warnings" +GEN="$CODEGEN/src/generate.ts" +SCHEMA="$DIR/schema.json" + +echo "Generating echo types from $SCHEMA" + +$NODE "$GEN" --schema "$SCHEMA" --lang cpp --server --client --uds --out "$EXAMPLES/cpp/echo/generated" --prefix Echo --cpp-namespace echo --cpp-include-dir echo +$NODE "$GEN" --schema "$SCHEMA" --lang ts --server --client --uds --out "$EXAMPLES/ts/echo/generated" --prefix Echo +$NODE "$GEN" --schema "$SCHEMA" --lang rust --server --client --uds --out "$EXAMPLES/rust/echo/src/generated" --prefix Echo +$NODE "$GEN" --schema "$SCHEMA" --lang zig --server --client --uds --out "$EXAMPLES/zig/echo/generated" --prefix Echo + +echo "Done." diff --git a/ipc-codegen/examples/echo-schema/golden/echo_bytes_request.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_bytes_request.msgpack new file mode 100644 index 000000000000..fbd6f3e3f32d --- /dev/null +++ b/ipc-codegen/examples/echo-schema/golden/echo_bytes_request.msgpack @@ -0,0 +1 @@ +EchoBytesdataޭB \ No newline at end of file diff --git a/ipc-codegen/examples/echo-schema/golden/echo_bytes_response.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_bytes_response.msgpack new file mode 100644 index 000000000000..f15b07218068 --- /dev/null +++ b/ipc-codegen/examples/echo-schema/golden/echo_bytes_response.msgpack @@ -0,0 +1 @@ +EchoBytesResponsedataޭB \ No newline at end of file diff --git a/ipc-codegen/examples/echo-schema/golden/echo_fields_request.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_fields_request.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..d72172943bc061a39526e1f5f3734702a5d546ec GIT binary patch literal 47 zcmbO@X_aeoM!s8SYEDXV^TI@}g-Pca_?_&R = std::env::args().collect(); + let output_dir = args.iter() + .position(|a| a == "--output-dir") + .and_then(|i| args.get(i + 1)) + .expect("Usage: generate_golden --output-dir "); + + fs::create_dir_all(output_dir).unwrap(); + + // Request format: [command] — serialized as Vec (1-element array) + write_golden(output_dir, "echo_bytes_request.msgpack", &vec![ + Command::EchoBytes(EchoBytes::new(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42])) + ]); + + write_golden(output_dir, "echo_fields_request.msgpack", &vec![ + Command::EchoFields(EchoFields::new(42, 999999, "hello wire compat".to_string())) + ]); + + write_golden(output_dir, "echo_nested_request.msgpack", &vec![ + Command::EchoNested(EchoNested::new(EchoInner { + values: vec![vec![1, 2, 3], vec![4, 5]], + flag: Some(true), + })) + ]); + + // Response format: NamedUnion (no tuple wrapper) + write_golden(output_dir, "echo_bytes_response.msgpack", + &Response::EchoBytesResponse(EchoBytesResponse { + data: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42], + })); + + write_golden(output_dir, "echo_fields_response.msgpack", + &Response::EchoFieldsResponse(EchoFieldsResponse { + a: 42, + b: 999999, + name: "hello wire compat".to_string(), + })); + + write_golden(output_dir, "echo_nested_response.msgpack", + &Response::EchoNestedResponse(EchoNestedResponse { + inner: EchoInner { + values: vec![vec![1, 2, 3], vec![4, 5]], + flag: Some(true), + }, + })); + + eprintln!("Generated 6 golden files in {}", output_dir); +} + +fn write_golden(dir: &str, name: &str, value: &T) { + let bytes = rmp_serde::to_vec_named(value).unwrap(); + let path = Path::new(dir).join(name); + fs::write(&path, &bytes).unwrap(); + eprintln!(" {} ({} bytes)", name, bytes.len()); +} diff --git a/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs b/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs new file mode 100644 index 000000000000..3f52379ceb6a --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs @@ -0,0 +1,109 @@ +//! Golden file deserialization test (Rust). +//! Verifies Rust can deserialize the golden msgpack files. +//! Usage: golden_test --golden-dir golden/ + +use echo_wire_compat::types_gen::*; +use std::fs; + +fn main() { + let args: Vec = std::env::args().collect(); + let golden_dir = args.iter() + .position(|a| a == "--golden-dir") + .and_then(|i| args.get(i + 1)) + .expect("Usage: golden_test --golden-dir "); + + let mut pass = 0; + let mut fail = 0; + + // Request golden files + match check_request::(golden_dir, "echo_bytes_request.msgpack") { + Ok(cmd) => { + assert_eq!(cmd.data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]); + eprintln!(" PASS: echo_bytes_request.msgpack"); + pass += 1; + } + Err(e) => { eprintln!(" FAIL: echo_bytes_request.msgpack: {e}"); fail += 1; } + } + + match check_request::(golden_dir, "echo_fields_request.msgpack") { + Ok(cmd) => { + assert_eq!(cmd.a, 42); + assert_eq!(cmd.b, 999999); + assert_eq!(cmd.name, "hello wire compat"); + eprintln!(" PASS: echo_fields_request.msgpack"); + pass += 1; + } + Err(e) => { eprintln!(" FAIL: echo_fields_request.msgpack: {e}"); fail += 1; } + } + + match check_request::(golden_dir, "echo_nested_request.msgpack") { + Ok(cmd) => { + assert_eq!(cmd.inner.values, vec![vec![1u8, 2, 3], vec![4, 5]]); + assert_eq!(cmd.inner.flag, Some(true)); + eprintln!(" PASS: echo_nested_request.msgpack"); + pass += 1; + } + Err(e) => { eprintln!(" FAIL: echo_nested_request.msgpack: {e}"); fail += 1; } + } + + // Response golden files + match check_response(golden_dir, "echo_bytes_response.msgpack") { + Ok(Response::EchoBytesResponse(r)) => { + assert_eq!(r.data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]); + eprintln!(" PASS: echo_bytes_response.msgpack"); + pass += 1; + } + Ok(_) => { eprintln!(" FAIL: echo_bytes_response.msgpack: wrong variant"); fail += 1; } + Err(e) => { eprintln!(" FAIL: echo_bytes_response.msgpack: {e}"); fail += 1; } + } + + match check_response(golden_dir, "echo_fields_response.msgpack") { + Ok(Response::EchoFieldsResponse(r)) => { + assert_eq!(r.a, 42); + assert_eq!(r.b, 999999); + assert_eq!(r.name, "hello wire compat"); + eprintln!(" PASS: echo_fields_response.msgpack"); + pass += 1; + } + Ok(_) => { eprintln!(" FAIL: echo_fields_response.msgpack: wrong variant"); fail += 1; } + Err(e) => { eprintln!(" FAIL: echo_fields_response.msgpack: {e}"); fail += 1; } + } + + match check_response(golden_dir, "echo_nested_response.msgpack") { + Ok(Response::EchoNestedResponse(r)) => { + assert_eq!(r.inner.values, vec![vec![1u8, 2, 3], vec![4, 5]]); + assert_eq!(r.inner.flag, Some(true)); + eprintln!(" PASS: echo_nested_response.msgpack"); + pass += 1; + } + Ok(_) => { eprintln!(" FAIL: echo_nested_response.msgpack: wrong variant"); fail += 1; } + Err(e) => { eprintln!(" FAIL: echo_nested_response.msgpack: {e}"); fail += 1; } + } + + eprintln!("\nResults: {pass}/{} passed, {fail} failed", pass + fail); + if fail > 0 { std::process::exit(1); } +} + +fn check_request(dir: &str, name: &str) -> Result { + let path = format!("{dir}/{name}"); + let data = fs::read(&path).map_err(|e| format!("read {path}: {e}"))?; + // Request format: [Command] — deserialized as Vec + let commands: Vec = rmp_serde::from_slice(&data) + .map_err(|e| format!("deserialize: {e}"))?; + let command = commands.into_iter().next().ok_or("empty")?; + // Extract the specific variant + // We need to serialize the inner value back to msgpack and then deserialize as T + let inner_bytes = match command { + Command::EchoBytes(v) => rmp_serde::to_vec_named(&v).unwrap(), + Command::EchoFields(v) => rmp_serde::to_vec_named(&v).unwrap(), + Command::EchoNested(v) => rmp_serde::to_vec_named(&v).unwrap(), + Command::EchoShutdown(v) => rmp_serde::to_vec_named(&v).unwrap(), + }; + rmp_serde::from_slice(&inner_bytes).map_err(|e| format!("re-deserialize: {e}")) +} + +fn check_response(dir: &str, name: &str) -> Result { + let path = format!("{dir}/{name}"); + let data = fs::read(&path).map_err(|e| format!("read {path}: {e}"))?; + rmp_serde::from_slice(&data).map_err(|e| format!("deserialize: {e}")) +} diff --git a/ipc-codegen/examples/rust/echo/src/echo_client.rs b/ipc-codegen/examples/rust/echo/src/echo_client.rs new file mode 100644 index 000000000000..31928ce5c106 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/echo_client.rs @@ -0,0 +1,47 @@ +//! Echo IPC client — uses GENERATED typed client (EchoApi) + UDS backend. +//! Usage: echo_client --socket /tmp/echo.sock +//! Exits 0 on success, 1 on failure. + +use echo_wire_compat::generated::echo_client::EchoApi; +use echo_wire_compat::generated::echo_types::EchoInner; +use echo_wire_compat::generated::error::Result; +use echo_wire_compat::generated::uds_backend::UdsBackend; + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + let socket_path = args.iter() + .position(|a| a == "--socket") + .and_then(|i| args.get(i + 1)) + .expect("Usage: echo_client --socket "); + + let backend = UdsBackend::connect(socket_path)?; + let mut client = EchoApi::new(backend); + + // Test 1: EchoBytes + let test_data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]; + let resp = client.bytes(&test_data)?; + assert_eq!(resp.data, test_data, "EchoBytes data mismatch"); + eprintln!("echo_client(rust): EchoBytes OK"); + + // Test 2: EchoFields + let resp = client.fields(42, 999999, "hello wire compat".to_string())?; + assert_eq!(resp.a, 42); + assert_eq!(resp.b, 999999); + assert_eq!(resp.name, "hello wire compat"); + eprintln!("echo_client(rust): EchoFields OK"); + + // Test 3: EchoNested + let inner = EchoInner { + values: vec![vec![1, 2, 3], vec![4, 5]], + flag: Some(true), + }; + let resp = client.nested(inner.clone())?; + assert_eq!(resp.inner.values, inner.values); + assert_eq!(resp.inner.flag, inner.flag); + eprintln!("echo_client(rust): EchoNested OK"); + + // Shutdown + client.shutdown()?; + eprintln!("echo_client(rust): all tests passed"); + Ok(()) +} diff --git a/ipc-codegen/examples/rust/echo/src/echo_server.rs b/ipc-codegen/examples/rust/echo/src/echo_server.rs new file mode 100644 index 000000000000..0d63b0d54820 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/echo_server.rs @@ -0,0 +1,71 @@ +//! Echo IPC server — uses GENERATED dispatch + server template + types. +//! Usage: echo_server --socket /tmp/echo.sock + +use echo_wire_compat::generated::echo_server::Handler; +use echo_wire_compat::generated::echo_types::*; +use echo_wire_compat::generated::error::{EchoError, Result}; +use echo_wire_compat::generated::ipc_server; +use std::cell::RefCell; + +struct EchoHandler; + +impl Handler for EchoHandler { + fn bytes(&mut self, cmd: EchoBytes) -> Result { + Ok(EchoBytesResponse { data: cmd.data }) + } + fn fields(&mut self, cmd: EchoFields) -> Result { + Ok(EchoFieldsResponse { a: cmd.a, b: cmd.b, name: cmd.name }) + } + fn nested(&mut self, cmd: EchoNested) -> Result { + Ok(EchoNestedResponse { inner: cmd.inner }) + } +} + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + let socket_path = args.iter() + .position(|a| a == "--socket") + .and_then(|i| args.get(i + 1)) + .expect("Usage: echo_server --socket "); + + let _ = std::fs::remove_file(socket_path); + + // Wrap handler in RefCell so the Fn closure can borrow mutably. + let handler = RefCell::new(EchoHandler); + + ipc_server::serve(socket_path, |payload: &[u8]| { + // Deserialize: [Command] + let request: Vec = rmp_serde::from_slice(payload) + .unwrap_or_default(); + + let command = match request.into_iter().next() { + Some(cmd) => cmd, + None => { + let err = Response::EchoErrorResponse(EchoErrorResponse { + message: "empty request".to_string(), + }); + return rmp_serde::to_vec_named(&err).unwrap_or_default(); + } + }; + + // Check for shutdown before dispatch + let is_shutdown = matches!(&command, Command::EchoShutdown(_)); + + let response = match echo_wire_compat::generated::echo_server::dispatch( + &mut *handler.borrow_mut(), command + ) { + Ok(resp) => resp, + Err(_e) => { + if is_shutdown { + Response::EchoShutdownResponse(EchoShutdownResponse {}) + } else { + Response::EchoErrorResponse(EchoErrorResponse { + message: _e.to_string(), + }) + } + } + }; + + rmp_serde::to_vec_named(&response).unwrap_or_default() + }).map_err(|e| EchoError::Io(e)) +} diff --git a/ipc-codegen/examples/rust/echo/src/generated/backend.rs b/ipc-codegen/examples/rust/echo/src/generated/backend.rs new file mode 100644 index 000000000000..75eef08387c3 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/backend.rs @@ -0,0 +1,44 @@ +//! Backend trait for msgpack communication +//! +//! This module defines a simple, pluggable interface for Barretenberg backends. +//! Users can easily implement custom backends (FFI, WASM, IPC, etc.). + +use crate::error::Result; + +/// Simple interface for msgpack backend implementations. +/// +/// Implement this trait to create a custom backend for Barretenberg. +/// The backend handles msgpack-encoded command/response communication. +/// +/// # Example +/// +/// ```ignore +/// struct MyCustomBackend { +/// // your FFI handle, connection, etc. +/// } +/// +/// impl Backend for MyCustomBackend { +/// fn call(&mut self, input: &[u8]) -> Result> { +/// // Send input to your backend +/// // Return the response +/// } +/// +/// fn destroy(&mut self) -> Result<()> { +/// // Clean up resources +/// Ok(()) +/// } +/// } +/// ``` +pub trait Backend { + /// Execute a msgpack command and return the msgpack response. + /// + /// # Arguments + /// * `input` - Msgpack-encoded command + /// + /// # Returns + /// Msgpack-encoded response + fn call(&mut self, input: &[u8]) -> Result>; + + /// Clean up resources and shutdown the backend. + fn destroy(&mut self) -> Result<()>; +} diff --git a/ipc-codegen/examples/rust/echo/src/generated/echo_client.rs b/ipc-codegen/examples/rust/echo/src/generated/echo_client.rs new file mode 100644 index 000000000000..8118e5557651 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/echo_client.rs @@ -0,0 +1,84 @@ +//! AUTOGENERATED - DO NOT EDIT +//! Echo IPC client API + +use super::backend::Backend; +use super::error::{BarretenbergError, Result}; +use super::echo_types::*; + +/// Echo IPC client API +pub struct EchoApi { + backend: B, +} + +impl EchoApi { + /// Create API with custom backend + pub fn new(backend: B) -> Self { + Self { backend } + } + + fn execute(&mut self, command: Command) -> Result { + let input_buffer = rmp_serde::to_vec_named(&vec![command]) + .map_err(|e| BarretenbergError::Serialization(e.to_string()))?; + + let output_buffer = self.backend.call(&input_buffer)?; + + let response: Response = rmp_serde::from_slice(&output_buffer) + .map_err(|e| BarretenbergError::Deserialization(e.to_string()))?; + + Ok(response) + } + + /// Execute EchoBytes + pub fn bytes(&mut self, data: &[u8]) -> Result { + let cmd = Command::EchoBytes(EchoBytes::new(data.to_vec())); + match self.execute(cmd)? { + Response::EchoBytesResponse(resp) => Ok(resp), + Response::EchoErrorResponse(err) => Err(BarretenbergError::Backend( + err.message + )), + _ => Err(BarretenbergError::InvalidResponse( + "Expected EchoBytesResponse".to_string() + )), + } + } + + /// Execute EchoFields + pub fn fields(&mut self, a: u32, b: u64, name: String) -> Result { + let cmd = Command::EchoFields(EchoFields::new(a, b, name)); + match self.execute(cmd)? { + Response::EchoFieldsResponse(resp) => Ok(resp), + Response::EchoErrorResponse(err) => Err(BarretenbergError::Backend( + err.message + )), + _ => Err(BarretenbergError::InvalidResponse( + "Expected EchoFieldsResponse".to_string() + )), + } + } + + /// Execute EchoNested + pub fn nested(&mut self, inner: EchoInner) -> Result { + let cmd = Command::EchoNested(EchoNested::new(inner)); + match self.execute(cmd)? { + Response::EchoNestedResponse(resp) => Ok(resp), + Response::EchoErrorResponse(err) => Err(BarretenbergError::Backend( + err.message + )), + _ => Err(BarretenbergError::InvalidResponse( + "Expected EchoNestedResponse".to_string() + )), + } + } + + /// Shutdown backend gracefully + pub fn shutdown(&mut self) -> Result<()> { + let cmd = Command::EchoShutdown(EchoShutdown::new()); + let _ = self.execute(cmd)?; + self.backend.destroy() + } + + /// Destroy backend without shutdown command + pub fn destroy(&mut self) -> Result<()> { + self.backend.destroy() + } +} diff --git a/ipc-codegen/examples/rust/echo/src/generated/echo_server.rs b/ipc-codegen/examples/rust/echo/src/generated/echo_server.rs new file mode 100644 index 000000000000..27b843f8bb55 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/echo_server.rs @@ -0,0 +1,40 @@ +//! AUTOGENERATED - DO NOT EDIT +//! Server-side dispatch for Echo IPC protocol + +use super::error::{BarretenbergError, Result}; +use super::echo_types::*; + +/// Handler trait — implement this to serve Echo commands. +pub trait Handler { + fn bytes(&mut self, cmd: EchoBytes) -> Result; + fn fields(&mut self, cmd: EchoFields) -> Result; + fn nested(&mut self, cmd: EchoNested) -> Result; +} + +/// Dispatch a single command to the handler and return the response. +pub fn dispatch(handler: &mut dyn Handler, command: Command) -> Result { + let response = match command { + Command::EchoBytes(cmd) => { + match handler.bytes(cmd) { + Ok(resp) => Response::EchoBytesResponse(resp), + Err(e) => Response::EchoErrorResponse(EchoErrorResponse { message: e.to_string() }), + } + } + Command::EchoFields(cmd) => { + match handler.fields(cmd) { + Ok(resp) => Response::EchoFieldsResponse(resp), + Err(e) => Response::EchoErrorResponse(EchoErrorResponse { message: e.to_string() }), + } + } + Command::EchoNested(cmd) => { + match handler.nested(cmd) { + Ok(resp) => Response::EchoNestedResponse(resp), + Err(e) => Response::EchoErrorResponse(EchoErrorResponse { message: e.to_string() }), + } + } + Command::EchoShutdown(_) => { + return Err(BarretenbergError::Backend("shutdown requested".to_string())); + } + }; + Ok(response) +} diff --git a/ipc-codegen/examples/rust/echo/src/generated/echo_types.rs b/ipc-codegen/examples/rust/echo/src/generated/echo_types.rs new file mode 100644 index 000000000000..72530f1e7ab4 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/echo_types.rs @@ -0,0 +1,401 @@ +//! AUTOGENERATED - DO NOT EDIT +//! Generated types for Echo IPC protocol + +use serde::{Deserialize, Serialize}; + +/// Schema version hash for compatibility checking +pub const SCHEMA_HASH: &str = "bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc"; + +/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated, no heap. +/// Serializes as msgpack bin32 on the wire. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Fr(pub [u8; 32]); + +impl Fr { + pub fn from_bytes(bytes: [u8; 32]) -> Self { Self(bytes) } + pub fn to_bytes(&self) -> &[u8; 32] { &self.0 } + pub fn as_slice(&self) -> &[u8] { &self.0 } +} + +impl Serialize for Fr { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de> Deserialize<'de> for Fr { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + let bytes: Vec = >::deserialize(deserializer)?; + let arr: [u8; 32] = bytes.try_into() + .map_err(|v: Vec| serde::de::Error::invalid_length(v.len(), &"32 bytes"))?; + Ok(Fr(arr)) + } +} + +mod serde_bytes { + use serde::{Deserialize, Deserializer, Serializer}; + pub fn serialize(bytes: &Vec, serializer: S) -> Result + where S: Serializer { serializer.serialize_bytes(bytes) } + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { >::deserialize(deserializer) } +} + +mod serde_vec_bytes { + use serde::{Deserialize, Deserializer, Serializer, Serialize}; + use serde::ser::SerializeSeq; + use serde::de::{SeqAccess, Visitor}; + + #[derive(Serialize, Deserialize)] + struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); + + pub fn serialize(vec: &Vec>, serializer: S) -> Result + where S: Serializer { + let mut seq = serializer.serialize_seq(Some(vec.len()))?; + for bytes in vec { + seq.serialize_element(&BytesWrapper(bytes.clone()))?; + } + seq.end() + } + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where D: Deserializer<'de> { + struct VecVecU8Visitor; + impl<'de> Visitor<'de> for VecVecU8Visitor { + type Value = Vec>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a sequence of byte arrays") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let mut vec = Vec::new(); + while let Some(wrapper) = seq.next_element::()? { + vec.push(wrapper.0); + } + Ok(vec) + } + } + deserializer.deserialize_seq(VecVecU8Visitor) + } +} + +mod serde_array4_bytes { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde::ser::SerializeTuple; + use serde::de::{SeqAccess, Visitor}; + + #[derive(Serialize, Deserialize)] + struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); + + pub fn serialize(arr: &[Vec; 4], serializer: S) -> Result + where S: Serializer { + let mut tup = serializer.serialize_tuple(4)?; + for bytes in arr { + tup.serialize_element(&BytesWrapper(bytes.clone()))?; + } + tup.end() + } + pub fn deserialize<'de, D>(deserializer: D) -> Result<[Vec; 4], D::Error> + where D: Deserializer<'de> { + struct Array4Visitor; + impl<'de> Visitor<'de> for Array4Visitor { + type Value = [Vec; 4]; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an array of 4 byte arrays") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let mut arr: [Vec; 4] = Default::default(); + for (i, item) in arr.iter_mut().enumerate() { + *item = seq.next_element::()? + .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?.0; + } + Ok(arr) + } + } + deserializer.deserialize_tuple(4, Array4Visitor) + } +} + +/// EchoInner +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoInner { + #[serde(with = "serde_vec_bytes")] + pub values: Vec>, + pub flag: Option, +} + +/// EchoBytes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoBytes { + #[serde(rename = "__typename", skip, default)] + pub type_name: String, + #[serde(with = "serde_bytes")] + pub data: Vec, +} + +impl EchoBytes { + pub fn new(data: Vec) -> Self { + Self { + type_name: "EchoBytes".to_string(), + data, + } + } +} + +/// EchoFields +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoFields { + #[serde(rename = "__typename", skip, default)] + pub type_name: String, + pub a: u32, + pub b: u64, + pub name: String, +} + +impl EchoFields { + pub fn new(a: u32, b: u64, name: String) -> Self { + Self { + type_name: "EchoFields".to_string(), + a, + b, + name, + } + } +} + +/// EchoNested +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoNested { + #[serde(rename = "__typename", skip, default)] + pub type_name: String, + pub inner: EchoInner, +} + +impl EchoNested { + pub fn new(inner: EchoInner) -> Self { + Self { + type_name: "EchoNested".to_string(), + inner, + } + } +} + +/// EchoShutdown +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoShutdown { + #[serde(rename = "__typename", skip, default)] + pub type_name: String, + +} + +impl EchoShutdown { + pub fn new() -> Self { + Self { + type_name: "EchoShutdown".to_string(), + } + } +} + +/// EchoBytesResponse +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoBytesResponse { + #[serde(with = "serde_bytes")] + pub data: Vec, +} + +/// EchoFieldsResponse +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoFieldsResponse { + pub a: u32, + pub b: u64, + pub name: String, +} + +/// EchoNestedResponse +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoNestedResponse { + pub inner: EchoInner, +} + +/// EchoShutdownResponse +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoShutdownResponse { + +} + +/// EchoErrorResponse +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EchoErrorResponse { + pub message: String, +} + +/// Command enum - wraps all possible commands +#[derive(Debug, Clone)] +pub enum Command { + EchoBytes(EchoBytes), + EchoFields(EchoFields), + EchoNested(EchoNested), + EchoShutdown(EchoShutdown), +} + +impl Serialize for Command { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + use serde::ser::SerializeTuple; + let mut tuple = serializer.serialize_tuple(2)?; + match self { + Command::EchoBytes(data) => { + tuple.serialize_element("EchoBytes")?; + tuple.serialize_element(data)?; + } + Command::EchoFields(data) => { + tuple.serialize_element("EchoFields")?; + tuple.serialize_element(data)?; + } + Command::EchoNested(data) => { + tuple.serialize_element("EchoNested")?; + tuple.serialize_element(data)?; + } + Command::EchoShutdown(data) => { + tuple.serialize_element("EchoShutdown")?; + tuple.serialize_element(data)?; + } + } + tuple.end() + } +} + +impl<'de> Deserialize<'de> for Command { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + use serde::de::{SeqAccess, Visitor}; + struct CommandVisitor; + + impl<'de> Visitor<'de> for CommandVisitor { + type Value = Command; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a 2-element array [name, payload]") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let name: String = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + match name.as_str() { + "EchoBytes" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Command::EchoBytes(data)) + } + "EchoFields" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Command::EchoFields(data)) + } + "EchoNested" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Command::EchoNested(data)) + } + "EchoShutdown" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Command::EchoShutdown(data)) + } + _ => Err(serde::de::Error::unknown_variant(&name, &["EchoBytes", "EchoFields", "EchoNested", "EchoShutdown"])), + } + } + } + deserializer.deserialize_tuple(2, CommandVisitor) + } +} + +/// Response enum - wraps all possible responses +#[derive(Debug, Clone)] +pub enum Response { + EchoBytesResponse(EchoBytesResponse), + EchoFieldsResponse(EchoFieldsResponse), + EchoNestedResponse(EchoNestedResponse), + EchoShutdownResponse(EchoShutdownResponse), + EchoErrorResponse(EchoErrorResponse), +} + +impl Serialize for Response { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + use serde::ser::SerializeTuple; + let mut tuple = serializer.serialize_tuple(2)?; + match self { + Response::EchoBytesResponse(data) => { + tuple.serialize_element("EchoBytesResponse")?; + tuple.serialize_element(data)?; + } + Response::EchoFieldsResponse(data) => { + tuple.serialize_element("EchoFieldsResponse")?; + tuple.serialize_element(data)?; + } + Response::EchoNestedResponse(data) => { + tuple.serialize_element("EchoNestedResponse")?; + tuple.serialize_element(data)?; + } + Response::EchoShutdownResponse(data) => { + tuple.serialize_element("EchoShutdownResponse")?; + tuple.serialize_element(data)?; + } + Response::EchoErrorResponse(data) => { + tuple.serialize_element("EchoErrorResponse")?; + tuple.serialize_element(data)?; + } + } + tuple.end() + } +} + +impl<'de> Deserialize<'de> for Response { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + use serde::de::{SeqAccess, Visitor}; + struct ResponseVisitor; + + impl<'de> Visitor<'de> for ResponseVisitor { + type Value = Response; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a 2-element array [name, payload]") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let name: String = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + match name.as_str() { + "EchoBytesResponse" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Response::EchoBytesResponse(data)) + } + "EchoFieldsResponse" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Response::EchoFieldsResponse(data)) + } + "EchoNestedResponse" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Response::EchoNestedResponse(data)) + } + "EchoShutdownResponse" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Response::EchoShutdownResponse(data)) + } + "EchoErrorResponse" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Response::EchoErrorResponse(data)) + } + _ => Err(serde::de::Error::unknown_variant(&name, &["EchoBytesResponse", "EchoFieldsResponse", "EchoNestedResponse", "EchoShutdownResponse", "EchoErrorResponse"])), + } + } + } + deserializer.deserialize_tuple(2, ResponseVisitor) + } +} diff --git a/ipc-codegen/examples/rust/echo/src/generated/error.rs b/ipc-codegen/examples/rust/echo/src/generated/error.rs new file mode 100644 index 000000000000..f70fa5e89612 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/error.rs @@ -0,0 +1,35 @@ +//! Error types for Barretenberg operations + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BarretenbergError { + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Deserialization error: {0}")] + Deserialization(String), + + #[error("Backend error: {0}")] + Backend(String), + + #[error("IPC error: {0}")] + Ipc(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Invalid response: {0}")] + InvalidResponse(String), + + #[error("Connection error: {0}")] + Connection(String), + + #[error("WASM error: {0}")] + Wasm(String), +} + +pub type Result = std::result::Result; + +// Alias for codegen compatibility — generated server/client code uses EchoError +pub type EchoError = BarretenbergError; diff --git a/ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs b/ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs new file mode 100644 index 000000000000..cfce19cbc26a --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs @@ -0,0 +1,51 @@ +//! Generic IPC server over Unix Domain Sockets. +//! Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. +//! Service-specific dispatch is injected via the dispatch function parameter. + +use std::io::{Read, Write}; +use std::os::unix::net::UnixListener; + +/// Dispatch function signature: takes raw command name + msgpack bytes, returns response name + bytes +pub type DispatchFn = Box Vec>; + +/// Run an IPC server. Accepts one connection, serves until disconnect or shutdown. +pub fn serve(socket_path: &str, handler: impl Fn(&[u8]) -> Vec) -> std::io::Result<()> { + let _ = std::fs::remove_file(socket_path); + let listener = UnixListener::bind(socket_path)?; + eprintln!("ipc-server(rust): listening on {}", socket_path); + + let (mut stream, _) = listener.accept()?; + + loop { + // Read 4-byte LE length + let mut len_buf = [0u8; 4]; + if stream.read_exact(&mut len_buf).is_err() { + break; + } + let len = u32::from_le_bytes(len_buf) as usize; + + // Read payload + let mut payload = vec![0u8; len]; + stream.read_exact(&mut payload)?; + + // Check for shutdown + let is_shutdown = payload.windows(8).any(|w| w == b"Shutdown"); + + // Dispatch + let response = handler(&payload); + + // Send length-prefixed response + let resp_len = (response.len() as u32).to_le_bytes(); + stream.write_all(&resp_len)?; + stream.write_all(&response)?; + stream.flush()?; + + if is_shutdown { + break; + } + } + + let _ = std::fs::remove_file(socket_path); + eprintln!("ipc-server(rust): shutdown"); + Ok(()) +} diff --git a/ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs b/ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs new file mode 100644 index 000000000000..a524391cd687 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs @@ -0,0 +1,78 @@ +//! UDS (Unix Domain Socket) backend for Barretenberg +//! +//! Connects to a running BB server over a Unix domain socket, +//! using the standard 4-byte LE length-prefixed msgpack protocol. +//! Same wire format as C++/TS/Zig IPC clients. + +use super::backend::Backend; +use super::error::{BarretenbergError, Result}; +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; + +/// UDS backend — connects to a BB server over Unix domain socket. +pub struct UdsBackend { + stream: UnixStream, +} + +impl UdsBackend { + /// Connect to a BB server at the given socket path. + /// + /// # Arguments + /// * `socket_path` - Path to the Unix domain socket (e.g. "/tmp/bb.sock") + pub fn connect(socket_path: impl AsRef) -> Result { + let stream = UnixStream::connect(socket_path.as_ref()).map_err(|e| { + BarretenbergError::Ipc(format!( + "Failed to connect to {}: {}", + socket_path.as_ref().display(), + e + )) + })?; + Ok(Self { stream }) + } + + fn send_with_prefix(&mut self, data: &[u8]) -> Result<()> { + let len = data.len() as u32; + self.stream + .write_all(&len.to_le_bytes()) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to write length: {}", e)))?; + self.stream + .write_all(data) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to write data: {}", e)))?; + self.stream + .flush() + .map_err(|e| BarretenbergError::Ipc(format!("Failed to flush: {}", e)))?; + Ok(()) + } + + fn receive_with_prefix(&mut self) -> Result> { + let mut len_buf = [0u8; 4]; + self.stream + .read_exact(&mut len_buf) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to read length: {}", e)))?; + let len = u32::from_le_bytes(len_buf) as usize; + let mut data = vec![0u8; len]; + self.stream + .read_exact(&mut data) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to read data: {}", e)))?; + Ok(data) + } +} + +impl Backend for UdsBackend { + fn call(&mut self, input: &[u8]) -> Result> { + self.send_with_prefix(input)?; + self.receive_with_prefix() + } + + fn destroy(&mut self) -> Result<()> { + let _ = self.stream.shutdown(std::net::Shutdown::Both); + Ok(()) + } +} + +impl Drop for UdsBackend { + fn drop(&mut self) { + let _ = self.destroy(); + } +} diff --git a/ipc-codegen/examples/rust/echo/src/lib.rs b/ipc-codegen/examples/rust/echo/src/lib.rs new file mode 100644 index 000000000000..1fae1aba7198 --- /dev/null +++ b/ipc-codegen/examples/rust/echo/src/lib.rs @@ -0,0 +1,16 @@ +// Generated modules live in src/generated/ +pub mod generated { + pub mod echo_types; + pub mod echo_server; + pub mod echo_client; + pub mod backend; + pub mod error; + pub mod ipc_server; + pub mod uds_backend; +} + +// Re-export under the names that generated server/client code expects +// (they use `crate::types_gen`, `crate::error`, `crate::backend`) +pub use generated::echo_types as types_gen; +pub use generated::error; +pub use generated::backend; diff --git a/ipc-codegen/examples/rust/wsdb/Cargo.toml b/ipc-codegen/examples/rust/wsdb/Cargo.toml new file mode 100644 index 000000000000..89a915403bba --- /dev/null +++ b/ipc-codegen/examples/rust/wsdb/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wsdb-service-example" +version = "0.1.0" +edition = "2021" +description = "Example WSDB service implementation in Rust using codegen" + +[dependencies] +rmp = "0.8" +rmp-serde = "1" +serde = { version = "1", features = ["derive"] } +thiserror = "2" diff --git a/ipc-codegen/examples/rust/wsdb/README.md b/ipc-codegen/examples/rust/wsdb/README.md new file mode 100644 index 000000000000..fdb68399e07f --- /dev/null +++ b/ipc-codegen/examples/rust/wsdb/README.md @@ -0,0 +1,45 @@ +# Rust WSDB Service Example + +Demonstrates building a WSDB service in Rust using the codegen tool. + +## Setup + +```bash +# Generate all bindings (types, client, server, backends, handler stubs) +./generate.sh + +# Build +cargo build +``` + +## Structure + +``` +src/ + generated/ # Always-regenerated by codegen (don't hand-edit) + wsdb_types.rs # Wire types with serialization + wsdb_client.rs # Typed client wrapper + wsdb_server.rs # Server dispatch + backend.rs # Backend trait (template, editable) + error.rs # Error types (template, editable) + uds_backend.rs # UDS transport (template, editable) + ffi_backend.rs # FFI transport (template, editable) + wsdb_handlers.rs # Handler stubs (skeleton, implement these!) + main.rs # Entry point (skeleton, edit as needed) + lib.rs # Module declarations +``` + +## Implementing handlers + +Edit `src/wsdb_handlers.rs` — each `handle_*` function receives a wire command +and returns a wire response. Add your business logic (database, state management, etc.) +to the `WsdbContext` struct. + +## Regenerating after schema changes + +```bash +./generate.sh +``` + +This regenerates types/client/server in `generated/` but does NOT overwrite +your handler implementations or backend templates. diff --git a/ipc-codegen/examples/rust/wsdb/generate.sh b/ipc-codegen/examples/rust/wsdb/generate.sh new file mode 100755 index 000000000000..a08975550bcb --- /dev/null +++ b/ipc-codegen/examples/rust/wsdb/generate.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Generate WSDB Rust bindings from the committed schema. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CODEGEN="$SCRIPT_DIR/../../.." +SCHEMA="$CODEGEN/schemas/wsdb_schema.json" +NODE_FLAGS="--experimental-strip-types --experimental-transform-types --no-warnings" + +echo "Generating Rust WSDB bindings..." +node $NODE_FLAGS "$CODEGEN/src/generate.ts" \ + --schema "$SCHEMA" \ + --lang rust \ + --out "$SCRIPT_DIR/src/generated" \ + --server --client --uds --ffi \ + --prefix Wsdb \ + --skeleton "$SCRIPT_DIR/src" + +echo "Done. Generated files in src/generated/, skeleton in src/" diff --git a/ipc-codegen/examples/rust/wsdb/src/lib.rs b/ipc-codegen/examples/rust/wsdb/src/lib.rs new file mode 100644 index 000000000000..d63ef4575379 --- /dev/null +++ b/ipc-codegen/examples/rust/wsdb/src/lib.rs @@ -0,0 +1,21 @@ +// WSDB service example — generated + hand-written code. +// +// To regenerate: ./generate.sh +// Then implement the handlers in wsdb_handlers.rs + +pub mod generated { + pub mod wsdb_types; + pub mod wsdb_client; + pub mod wsdb_server; + pub mod backend; + pub mod error; + + #[cfg(feature = "uds")] + pub mod uds_backend; + + #[cfg(feature = "ffi")] + pub mod ffi_backend; +} + +pub use generated::backend::Backend; +pub use generated::error::{BarretenbergError, Result}; diff --git a/ipc-codegen/examples/scripts/run_cross_language_tests.sh b/ipc-codegen/examples/scripts/run_cross_language_tests.sh new file mode 100755 index 000000000000..0cdea133ad4c --- /dev/null +++ b/ipc-codegen/examples/scripts/run_cross_language_tests.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# +# Cross-language IPC wire compatibility test matrix. +# Tests all server/client language pairs for the echo service. +# +# Usage: ./scripts/run_cross_language_tests.sh +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +EXAMPLES_DIR="$(dirname "$SCRIPT_DIR")" +cd "$EXAMPLES_DIR" + +PASS=0 +FAIL=0 +TOTAL=0 + +# Generate types from echo schema using the codegen CLI +"$EXAMPLES_DIR/echo-schema/generate.sh" + +# Build Rust binaries +echo "Building Rust echo binaries..." +(cd rust/echo && cargo build --quiet 2>&1) + +# Build C++ binaries +echo "Building C++ echo binaries..." +MSGPACK_INC="$(cd "$EXAMPLES_DIR/../../barretenberg/cpp/build/_deps/msgpack-c/src/msgpack-c/include" 2>/dev/null && pwd)" || true +BB_SERIALIZE="$(cd "$EXAMPLES_DIR/../../barretenberg/cpp/src/barretenberg/serialize/msgpack_impl" 2>/dev/null && pwd)" || true +if [ -n "$MSGPACK_INC" ] && [ -d "$MSGPACK_INC" ]; then + CXX_FLAGS="-std=c++20 -I $MSGPACK_INC -I $BB_SERIALIZE -I . -DMSGPACK_NO_BOOST -DMSGPACK_USE_STD_VARIANT_ADAPTOR" + (cd cpp/echo && clang++ $CXX_FLAGS -o echo_server echo_server.cpp 2>&1) + (cd cpp/echo && clang++ $CXX_FLAGS -o echo_client echo_client.cpp 2>&1) + CPP_AVAILABLE=true +else + echo " (skipping C++ — msgpack-c not found, run cmake first)" + CPP_AVAILABLE=false +fi + +# Install TS dependencies if needed +if [ ! -d ts/echo/node_modules ]; then + echo "Installing TS dependencies..." + (cd ts/echo && npm install --no-package-lock --quiet 2>&1) +fi + +# Server/client definitions +# Format: "lang:start_cmd:name" +SERVERS=( + "rust:rust/echo/target/debug/echo_server:Rust" + "ts:npx tsx ts/echo/echo_server.ts:TS" +) +CLIENTS=( + "rust:rust/echo/target/debug/echo_client:Rust" + "ts:npx tsx ts/echo/echo_client.ts:TS" +) + +if [ "$CPP_AVAILABLE" = true ]; then + SERVERS+=("cpp:cpp/echo/echo_server:C++") + CLIENTS+=("cpp:cpp/echo/echo_client:C++") +fi + +# Build Zig binaries +echo "Building Zig echo binaries..." +(cd zig/echo && zig build 2>&1) +SERVERS+=("zig:zig/echo/zig-out/bin/echo_server:Zig") +CLIENTS+=("zig:zig/echo/zig-out/bin/echo_client:Zig") + +run_pair() { + local server_cmd="$1" + local server_name="$2" + local client_cmd="$3" + local client_name="$4" + + TOTAL=$((TOTAL + 1)) + local socket="/tmp/echo-matrix-${server_name}-${client_name}-$$.sock" + + # Start server + $server_cmd --socket "$socket" & + local server_pid=$! + + # Wait for socket (up to 2 seconds) + for i in $(seq 1 20); do + if [ -S "$socket" ]; then break; fi + sleep 0.1 + done + + if [ ! -S "$socket" ]; then + echo " FAIL: server did not create socket" + FAIL=$((FAIL + 1)) + kill $server_pid 2>/dev/null || true + return + fi + + # Run client + if $client_cmd --socket "$socket" 2>/dev/null; then + echo " PASS: $server_name server + $client_name client" + PASS=$((PASS + 1)) + else + echo " FAIL: $server_name server + $client_name client" + FAIL=$((FAIL + 1)) + fi + + # Cleanup + wait $server_pid 2>/dev/null || true + rm -f "$socket" +} + +# --------------------------------------------------------------------------- +# Level 1: Golden file deserialization tests +# --------------------------------------------------------------------------- +echo "=== Golden File Deserialization Tests ===" +echo "" + +# Generate golden files from Rust reference +echo "Generating golden files from Rust reference..." +(cd rust/echo && cargo build --quiet --bin generate_golden 2>&1) +rust/echo/target/debug/generate_golden --output-dir echo-schema/golden 2>/dev/null + +# Rust golden test +echo " Rust:" +if (cd rust/echo && cargo build --quiet --bin golden_test 2>&1) && \ + rust/echo/target/debug/golden_test --golden-dir echo-schema/golden 2>/dev/null; then + echo " PASS" + PASS=$((PASS + 1)) +else + echo " FAIL" + FAIL=$((FAIL + 1)) +fi +TOTAL=$((TOTAL + 1)) + +# TypeScript golden test +echo " TypeScript:" +if npx tsx ts/echo/golden_test.ts 2>/dev/null; then + echo " PASS" + PASS=$((PASS + 1)) +else + echo " FAIL" + FAIL=$((FAIL + 1)) +fi +TOTAL=$((TOTAL + 1)) + +echo "" + +# --------------------------------------------------------------------------- +# Level 2+3: IPC Round-Trip Matrix +# --------------------------------------------------------------------------- +echo "=== Cross-Language Wire Compatibility Matrix ===" +echo "" + +for server_entry in "${SERVERS[@]}"; do + IFS=: read -r _ server_cmd server_name <<< "$server_entry" + for client_entry in "${CLIENTS[@]}"; do + IFS=: read -r _ client_cmd client_name <<< "$client_entry" + run_pair "$server_cmd" "$server_name" "$client_cmd" "$client_name" + done +done + +echo "" +echo "Results: $PASS/$TOTAL passed, $FAIL failed" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/ipc-codegen/examples/ts/echo/echo_client.ts b/ipc-codegen/examples/ts/echo/echo_client.ts new file mode 100644 index 000000000000..5d42daf5f04e --- /dev/null +++ b/ipc-codegen/examples/ts/echo/echo_client.ts @@ -0,0 +1,64 @@ +/** + * Echo IPC client (TypeScript) — uses GENERATED types + IPC client template. + * Usage: npx tsx echo_client.ts --socket /tmp/echo.sock + * Exits 0 on success, 1 on failure. + * + * Note: The generated AsyncApi client depends on barretenberg-specific interfaces + * (IMsgpackBackendAsync, BBApiException). For this standalone test we use the + * generated IpcClient template directly with raw call(). + */ +import { IpcClient } from './generated/ipc_client.js'; +import type { + EchoBytesResponse, EchoFieldsResponse, EchoNestedResponse, +} from './generated/echo_types.js'; + +const args = process.argv.slice(2); +const socketIdx = args.indexOf('--socket'); +const socketPath = socketIdx >= 0 ? args[socketIdx + 1] : undefined; +if (!socketPath) { + console.error('Usage: echo_client.ts --socket '); + process.exit(1); +} + +function assertEqual(actual: any, expected: any, label: string) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a !== e) throw new Error(`${label}: expected ${e}, got ${a}`); +} + +async function run() { + const client = await IpcClient.connect(socketPath); + + // Test 1: EchoBytes + const testData = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF, 0x42]); + const [name1, resp1] = await client.call('EchoBytes', { data: testData }) as [string, EchoBytesResponse]; + assertEqual(name1, 'EchoBytesResponse', 'EchoBytes name'); + assertEqual(Buffer.from(resp1.data).toString('hex'), testData.toString('hex'), 'EchoBytes data'); + console.error('echo_client(ts): EchoBytes OK'); + + // Test 2: EchoFields + const [name2, resp2] = await client.call('EchoFields', { a: 42, b: 999999, name: 'hello wire compat' }) as [string, EchoFieldsResponse]; + assertEqual(name2, 'EchoFieldsResponse', 'EchoFields name'); + assertEqual(resp2.a, 42, 'EchoFields a'); + assertEqual(resp2.b, 999999, 'EchoFields b'); + assertEqual(resp2.name, 'hello wire compat', 'EchoFields name field'); + console.error('echo_client(ts): EchoFields OK'); + + // Test 3: EchoNested + const inner = { values: [Buffer.from([1, 2, 3]), Buffer.from([4, 5])], flag: true }; + const [name3, resp3] = await client.call('EchoNested', { inner }) as [string, EchoNestedResponse]; + assertEqual(name3, 'EchoNestedResponse', 'EchoNested name'); + assertEqual(resp3.inner.flag, true, 'EchoNested flag'); + assertEqual(resp3.inner.values.length, 2, 'EchoNested values length'); + console.error('echo_client(ts): EchoNested OK'); + + // Shutdown + await client.call('EchoShutdown', {}); + client.close(); + console.error('echo_client(ts): all tests passed'); +} + +run().catch(e => { + console.error(`echo_client(ts): FAILED: ${e.message}`); + process.exit(1); +}); diff --git a/ipc-codegen/examples/ts/echo/echo_server.ts b/ipc-codegen/examples/ts/echo/echo_server.ts new file mode 100644 index 000000000000..9b9336a20ac6 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/echo_server.ts @@ -0,0 +1,35 @@ +/** + * Echo IPC server (TypeScript) — uses GENERATED dispatch + IPC server template. + * Usage: npx tsx echo_server.ts --socket /tmp/echo.sock + */ +import { createServer } from './generated/ipc_server.js'; +import { dispatch } from './generated/server.js'; +import type { Handler } from './generated/server.js'; +import type { + EchoBytes, EchoBytesResponse, + EchoFields, EchoFieldsResponse, + EchoNested, EchoNestedResponse, +} from './generated/echo_types.js'; + +const args = process.argv.slice(2); +const socketIdx = args.indexOf('--socket'); +const socketPath = socketIdx >= 0 ? args[socketIdx + 1] : undefined; +if (!socketPath) { + console.error('Usage: echo_server.ts --socket '); + process.exit(1); +} + +// Implement the GENERATED Handler interface — echo everything back +const handler: Handler = { + async echoBytes(cmd: EchoBytes): Promise { + return { data: cmd.data }; + }, + async echoFields(cmd: EchoFields): Promise { + return { a: cmd.a, b: cmd.b, name: cmd.name }; + }, + async echoNested(cmd: EchoNested): Promise { + return { inner: cmd.inner }; + }, +}; + +createServer(socketPath, (commandName, payload) => dispatch(handler, commandName, payload)); diff --git a/ipc-codegen/examples/ts/echo/generated/api_types.ts b/ipc-codegen/examples/ts/echo/generated/api_types.ts new file mode 100644 index 000000000000..c052de104568 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/generated/api_types.ts @@ -0,0 +1,259 @@ +// AUTOGENERATED FILE - DO NOT EDIT + +/** Schema version hash for compatibility checking */ +export const SCHEMA_HASH = 'bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc'; + +// Type aliases for primitive types +/** 32-byte field element (Fr/Fq). Branded Uint8Array — no arithmetic, just type safety. */ +export type Fr = Uint8Array; +export type Field2 = [Fr, Fr]; + +// Public interfaces (exported) +export interface EchoInner { + values: Uint8Array[]; + flag: boolean | null; +} + +export interface EchoBytes { + data: Uint8Array; +} + +export interface EchoFields { + a: number; + b: number; + name: string; +} + +export interface EchoNested { + inner: EchoInner; +} + +export interface EchoShutdown { + +} + +export interface EchoBytesResponse { + data: Uint8Array; +} + +export interface EchoFieldsResponse { + a: number; + b: number; + name: string; +} + +export interface EchoNestedResponse { + inner: EchoInner; +} + +export interface EchoShutdownResponse { + +} + +export interface EchoErrorResponse { + message: string; +} + +// Private Msgpack interfaces (not exported) +interface MsgpackEchoInner { + values: Uint8Array[]; + flag: boolean | null; +} + +interface MsgpackEchoBytes { + data: Uint8Array; +} + +interface MsgpackEchoFields { + a: number; + b: number; + name: string; +} + +interface MsgpackEchoNested { + inner: MsgpackEchoInner; +} + +interface MsgpackEchoShutdown { + +} + +interface MsgpackEchoBytesResponse { + data: Uint8Array; +} + +interface MsgpackEchoFieldsResponse { + a: number; + b: number; + name: string; +} + +interface MsgpackEchoNestedResponse { + inner: MsgpackEchoInner; +} + +interface MsgpackEchoShutdownResponse { + +} + +interface MsgpackEchoErrorResponse { + message: string; +} + +// Conversion functions (exported) +export function toEchoInner(o: MsgpackEchoInner): EchoInner { + if (o.values === undefined) { throw new Error("Expected values in EchoInner deserialization"); } + if (o.flag === undefined) { throw new Error("Expected flag in EchoInner deserialization"); }; + return { + values: o.values, + flag: o.flag, + }; +} + +export function toEchoBytes(o: MsgpackEchoBytes): EchoBytes { + if (o.data === undefined) { throw new Error("Expected data in EchoBytes deserialization"); }; + return { + data: o.data, + }; +} + +export function toEchoFields(o: MsgpackEchoFields): EchoFields { + if (o.a === undefined) { throw new Error("Expected a in EchoFields deserialization"); } + if (o.b === undefined) { throw new Error("Expected b in EchoFields deserialization"); } + if (o.name === undefined) { throw new Error("Expected name in EchoFields deserialization"); }; + return { + a: o.a, + b: o.b, + name: o.name, + }; +} + +export function toEchoNested(o: MsgpackEchoNested): EchoNested { + if (o.inner === undefined) { throw new Error("Expected inner in EchoNested deserialization"); }; + return { + inner: toEchoInner(o.inner), + }; +} + +export function toEchoShutdown(o: MsgpackEchoShutdown): EchoShutdown { + return {}; +} + +export function toEchoBytesResponse(o: MsgpackEchoBytesResponse): EchoBytesResponse { + if (o.data === undefined) { throw new Error("Expected data in EchoBytesResponse deserialization"); }; + return { + data: o.data, + }; +} + +export function toEchoFieldsResponse(o: MsgpackEchoFieldsResponse): EchoFieldsResponse { + if (o.a === undefined) { throw new Error("Expected a in EchoFieldsResponse deserialization"); } + if (o.b === undefined) { throw new Error("Expected b in EchoFieldsResponse deserialization"); } + if (o.name === undefined) { throw new Error("Expected name in EchoFieldsResponse deserialization"); }; + return { + a: o.a, + b: o.b, + name: o.name, + }; +} + +export function toEchoNestedResponse(o: MsgpackEchoNestedResponse): EchoNestedResponse { + if (o.inner === undefined) { throw new Error("Expected inner in EchoNestedResponse deserialization"); }; + return { + inner: toEchoInner(o.inner), + }; +} + +export function toEchoShutdownResponse(o: MsgpackEchoShutdownResponse): EchoShutdownResponse { + return {}; +} + +export function toEchoErrorResponse(o: MsgpackEchoErrorResponse): EchoErrorResponse { + if (o.message === undefined) { throw new Error("Expected message in EchoErrorResponse deserialization"); }; + return { + message: o.message, + }; +} + +export function fromEchoInner(o: EchoInner): MsgpackEchoInner { + if (o.values === undefined) { throw new Error("Expected values in EchoInner serialization"); } + if (o.flag === undefined) { throw new Error("Expected flag in EchoInner serialization"); }; + return { + values: o.values, + flag: o.flag, + }; +} + +export function fromEchoBytes(o: EchoBytes): MsgpackEchoBytes { + if (o.data === undefined) { throw new Error("Expected data in EchoBytes serialization"); }; + return { + data: o.data, + }; +} + +export function fromEchoFields(o: EchoFields): MsgpackEchoFields { + if (o.a === undefined) { throw new Error("Expected a in EchoFields serialization"); } + if (o.b === undefined) { throw new Error("Expected b in EchoFields serialization"); } + if (o.name === undefined) { throw new Error("Expected name in EchoFields serialization"); }; + return { + a: o.a, + b: o.b, + name: o.name, + }; +} + +export function fromEchoNested(o: EchoNested): MsgpackEchoNested { + if (o.inner === undefined) { throw new Error("Expected inner in EchoNested serialization"); }; + return { + inner: fromEchoInner(o.inner), + }; +} + +export function fromEchoShutdown(o: EchoShutdown): MsgpackEchoShutdown { + return {}; +} + +export function fromEchoBytesResponse(o: EchoBytesResponse): MsgpackEchoBytesResponse { + if (o.data === undefined) { throw new Error("Expected data in EchoBytesResponse serialization"); }; + return { + data: o.data, + }; +} + +export function fromEchoFieldsResponse(o: EchoFieldsResponse): MsgpackEchoFieldsResponse { + if (o.a === undefined) { throw new Error("Expected a in EchoFieldsResponse serialization"); } + if (o.b === undefined) { throw new Error("Expected b in EchoFieldsResponse serialization"); } + if (o.name === undefined) { throw new Error("Expected name in EchoFieldsResponse serialization"); }; + return { + a: o.a, + b: o.b, + name: o.name, + }; +} + +export function fromEchoNestedResponse(o: EchoNestedResponse): MsgpackEchoNestedResponse { + if (o.inner === undefined) { throw new Error("Expected inner in EchoNestedResponse serialization"); }; + return { + inner: fromEchoInner(o.inner), + }; +} + +export function fromEchoShutdownResponse(o: EchoShutdownResponse): MsgpackEchoShutdownResponse { + return {}; +} + +export function fromEchoErrorResponse(o: EchoErrorResponse): MsgpackEchoErrorResponse { + if (o.message === undefined) { throw new Error("Expected message in EchoErrorResponse serialization"); }; + return { + message: o.message, + }; +} + +// Base API interface +export interface BbApiBase { + echoBytes(command: EchoBytes): Promise; + echoFields(command: EchoFields): Promise; + echoNested(command: EchoNested): Promise; + echoShutdown(command: EchoShutdown): Promise; + destroy(): Promise; +} diff --git a/ipc-codegen/examples/ts/echo/generated/async.ts b/ipc-codegen/examples/ts/echo/generated/async.ts new file mode 100644 index 000000000000..a64717f9af5f --- /dev/null +++ b/ipc-codegen/examples/ts/echo/generated/async.ts @@ -0,0 +1,72 @@ +// AUTOGENERATED FILE - DO NOT EDIT + +import { IMsgpackBackendAsync } from '../../bb_backends/interface.js'; +import { Decoder, Encoder } from 'msgpackr'; +import { BBApiException } from '../../bbapi_exception.js'; +import { BbApiBase, EchoBytes, EchoBytesResponse, EchoFields, EchoFieldsResponse, EchoNested, EchoNestedResponse, EchoShutdown, EchoShutdownResponse, fromEchoBytes, fromEchoFields, fromEchoNested, fromEchoShutdown, toEchoBytesResponse, toEchoFieldsResponse, toEchoNestedResponse, toEchoShutdownResponse } from './api_types.js'; + +async function msgpackCall(backend: IMsgpackBackendAsync, input: any[]) { + const inputBuffer = new Encoder({ useRecords: false }).pack(input); + const encodedResult = await backend.call(inputBuffer); + return new Decoder({ useRecords: false }).unpack(encodedResult); +} + +export class AsyncApi implements BbApiBase { + constructor(protected backend: IMsgpackBackendAsync) {} + + echoBytes(command: EchoBytes): Promise { + const msgpackCommand = fromEchoBytes(command); + return msgpackCall(this.backend, [["EchoBytes", msgpackCommand]]).then(([variantName, result]: [string, any]) => { + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoBytesResponse') { + throw new BBApiException(`Expected variant name 'EchoBytesResponse' but got '${variantName}'`); + } + return toEchoBytesResponse(result); + }); + } + + echoFields(command: EchoFields): Promise { + const msgpackCommand = fromEchoFields(command); + return msgpackCall(this.backend, [["EchoFields", msgpackCommand]]).then(([variantName, result]: [string, any]) => { + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoFieldsResponse') { + throw new BBApiException(`Expected variant name 'EchoFieldsResponse' but got '${variantName}'`); + } + return toEchoFieldsResponse(result); + }); + } + + echoNested(command: EchoNested): Promise { + const msgpackCommand = fromEchoNested(command); + return msgpackCall(this.backend, [["EchoNested", msgpackCommand]]).then(([variantName, result]: [string, any]) => { + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoNestedResponse') { + throw new BBApiException(`Expected variant name 'EchoNestedResponse' but got '${variantName}'`); + } + return toEchoNestedResponse(result); + }); + } + + echoShutdown(command: EchoShutdown): Promise { + const msgpackCommand = fromEchoShutdown(command); + return msgpackCall(this.backend, [["EchoShutdown", msgpackCommand]]).then(([variantName, result]: [string, any]) => { + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoShutdownResponse') { + throw new BBApiException(`Expected variant name 'EchoShutdownResponse' but got '${variantName}'`); + } + return toEchoShutdownResponse(result); + }); + } + + destroy(): Promise { + return this.backend.destroy ? this.backend.destroy() : Promise.resolve(); + } +} diff --git a/ipc-codegen/examples/ts/echo/generated/ipc_client.ts b/ipc-codegen/examples/ts/echo/generated/ipc_client.ts new file mode 100644 index 000000000000..1fbdf761b3b1 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/generated/ipc_client.ts @@ -0,0 +1,58 @@ +/** + * Generic IPC client over Unix Domain Sockets. + * Handles: socket connect, length-prefixed framing, msgpack encode/decode. + * Service-specific typed methods are in the generated wrapper. + */ +import * as net from 'node:net'; +import { Decoder, Encoder } from 'msgpackr'; + +const encoder = new Encoder({ useRecords: false }); +const decoder = new Decoder({ useRecords: false }); + +export class IpcClient { + private conn: net.Socket; + private buffer = Buffer.alloc(0); + + private constructor(conn: net.Socket) { + this.conn = conn; + } + + static async connect(socketPath: string): Promise { + const conn = net.createConnection(socketPath); + await new Promise((resolve, reject) => { + conn.on('connect', resolve); + conn.on('error', reject); + }); + return new IpcClient(conn); + } + + close() { + this.conn.destroy(); + } + + /** Send a command and receive the response. */ + async call(commandName: string, fields: any): Promise<[string, any]> { + const packed = encoder.pack([[commandName, fields]]); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32LE(packed.length, 0); + this.conn.write(lenBuf); + this.conn.write(packed); + + return new Promise((resolve, reject) => { + const onData = (data: Buffer) => { + this.buffer = Buffer.concat([this.buffer, data]); + if (this.buffer.length >= 4) { + const len = this.buffer.readUInt32LE(0); + if (this.buffer.length >= 4 + len) { + this.conn.removeListener('data', onData); + const payload = this.buffer.subarray(4, 4 + len); + this.buffer = this.buffer.subarray(4 + len); + resolve(decoder.unpack(payload) as [string, any]); + } + } + }; + this.conn.on('data', onData); + this.conn.on('error', reject); + }); + } +} diff --git a/ipc-codegen/examples/ts/echo/generated/ipc_server.ts b/ipc-codegen/examples/ts/echo/generated/ipc_server.ts new file mode 100644 index 000000000000..3ef981ba2182 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/generated/ipc_server.ts @@ -0,0 +1,82 @@ +/** + * Generic IPC server over Unix Domain Sockets. + * Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. + * Service-specific dispatch is injected via the dispatchFn parameter. + */ +import * as net from 'node:net'; +import * as fs from 'node:fs'; +import { Decoder, Encoder } from 'msgpackr'; + +const encoder = new Encoder({ useRecords: false }); +const decoder = new Decoder({ useRecords: false }); + +export type DispatchFn = (commandName: string, payload: any) => Promise<[string, any]>; + +export function createServer(socketPath: string, dispatchFn: DispatchFn): { close: () => Promise } { + try { fs.unlinkSync(socketPath); } catch {} + + const server = net.createServer((conn) => { + let buffer = Buffer.alloc(0); + let responseChain: Promise = Promise.resolve(); + + conn.on('data', (data: Buffer) => { + buffer = Buffer.concat([buffer, data]); + + while (buffer.length >= 4) { + const len = buffer.readUInt32LE(0); + if (buffer.length < 4 + len) break; + + const payload = buffer.subarray(4, 4 + len); + buffer = buffer.subarray(4 + len); + + const request = decoder.unpack(payload) as any[]; + const [commandName, fields] = request[0] as [string, any]; + + if (commandName.endsWith('Shutdown')) { + sendResponse(conn, [commandName.replace(/^(.*)$/, '$1Response'), {}]); + setTimeout(() => { + server.close(); + try { fs.unlinkSync(socketPath); } catch {} + }, 50); + return; + } + + const prev = responseChain; + const result = dispatchFn(commandName, fields ?? {}); + responseChain = (async () => { + await prev; + try { + const [name, resp] = await result; + sendResponse(conn, [name, resp]); + } catch (err: any) { + sendResponse(conn, ['ErrorResponse', { message: err.message ?? 'Unknown error' }]); + } + })(); + void responseChain.catch(() => {}); + } + }); + + conn.on('error', () => {}); + }); + + server.listen(socketPath, () => { + console.error(`ipc-server(ts): listening on ${socketPath}`); + }); + + return { + close: () => new Promise(resolve => { + server.close(() => { + try { fs.unlinkSync(socketPath); } catch {} + resolve(); + }); + }), + }; +} + +function sendResponse(conn: net.Socket, response: [string, any]) { + const packed = encoder.pack(response); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32LE(packed.length, 0); + conn.write(lenBuf); + conn.write(packed); +} diff --git a/ipc-codegen/examples/ts/echo/generated/server.ts b/ipc-codegen/examples/ts/echo/generated/server.ts new file mode 100644 index 000000000000..55f9c283140d --- /dev/null +++ b/ipc-codegen/examples/ts/echo/generated/server.ts @@ -0,0 +1,41 @@ +// AUTOGENERATED FILE - DO NOT EDIT +// Server-side dispatch for IPC protocol + +import { EchoBytes, EchoBytesResponse, EchoFields, EchoFieldsResponse, EchoNested, EchoNestedResponse, fromEchoBytesResponse, fromEchoFieldsResponse, fromEchoNestedResponse, toEchoBytes, toEchoFields, toEchoNested } from './api_types.js'; + +/** Handler interface — implement this to serve commands. */ +export interface Handler { + echoBytes(command: EchoBytes): Promise; + echoFields(command: EchoFields): Promise; + echoNested(command: EchoNested): Promise; +} + +/** + * Dispatch a [commandName, payload] pair to the handler. + * Returns [responseName, responsePayload] for serialization. + */ +export async function dispatch( + handler: Handler, + commandName: string, + payload: any, +): Promise<[string, any]> { + switch (commandName) { + case 'EchoBytes': { + const cmd = toEchoBytes(payload); + const result = await handler.echoBytes(cmd); + return ['EchoBytesResponse', fromEchoBytesResponse(result)]; + } + case 'EchoFields': { + const cmd = toEchoFields(payload); + const result = await handler.echoFields(cmd); + return ['EchoFieldsResponse', fromEchoFieldsResponse(result)]; + } + case 'EchoNested': { + const cmd = toEchoNested(payload); + const result = await handler.echoNested(cmd); + return ['EchoNestedResponse', fromEchoNestedResponse(result)]; + } + default: + throw new Error(`Unknown command: ${commandName}`); + } +} diff --git a/ipc-codegen/examples/ts/echo/generated/sync.ts b/ipc-codegen/examples/ts/echo/generated/sync.ts new file mode 100644 index 000000000000..dff2770bac54 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/generated/sync.ts @@ -0,0 +1,68 @@ +// AUTOGENERATED FILE - DO NOT EDIT + +import { IMsgpackBackendSync } from '../../bb_backends/interface.js'; +import { Decoder, Encoder } from 'msgpackr'; +import { BBApiException } from '../../bbapi_exception.js'; +import { BbApiBase, EchoBytes, EchoBytesResponse, EchoFields, EchoFieldsResponse, EchoNested, EchoNestedResponse, EchoShutdown, EchoShutdownResponse, fromEchoBytes, fromEchoFields, fromEchoNested, fromEchoShutdown, toEchoBytesResponse, toEchoFieldsResponse, toEchoNestedResponse, toEchoShutdownResponse } from './api_types.js'; + +function msgpackCall(backend: IMsgpackBackendSync, input: any[]) { + const inputBuffer = new Encoder({ useRecords: false }).pack(input); + const encodedResult = backend.call(inputBuffer); + return new Decoder({ useRecords: false }).unpack(encodedResult); +} + +export class SyncApi { + constructor(protected backend: IMsgpackBackendSync) {} + + echoBytes(command: EchoBytes): EchoBytesResponse { + const msgpackCommand = fromEchoBytes(command); + const [variantName, result] = msgpackCall(this.backend, [["EchoBytes", msgpackCommand]]); + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoBytesResponse') { + throw new BBApiException(`Expected variant name 'EchoBytesResponse' but got '${variantName}'`); + } + return toEchoBytesResponse(result); + } + + echoFields(command: EchoFields): EchoFieldsResponse { + const msgpackCommand = fromEchoFields(command); + const [variantName, result] = msgpackCall(this.backend, [["EchoFields", msgpackCommand]]); + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoFieldsResponse') { + throw new BBApiException(`Expected variant name 'EchoFieldsResponse' but got '${variantName}'`); + } + return toEchoFieldsResponse(result); + } + + echoNested(command: EchoNested): EchoNestedResponse { + const msgpackCommand = fromEchoNested(command); + const [variantName, result] = msgpackCall(this.backend, [["EchoNested", msgpackCommand]]); + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoNestedResponse') { + throw new BBApiException(`Expected variant name 'EchoNestedResponse' but got '${variantName}'`); + } + return toEchoNestedResponse(result); + } + + echoShutdown(command: EchoShutdown): EchoShutdownResponse { + const msgpackCommand = fromEchoShutdown(command); + const [variantName, result] = msgpackCall(this.backend, [["EchoShutdown", msgpackCommand]]); + if (variantName === 'EchoErrorResponse') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== 'EchoShutdownResponse') { + throw new BBApiException(`Expected variant name 'EchoShutdownResponse' but got '${variantName}'`); + } + return toEchoShutdownResponse(result); + } + + destroy(): void { + if (this.backend.destroy) this.backend.destroy(); + } +} diff --git a/ipc-codegen/examples/ts/echo/golden_test.ts b/ipc-codegen/examples/ts/echo/golden_test.ts new file mode 100644 index 000000000000..c48da32470b3 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/golden_test.ts @@ -0,0 +1,116 @@ +/** + * Golden file wire compatibility test (TypeScript). + * + * Verifies that TypeScript can correctly deserialize msgpack data produced by + * the Rust reference implementation (the golden files). This is the critical + * cross-language compatibility check — if TS can read Rust's output, and the + * round-trip tests show Rust can read TS's output, wire compat is proven. + * + * Usage: npx tsx golden_test.ts + * Exits 0 if all pass, 1 on failure. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { Decoder } from 'msgpackr'; + +const decoder = new Decoder({ useRecords: false }); +const goldenDir = path.join(import.meta.dirname!, '../../echo-schema', 'golden'); + +let pass = 0; +let fail = 0; + +function assertEqual(actual: any, expected: any, label: string) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a !== e) { + throw new Error(`${label}: expected ${e}, got ${a}`); + } +} + +function bufEqual(a: Uint8Array, b: number[]) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + +function checkGoldenRequest(name: string, expectedCmdName: string, validate: (fields: any) => void) { + try { + const golden = fs.readFileSync(path.join(goldenDir, name)); + const decoded = decoder.unpack(golden) as any[]; + // Request format: [[commandName, {fields}]] + assertEqual(decoded.length, 1, `${name} array length`); + const [cmdName, fields] = decoded[0]; + assertEqual(cmdName, expectedCmdName, `${name} command name`); + validate(fields); + console.log(` PASS: ${name}`); + pass++; + } catch (e: any) { + console.log(` FAIL: ${name}: ${e.message}`); + fail++; + } +} + +function checkGoldenResponse(name: string, expectedRespName: string, validate: (fields: any) => void) { + try { + const golden = fs.readFileSync(path.join(goldenDir, name)); + const decoded = decoder.unpack(golden) as any[]; + // Response format: [responseName, {fields}] + assertEqual(decoded.length, 2, `${name} array length`); + const [respName, fields] = decoded; + assertEqual(respName, expectedRespName, `${name} response name`); + validate(fields); + console.log(` PASS: ${name}`); + pass++; + } catch (e: any) { + console.log(` FAIL: ${name}: ${e.message}`); + fail++; + } +} + +console.log('Golden file deserialization tests (TypeScript):\n'); + +// Request golden files +checkGoldenRequest('echo_bytes_request.msgpack', 'EchoBytes', (f) => { + if (!bufEqual(f.data, [0xDE, 0xAD, 0xBE, 0xEF, 0x42])) { + throw new Error('data mismatch'); + } +}); + +checkGoldenRequest('echo_fields_request.msgpack', 'EchoFields', (f) => { + assertEqual(f.a, 42, 'a'); + assertEqual(f.b, 999999, 'b'); + assertEqual(f.name, 'hello wire compat', 'name'); +}); + +checkGoldenRequest('echo_nested_request.msgpack', 'EchoNested', (f) => { + assertEqual(f.inner.flag, true, 'flag'); + assertEqual(f.inner.values.length, 2, 'values length'); + if (!bufEqual(f.inner.values[0], [1, 2, 3])) throw new Error('values[0] mismatch'); + if (!bufEqual(f.inner.values[1], [4, 5])) throw new Error('values[1] mismatch'); +}); + +// Response golden files +checkGoldenResponse('echo_bytes_response.msgpack', 'EchoBytesResponse', (f) => { + if (!bufEqual(f.data, [0xDE, 0xAD, 0xBE, 0xEF, 0x42])) { + throw new Error('data mismatch'); + } +}); + +checkGoldenResponse('echo_fields_response.msgpack', 'EchoFieldsResponse', (f) => { + assertEqual(f.a, 42, 'a'); + assertEqual(f.b, 999999, 'b'); + assertEqual(f.name, 'hello wire compat', 'name'); +}); + +checkGoldenResponse('echo_nested_response.msgpack', 'EchoNestedResponse', (f) => { + assertEqual(f.inner.flag, true, 'flag'); + assertEqual(f.inner.values.length, 2, 'values length'); + if (!bufEqual(f.inner.values[0], [1, 2, 3])) throw new Error('values[0] mismatch'); + if (!bufEqual(f.inner.values[1], [4, 5])) throw new Error('values[1] mismatch'); +}); + +console.log(`\nResults: ${pass}/${pass + fail} passed, ${fail} failed`); +if (fail > 0) process.exit(1); diff --git a/ipc-codegen/examples/ts/echo/package.json b/ipc-codegen/examples/ts/echo/package.json new file mode 100644 index 000000000000..3f4aaee24171 --- /dev/null +++ b/ipc-codegen/examples/ts/echo/package.json @@ -0,0 +1,8 @@ +{ + "name": "echo-wire-compat-ts", + "private": true, + "type": "module", + "dependencies": { + "msgpackr": "^1.10.0" + } +} diff --git a/ipc-codegen/examples/zig/echo/build.zig b/ipc-codegen/examples/zig/echo/build.zig new file mode 100644 index 000000000000..bf3f9a6f61d5 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/build.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const msgpack_dep = b.dependency("zig_msgpack", .{ + .target = target, + .optimize = optimize, + }); + const msgpack_mod = msgpack_dep.module("msgpack"); + + // Echo server + const server_exe = b.addExecutable(.{ + .name = "echo_server", + .root_module = b.createModule(.{ + .root_source_file = b.path("echo_server.zig"), + .target = target, + .optimize = optimize, + }), + }); + server_exe.root_module.addImport("msgpack", msgpack_mod); + b.installArtifact(server_exe); + + // Echo client + const client_exe = b.addExecutable(.{ + .name = "echo_client", + .root_module = b.createModule(.{ + .root_source_file = b.path("echo_client.zig"), + .target = target, + .optimize = optimize, + }), + }); + client_exe.root_module.addImport("msgpack", msgpack_mod); + b.installArtifact(client_exe); +} diff --git a/ipc-codegen/examples/zig/echo/build.zig.zon b/ipc-codegen/examples/zig/echo/build.zig.zon new file mode 100644 index 000000000000..0a16193fa043 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/build.zig.zon @@ -0,0 +1,19 @@ +.{ + .name = .echo_zig, + .version = "0.1.0", + .fingerprint = 0x15539c02bc3573e2, + .minimum_zig_version = "0.14.0", + .dependencies = .{ + .zig_msgpack = .{ + .url = "https://github.com/zigcc/zig-msgpack/archive/refs/heads/main.tar.gz", + .hash = "zig_msgpack-0.0.14-evvueIkqBQA7F_-8hpmPYvAjmg9MOWJLm6p8sb0HnGem", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "echo_server.zig", + "echo_client.zig", + "generated", + }, +} diff --git a/ipc-codegen/examples/zig/echo/echo_client.zig b/ipc-codegen/examples/zig/echo/echo_client.zig new file mode 100644 index 000000000000..9281ea21d764 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/echo_client.zig @@ -0,0 +1,69 @@ +/// Echo IPC client (Zig) — uses GENERATED typed client + UDS backend. +/// Usage: echo_client --socket /tmp/echo.sock +const std = @import("std"); +const echo_client = @import("generated/echo_client.zig"); +const uds_backend = @import("generated/uds_backend.zig"); +const types = @import("generated/echo_types.zig"); + +pub fn main() !void { + var args = std.process.args(); + _ = args.next(); + var socket_path: ?[]const u8 = null; + while (args.next()) |arg| { + if (std.mem.eql(u8, arg, "--socket")) { + socket_path = args.next(); + } + } + const path = socket_path orelse { + std.debug.print("Usage: echo_client --socket \n", .{}); + std.process.exit(1); + }; + + var backend = try uds_backend.UdsBackend.connect(path); + const EchoClient = echo_client.Client(uds_backend.UdsBackend); + var client = EchoClient.init(&backend); + + // Test 1: EchoBytes + { + const cmd = types.EchoBytes{ .data = &[_]u8{ 0xDE, 0xAD, 0xBE, 0xEF, 0x42 } }; + const resp = try client.bytes(cmd); + if (!std.mem.eql(u8, resp.data, &[_]u8{ 0xDE, 0xAD, 0xBE, 0xEF, 0x42 })) { + std.debug.print("echo_client(zig): EchoBytes FAIL\n", .{}); + std.process.exit(1); + } + std.debug.print("echo_client(zig): EchoBytes OK\n", .{}); + } + + // Test 2: EchoFields + { + const cmd = types.EchoFields{ .a = 42, .b = 999999, .name = "hello wire compat" }; + const resp = try client.fields(cmd); + if (resp.a != 42 or resp.b != 999999 or !std.mem.eql(u8, resp.name, "hello wire compat")) { + std.debug.print("echo_client(zig): EchoFields FAIL\n", .{}); + std.process.exit(1); + } + std.debug.print("echo_client(zig): EchoFields OK\n", .{}); + } + + // Test 3: EchoNested + { + const values = &[_][]const u8{ &[_]u8{ 1, 2, 3 }, &[_]u8{ 4, 5 } }; + const cmd = types.EchoNested{ + .inner = types.EchoInner{ .values = values, .flag = true }, + }; + const resp = try client.nested(cmd); + if (resp.inner.values.len != 2) { + std.debug.print("echo_client(zig): EchoNested FAIL\n", .{}); + std.process.exit(1); + } + if (resp.inner.flag != true) { + std.debug.print("echo_client(zig): EchoNested flag FAIL\n", .{}); + std.process.exit(1); + } + std.debug.print("echo_client(zig): EchoNested OK\n", .{}); + } + + // Shutdown + try client.shutdown(); + std.debug.print("echo_client(zig): all tests passed\n", .{}); +} diff --git a/ipc-codegen/examples/zig/echo/echo_server.zig b/ipc-codegen/examples/zig/echo/echo_server.zig new file mode 100644 index 000000000000..ad9071b39cb1 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/echo_server.zig @@ -0,0 +1,63 @@ +/// Echo IPC server (Zig) — uses GENERATED types + IPC server template. +/// Hand-written dispatch + echo handlers. +/// Usage: echo_server --socket /tmp/echo.sock +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const types = @import("generated/echo_types.zig"); +const ipc_server = @import("generated/ipc_server.zig"); + +const alloc = std.heap.page_allocator; + +pub fn main() !void { + var args = std.process.args(); + _ = args.next(); + var socket_path: ?[]const u8 = null; + while (args.next()) |arg| { + if (std.mem.eql(u8, arg, "--socket")) { + socket_path = args.next(); + } + } + const path = socket_path orelse { + std.debug.print("Usage: echo_server --socket \n", .{}); + return error.InvalidArgument; + }; + + try ipc_server.serve(path, dispatch); +} + +fn dispatch(cmd_name: []const u8, fields: Payload) ipc_server.DispatchResult { + // Shutdown + if (std.mem.eql(u8, cmd_name, "EchoShutdown")) { + return .{ .resp_name = "EchoShutdownResponse", .resp_payload = Payload.mapPayload(alloc) }; + } + + // EchoBytes — echo back + if (std.mem.eql(u8, cmd_name, "EchoBytes")) { + const cmd = types.EchoBytes.fromPayload(fields) catch return makeError("deser failed"); + const resp = types.EchoBytesResponse{ .data = cmd.data }; + return .{ .resp_name = "EchoBytesResponse", .resp_payload = resp.toPayload(alloc) }; + } + + // EchoFields — echo back + if (std.mem.eql(u8, cmd_name, "EchoFields")) { + const cmd = types.EchoFields.fromPayload(fields) catch return makeError("deser failed"); + const resp = types.EchoFieldsResponse{ .a = cmd.a, .b = cmd.b, .name = cmd.name }; + return .{ .resp_name = "EchoFieldsResponse", .resp_payload = resp.toPayload(alloc) }; + } + + // EchoNested — echo back + if (std.mem.eql(u8, cmd_name, "EchoNested")) { + const cmd = types.EchoNested.fromPayload(fields) catch return makeError("deser failed"); + const resp = types.EchoNestedResponse{ .inner = cmd.inner }; + return .{ .resp_name = "EchoNestedResponse", .resp_payload = resp.toPayload(alloc) }; + } + + return makeError("unknown command"); +} + +fn makeError(message: []const u8) ipc_server.DispatchResult { + var err_map = Payload.mapPayload(alloc); + err_map.mapPut("message", Payload.strToPayload(message, alloc) catch return .{ .resp_name = "EchoErrorResponse", .resp_payload = Payload.mapPayload(alloc) }) catch {}; + return .{ .resp_name = "EchoErrorResponse", .resp_payload = err_map }; +} diff --git a/ipc-codegen/examples/zig/echo/generated/backend.zig b/ipc-codegen/examples/zig/echo/generated/backend.zig new file mode 100644 index 000000000000..97a65bdbfa07 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/generated/backend.zig @@ -0,0 +1,27 @@ +/// Backend abstraction — comptime interface for transport. +/// +/// A valid backend type must provide: +/// fn call(self: *T, request: []const u8) ![]u8 +/// fn destroy(self: *T) void +/// +/// Implementations: +/// UdsBackend (uds_backend.zig) — Unix Domain Socket IPC +/// FfiBackend (ffi_backend.zig) — Direct C FFI linking +/// +/// Usage with the generated client: +/// const Client = @import("wsdb_client.zig").Client; +/// const UdsBackend = @import("uds_backend.zig").UdsBackend; +/// var backend = try UdsBackend.connect("/tmp/wsdb.sock"); +/// var client = Client(UdsBackend){ .backend = &backend }; + +/// Compile-time check that a type satisfies the backend interface. +pub fn assertBackend(comptime T: type) void { + // Must have: fn call(self: *T, request: []const u8) ![]u8 + if (!@hasDecl(T, "call")) { + @compileError("Backend type " ++ @typeName(T) ++ " missing 'call' method"); + } + // Must have: fn destroy(self: *T) void + if (!@hasDecl(T, "destroy")) { + @compileError("Backend type " ++ @typeName(T) ++ " missing 'destroy' method"); + } +} diff --git a/ipc-codegen/examples/zig/echo/generated/echo_client.zig b/ipc-codegen/examples/zig/echo/generated/echo_client.zig new file mode 100644 index 000000000000..c06d06f9c33b --- /dev/null +++ b/ipc-codegen/examples/zig/echo/generated/echo_client.zig @@ -0,0 +1,94 @@ +//! AUTOGENERATED - DO NOT EDIT +//! Echo client — typed methods parameterized on a backend type. +//! +//! The backend must satisfy: call(self, request: []const u8) ![]u8 and destroy(self) void. +//! See backend.zig for the interface contract. +//! Implementations: UdsBackend (uds_backend.zig), FfiBackend (ffi_backend.zig). + +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const types = @import("echo_types.zig"); +const backend_mod = @import("backend.zig"); + +const alloc = std.heap.page_allocator; + +pub fn Client(comptime BackendType: type) type { + comptime backend_mod.assertBackend(BackendType); + + return struct { + const Self = @This(); + backend: *BackendType, + + pub fn init(backend: *BackendType) Self { + return .{ .backend = backend }; + } + + pub fn destroy(self: *Self) void { + self.backend.destroy(); + } + + pub fn shutdown(self: *Self) !void { + const request_bytes = try Self.encode("EchoShutdown", Payload.mapPayload(alloc)); + defer alloc.free(request_bytes); + const response_bytes = try self.backend.call(request_bytes); + alloc.free(response_bytes); + } + + pub fn bytes(self: *Self, cmd: types.EchoBytes) !types.EchoBytesResponse { + const request_bytes = try Self.encode("EchoBytes", try cmd.toPayload(alloc)); + defer alloc.free(request_bytes); + const response_bytes = try self.backend.call(request_bytes); + defer alloc.free(response_bytes); + const resp_name, const resp_payload = try Self.decode(response_bytes); + if (std.mem.eql(u8, resp_name, "EchoErrorResponse")) return error.ServerError; + return try types.EchoBytesResponse.fromPayload(resp_payload); + } + + pub fn fields(self: *Self, cmd: types.EchoFields) !types.EchoFieldsResponse { + const request_bytes = try Self.encode("EchoFields", try cmd.toPayload(alloc)); + defer alloc.free(request_bytes); + const response_bytes = try self.backend.call(request_bytes); + defer alloc.free(response_bytes); + const resp_name, const resp_payload = try Self.decode(response_bytes); + if (std.mem.eql(u8, resp_name, "EchoErrorResponse")) return error.ServerError; + return try types.EchoFieldsResponse.fromPayload(resp_payload); + } + + pub fn nested(self: *Self, cmd: types.EchoNested) !types.EchoNestedResponse { + const request_bytes = try Self.encode("EchoNested", try cmd.toPayload(alloc)); + defer alloc.free(request_bytes); + const response_bytes = try self.backend.call(request_bytes); + defer alloc.free(response_bytes); + const resp_name, const resp_payload = try Self.decode(response_bytes); + if (std.mem.eql(u8, resp_name, "EchoErrorResponse")) return error.ServerError; + return try types.EchoNestedResponse.fromPayload(resp_payload); + } + + // --- internal helpers --- + + fn encode(cmd_name: []const u8, cmd_fields: Payload) ![]u8 { + var inner = try Payload.arrPayload(2, alloc); + try inner.setArrElement(0, try Payload.strToPayload(cmd_name, alloc)); + try inner.setArrElement(1, cmd_fields); + var outer = try Payload.arrPayload(1, alloc); + try outer.setArrElement(0, inner); + + var allocating_writer = std.Io.Writer.Allocating.init(alloc); + var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); + try packer.write(outer); + return try allocating_writer.toOwnedSlice(); + } + + fn decode(response_bytes: []const u8) !struct { []const u8, Payload } { + var reader = std.Io.Reader.fixed(response_bytes); + var unpacker = msgpack.PackerIO.init(&reader, undefined); + const resp = try unpacker.read(alloc); + const resp_len = try resp.getArrLen(); + if (resp_len != 2) return error.InvalidResponse; + const name = try (try resp.getArrElement(0)).asStr(); + const payload = try resp.getArrElement(1); + return .{ name, payload }; + } + }; +} diff --git a/ipc-codegen/examples/zig/echo/generated/echo_server.zig b/ipc-codegen/examples/zig/echo/generated/echo_server.zig new file mode 100644 index 000000000000..f6da558629e4 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/generated/echo_server.zig @@ -0,0 +1,72 @@ +//! AUTOGENERATED - DO NOT EDIT +//! Echo IPC server — dispatch + stub handlers over generic IPC transport. +//! +//! Implement the handler functions below to build a working Echo service. +//! Then call: serve("socket_path") + +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const types = @import("echo_types.zig"); +const ipc_server = @import("ipc_server.zig"); + +const alloc = std.heap.page_allocator; + +/// Start the Echo server on the given socket path. +pub fn serve(socket_path: []const u8) !void { + try ipc_server.serve(socket_path, dispatch); +} + +fn dispatch(cmd_name: []const u8, cmd_fields: Payload) ipc_server.DispatchResult { + // Shutdown + if (std.mem.eql(u8, cmd_name, "EchoShutdown")) { + return .{ .resp_name = "EchoShutdownResponse", .resp_payload = Payload.mapPayload(alloc) }; + } + + // Command dispatch + if (std.mem.eql(u8, cmd_name, "EchoBytes")) { + const cmd = types.EchoBytes.fromPayload(cmd_fields) catch return makeError("deser failed"); + const resp = bytes(cmd) catch return makeError("not implemented: EchoBytes"); + return .{ .resp_name = "EchoBytesResponse", .resp_payload = resp.toPayload(alloc) }; + } + if (std.mem.eql(u8, cmd_name, "EchoFields")) { + const cmd = types.EchoFields.fromPayload(cmd_fields) catch return makeError("deser failed"); + const resp = fields(cmd) catch return makeError("not implemented: EchoFields"); + return .{ .resp_name = "EchoFieldsResponse", .resp_payload = resp.toPayload(alloc) }; + } + if (std.mem.eql(u8, cmd_name, "EchoNested")) { + const cmd = types.EchoNested.fromPayload(cmd_fields) catch return makeError("deser failed"); + const resp = nested(cmd) catch return makeError("not implemented: EchoNested"); + return .{ .resp_name = "EchoNestedResponse", .resp_payload = resp.toPayload(alloc) }; + } + + return makeError("unknown command"); +} + +fn makeError(message: []const u8) ipc_server.DispatchResult { + var err_map = Payload.mapPayload(alloc); + err_map.mapPut("message", Payload.strToPayload(message, alloc) catch return .{ .resp_name = "EchoErrorResponse", .resp_payload = Payload.mapPayload(alloc) }) catch {}; + return .{ .resp_name = "EchoErrorResponse", .resp_payload = err_map }; +} + +// --------------------------------------------------------------------------- +// Handler stubs — implement these to build your Echo service. +// --------------------------------------------------------------------------- + +/// TODO: implement EchoBytes +fn bytes(cmd: types.EchoBytes) !types.EchoBytesResponse { + _ = cmd; + return error.NotImplemented; +} + +/// TODO: implement EchoFields +fn fields(cmd: types.EchoFields) !types.EchoFieldsResponse { + _ = cmd; + return error.NotImplemented; +} + +/// TODO: implement EchoNested +fn nested(cmd: types.EchoNested) !types.EchoNestedResponse { + _ = cmd; + return error.NotImplemented; +} diff --git a/ipc-codegen/examples/zig/echo/generated/echo_types.zig b/ipc-codegen/examples/zig/echo/generated/echo_types.zig new file mode 100644 index 000000000000..9ecd3bd833a6 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/generated/echo_types.zig @@ -0,0 +1,239 @@ +//! AUTOGENERATED - DO NOT EDIT +//! Generated from Aztec IPC msgpack schema +//! +//! Each struct has toPayload() and fromPayload() methods that convert +//! to/from zig-msgpack Payload objects for serialization. + +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const PackerIO = msgpack.PackerIO; + +/// Schema version hash for compatibility checking +pub const SCHEMA_HASH = "bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc"; + +/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. +pub const Fr = [32]u8; + +// --------------------------------------------------------------------------- +// Type definitions +// --------------------------------------------------------------------------- + +/// EchoInner +pub const EchoInner = struct { + values: []const []const u8, + flag: ?bool, + + pub fn toPayload(self: EchoInner, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("values", blk: { + var arr = try Payload.arrPayload(self.values.len, allocator); + for (self.values, 0..) |item, i| { + try arr.setArrElement(i, try Payload.binToPayload(item, allocator)); + } + break :blk arr; + }); + try map.mapPut("flag", if (self.flag) |v| Payload{ .bool = v } else Payload{ .nil = {} }); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoInner { + return EchoInner{ + .values = blk: { + const arr_len = try (try payload.mapGet("values")).?.getArrLen(); + var result = try std.heap.page_allocator.alloc([]const u8, arr_len); + for (0..arr_len) |i| { + const elem = try (try payload.mapGet("values")).?.getArrElement(i); + result[i] = elem.bin.value(); + } + break :blk result; + }, + .flag = if ((try payload.mapGet("flag")).? == .nil) null else try (try payload.mapGet("flag")).?.asBool(), + }; + } +}; + +/// EchoBytes +pub const EchoBytes = struct { + data: []const u8, + + pub fn toPayload(self: EchoBytes, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("data", try Payload.binToPayload(self.data, allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoBytes { + return EchoBytes{ + .data = (try payload.mapGet("data")).?.bin.value(), + }; + } +}; + +/// EchoFields +pub const EchoFields = struct { + a: u32, + b: u64, + name: []const u8, + + pub fn toPayload(self: EchoFields, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("a", Payload{ .uint = @intCast(self.a) }); + try map.mapPut("b", Payload{ .uint = @intCast(self.b) }); + try map.mapPut("name", try Payload.strToPayload(self.name, allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoFields { + return EchoFields{ + .a = @intCast(try (try payload.mapGet("a")).?.asUint()), + .b = try (try payload.mapGet("b")).?.asUint(), + .name = try (try payload.mapGet("name")).?.asStr(), + }; + } +}; + +/// EchoNested +pub const EchoNested = struct { + inner: EchoInner, + + pub fn toPayload(self: EchoNested, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("inner", try self.inner.toPayload(allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoNested { + return EchoNested{ + .inner = try EchoInner.fromPayload((try payload.mapGet("inner")).?), + }; + } +}; + +/// EchoShutdown +pub const EchoShutdown = struct { + + pub fn toPayload(_: EchoShutdown, allocator: std.mem.Allocator) !Payload { + return Payload.mapPayload(allocator); + } + + pub fn fromPayload(_: Payload) !EchoShutdown { + return EchoShutdown{}; + } +}; + +/// EchoBytesResponse +pub const EchoBytesResponse = struct { + data: []const u8, + + pub fn toPayload(self: EchoBytesResponse, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("data", try Payload.binToPayload(self.data, allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoBytesResponse { + return EchoBytesResponse{ + .data = (try payload.mapGet("data")).?.bin.value(), + }; + } +}; + +/// EchoFieldsResponse +pub const EchoFieldsResponse = struct { + a: u32, + b: u64, + name: []const u8, + + pub fn toPayload(self: EchoFieldsResponse, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("a", Payload{ .uint = @intCast(self.a) }); + try map.mapPut("b", Payload{ .uint = @intCast(self.b) }); + try map.mapPut("name", try Payload.strToPayload(self.name, allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoFieldsResponse { + return EchoFieldsResponse{ + .a = @intCast(try (try payload.mapGet("a")).?.asUint()), + .b = try (try payload.mapGet("b")).?.asUint(), + .name = try (try payload.mapGet("name")).?.asStr(), + }; + } +}; + +/// EchoNestedResponse +pub const EchoNestedResponse = struct { + inner: EchoInner, + + pub fn toPayload(self: EchoNestedResponse, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("inner", try self.inner.toPayload(allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoNestedResponse { + return EchoNestedResponse{ + .inner = try EchoInner.fromPayload((try payload.mapGet("inner")).?), + }; + } +}; + +/// EchoShutdownResponse +pub const EchoShutdownResponse = struct { + + pub fn toPayload(_: EchoShutdownResponse, allocator: std.mem.Allocator) !Payload { + return Payload.mapPayload(allocator); + } + + pub fn fromPayload(_: Payload) !EchoShutdownResponse { + return EchoShutdownResponse{}; + } +}; + +/// EchoErrorResponse +pub const EchoErrorResponse = struct { + message: []const u8, + + pub fn toPayload(self: EchoErrorResponse, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); + try map.mapPut("message", try Payload.strToPayload(self.message, allocator)); + return map; + } + + pub fn fromPayload(payload: Payload) !EchoErrorResponse { + return EchoErrorResponse{ + .message = try (try payload.mapGet("message")).?.asStr(), + }; + } +}; + +// --------------------------------------------------------------------------- +// Command / Response unions +// --------------------------------------------------------------------------- + +/// Tagged union of all commands +pub const Command = union(enum) { + echo_bytes: EchoBytes, + echo_fields: EchoFields, + echo_nested: EchoNested, + echo_shutdown: EchoShutdown, + + pub fn schemaName(self: Command) []const u8 { + return switch (self) { + .echo_bytes => "EchoBytes", + .echo_fields => "EchoFields", + .echo_nested => "EchoNested", + .echo_shutdown => "EchoShutdown", + }; + } +}; + +/// Tagged union of all responses +pub const Response = union(enum) { + echo_bytes_response: EchoBytesResponse, + echo_fields_response: EchoFieldsResponse, + echo_nested_response: EchoNestedResponse, + echo_shutdown_response: EchoShutdownResponse, + echo_error_response: EchoErrorResponse, +}; diff --git a/ipc-codegen/examples/zig/echo/generated/ipc_server.zig b/ipc-codegen/examples/zig/echo/generated/ipc_server.zig new file mode 100644 index 000000000000..ba58a09b5a61 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/generated/ipc_server.zig @@ -0,0 +1,112 @@ +/// Generic IPC server over Unix Domain Sockets. +/// Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. +/// Service-specific dispatch is injected via the DispatchFn parameter. +const std = @import("std"); +const posix = std.posix; +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; + +const alloc = std.heap.page_allocator; + +pub const DispatchFn = *const fn (cmd_name: []const u8, fields: Payload) DispatchResult; +pub const DispatchResult = struct { resp_name: []const u8, resp_payload: anyerror!Payload }; + +/// Run an IPC server on the given UDS path. +/// Accepts one connection, serves requests until shutdown or disconnect. +pub fn serve(socket_path: []const u8, dispatch: DispatchFn) !void { + std.fs.cwd().deleteFile(socket_path) catch {}; + + const address = try std.net.Address.initUnix(socket_path); + const server_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); + defer posix.close(server_fd); + try posix.bind(server_fd, &address.any, address.getOsSockLen()); + try posix.listen(server_fd, 1); + + std.debug.print("ipc-server: listening on {s}\n", .{socket_path}); + + const client_fd = try posix.accept(server_fd, null, null, 0); + defer posix.close(client_fd); + + while (true) { + const frame = recvFrame(client_fd) catch break; + defer alloc.free(frame); + + // Decode msgpack: [[commandName, {fields}]] + var reader = std.Io.Reader.fixed(frame); + var packer = msgpack.PackerIO.init(&reader, undefined); + const request = packer.read(alloc) catch break; + + const outer_len = request.getArrLen() catch break; + if (outer_len != 1) break; + + const inner = request.getArrElement(0) catch break; + const inner_len = inner.getArrLen() catch break; + if (inner_len != 2) break; + + const cmd_name = (inner.getArrElement(0) catch break).asStr() catch break; + const fields = inner.getArrElement(1) catch break; + + // Dispatch + const result = dispatch(cmd_name, fields); + const resp_payload = result.resp_payload catch blk: { + var err_map = Payload.mapPayload(alloc); + const msg = std.fmt.allocPrint(alloc, "error: {s}", .{cmd_name}) catch "error"; + err_map.mapPut("message", Payload.strToPayload(msg, alloc) catch break) catch break; + break :blk err_map; + }; + const is_error = if (result.resp_payload) |_| false else |_| true; + const resp_name = if (is_error) "ErrorResponse" else result.resp_name; + + const response = encodeResponse(resp_name, resp_payload) catch break; + defer alloc.free(response); + sendFrame(client_fd, response) catch break; + + // Check for shutdown + if (std.mem.indexOf(u8, cmd_name, "Shutdown") != null) break; + } + + std.fs.cwd().deleteFile(socket_path) catch {}; + std.debug.print("ipc-server: shutdown\n", .{}); +} + +fn encodeResponse(name: []const u8, payload: Payload) ![]u8 { + var resp_arr = try Payload.arrPayload(2, alloc); + try resp_arr.setArrElement(0, try Payload.strToPayload(name, alloc)); + try resp_arr.setArrElement(1, payload); + + var allocating_writer = std.Io.Writer.Allocating.init(alloc); + var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); + try packer.write(resp_arr); + return try allocating_writer.toOwnedSlice(); +} + +fn recvFrame(fd: posix.socket_t) ![]u8 { + var hdr: [4]u8 = undefined; + var got: usize = 0; + while (got < 4) { + const n = try posix.read(fd, hdr[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); + const data = try alloc.alloc(u8, len); + got = 0; + while (got < len) { + const n = try posix.read(fd, data[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + return data; +} + +fn sendFrame(fd: posix.socket_t, data: []const u8) !void { + const len: u32 = @intCast(data.len); + const header = [4]u8{ + @intCast(len & 0xFF), + @intCast((len >> 8) & 0xFF), + @intCast((len >> 16) & 0xFF), + @intCast((len >> 24) & 0xFF), + }; + _ = try posix.write(fd, &header); + _ = try posix.write(fd, data); +} diff --git a/ipc-codegen/examples/zig/echo/generated/uds_backend.zig b/ipc-codegen/examples/zig/echo/generated/uds_backend.zig new file mode 100644 index 000000000000..af701e2d05c9 --- /dev/null +++ b/ipc-codegen/examples/zig/echo/generated/uds_backend.zig @@ -0,0 +1,62 @@ +/// UDS (Unix Domain Socket) backend for IPC communication. +/// Handles: socket connect, length-prefixed framing, raw byte send/receive. +/// Satisfies the backend interface: call(request) -> response, destroy(). +const std = @import("std"); +const posix = std.posix; + +const alloc = std.heap.page_allocator; + +pub const UdsBackend = struct { + fd: posix.socket_t, + + /// Connect to a service at the given UDS path. + pub fn connect(socket_path: []const u8) !UdsBackend { + const address = try std.net.Address.initUnix(socket_path); + const fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); + try posix.connect(fd, &address.any, address.getOsSockLen()); + return .{ .fd = fd }; + } + + /// Send a raw msgpack request and receive a raw msgpack response. + /// Framing: 4-byte LE length prefix + payload. + pub fn call(self: *UdsBackend, request: []const u8) ![]u8 { + try sendFrame(self.fd, request); + return try recvFrame(self.fd); + } + + /// Close the connection. + pub fn destroy(self: *UdsBackend) void { + posix.close(self.fd); + } + + fn sendFrame(fd: posix.socket_t, data: []const u8) !void { + const len: u32 = @intCast(data.len); + const header = [4]u8{ + @intCast(len & 0xFF), + @intCast((len >> 8) & 0xFF), + @intCast((len >> 16) & 0xFF), + @intCast((len >> 24) & 0xFF), + }; + _ = try posix.write(fd, &header); + _ = try posix.write(fd, data); + } + + fn recvFrame(fd: posix.socket_t) ![]u8 { + var hdr: [4]u8 = undefined; + var got: usize = 0; + while (got < 4) { + const n = try posix.read(fd, hdr[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); + const data = try alloc.alloc(u8, len); + got = 0; + while (got < len) { + const n = try posix.read(fd, data[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + return data; + } +}; diff --git a/ipc-codegen/examples/zig/wsdb/.gitignore b/ipc-codegen/examples/zig/wsdb/.gitignore new file mode 100644 index 000000000000..647d2f5a5919 --- /dev/null +++ b/ipc-codegen/examples/zig/wsdb/.gitignore @@ -0,0 +1,3 @@ +src/generated/ +.zig-cache/ +zig-out/ diff --git a/ipc-codegen/examples/zig/wsdb/README.md b/ipc-codegen/examples/zig/wsdb/README.md new file mode 100644 index 000000000000..af39477f79ed --- /dev/null +++ b/ipc-codegen/examples/zig/wsdb/README.md @@ -0,0 +1,93 @@ +# Zig WSDB Server Example + +A skeleton WSDB (World State Database) implementation in Zig, using +generated types and serialization from the Aztec IPC codegen tool. + +All command handlers return "not implemented" errors. Replace them with +your world-state logic to build a working WSDB. + +## Prerequisites + +- [Zig](https://ziglang.org/) 0.15+ +- [Node.js](https://nodejs.org/) 22+ (for the codegen step) + +## Build & Run + +```bash +# 1. Generate types from the committed WSDB schema +./generate.sh + +# 2. Build +zig build + +# 3. Run +./zig-out/bin/zig-wsdb --socket /tmp/wsdb.sock +``` + +## How It Works + +``` +generate.sh calls: + codegen/src/generate.ts --schema wsdb_schema.json --lang zig --prefix Wsdb --out src/generated/ + + produces: src/generated/types.zig (WSDB command/response structs) + src/generated/server.zig (server dispatch vtable) + +src/main.zig + UDS server → recv frame → decode msgpack → dispatch by command name + → deserialize fields into generated struct via fromPayload() + → call handler (your code) + → serialize response via toPayload() + → encode as NamedUnion [responseName, {fields}] + → send frame +``` + +## Implementing a Command + +Edit `handleCommand()` in `src/main.zig`: + +```zig +fn handleCommand(comptime CmdType: type, comptime RespType: type, cmd: CmdType) !RespType { + if (CmdType == types.WsdbGetTreeInfo) { + return types.WsdbGetTreeInfoResponse{ + .tree_id = cmd.tree_id, + .root = std.mem.zeroes(types.Fr), // 32-byte field element + .size = 0, + .depth = 32, + }; + } + return error.NotImplemented; +} +``` + +## Connecting a Client + +Any WSDB client that speaks the IPC protocol can connect — C++, TypeScript, Rust, or Zig. +The wire format is: 4-byte LE length prefix + msgpack payload. + +```bash +# Example: connect the existing C++ WSDB client +aztec-wsdb-client --socket /tmp/wsdb.sock +``` + +## Project Structure + +``` +├── generate.sh # Invokes codegen CLI to produce types +├── build.zig # Zig build file +├── build.zig.zon # Zig package manifest (depends on zig-msgpack) +├── src/ +│ ├── main.zig # Server: UDS, framing, dispatch, handlers +│ └── generated/ # AUTOGENERATED (gitignored) +│ ├── types.zig # All WSDB command/response structs +│ └── server.zig # Server dispatch vtable +└── README.md +``` + +## Schema Updates + +If the WSDB schema changes: +1. Someone updates `ipc-codegen/schemas/wsdb_schema.json` +2. Run `./generate.sh` to regenerate types +3. Fix any compilation errors from changed/removed commands +4. `zig build` diff --git a/ipc-codegen/examples/zig/wsdb/build.zig b/ipc-codegen/examples/zig/wsdb/build.zig new file mode 100644 index 000000000000..e287aaa276e1 --- /dev/null +++ b/ipc-codegen/examples/zig/wsdb/build.zig @@ -0,0 +1,31 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const msgpack_dep = b.dependency("zig_msgpack", .{ + .target = target, + .optimize = optimize, + }); + const msgpack_mod = msgpack_dep.module("msgpack"); + + const exe = b.addExecutable(.{ + .name = "zig-wsdb", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + exe.root_module.addImport("msgpack", msgpack_mod); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + const run_step = b.step("run", "Run the WSDB server"); + run_step.dependOn(&run_cmd.step); +} diff --git a/ipc-codegen/examples/zig/wsdb/build.zig.zon b/ipc-codegen/examples/zig/wsdb/build.zig.zon new file mode 100644 index 000000000000..1e4c2b8243ab --- /dev/null +++ b/ipc-codegen/examples/zig/wsdb/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .zig_wsdb, + .version = "0.1.0", + .fingerprint = 0xeb5cdc04a6a8770f, + .minimum_zig_version = "0.14.0", + .dependencies = .{ + .zig_msgpack = .{ + .url = "https://github.com/zigcc/zig-msgpack/archive/refs/heads/main.tar.gz", + .hash = "zig_msgpack-0.0.14-evvueIkqBQA7F_-8hpmPYvAjmg9MOWJLm6p8sb0HnGem", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/ipc-codegen/examples/zig/wsdb/generate.sh b/ipc-codegen/examples/zig/wsdb/generate.sh new file mode 100755 index 000000000000..9c9fe638c304 --- /dev/null +++ b/ipc-codegen/examples/zig/wsdb/generate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Generate WSDB Zig bindings from the committed schema. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CODEGEN="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +mkdir -p "$SCRIPT_DIR/src/generated" + +node --experimental-strip-types --experimental-transform-types --no-warnings \ + "$CODEGEN/src/generate.ts" \ + --schema "$CODEGEN/schemas/wsdb_schema.json" \ + --lang zig \ + --out "$SCRIPT_DIR/src/generated" \ + --server --client --uds --ffi \ + --prefix Wsdb diff --git a/ipc-codegen/examples/zig/wsdb/src/main.zig b/ipc-codegen/examples/zig/wsdb/src/main.zig new file mode 100644 index 000000000000..2e65067c85f4 --- /dev/null +++ b/ipc-codegen/examples/zig/wsdb/src/main.zig @@ -0,0 +1,24 @@ +/// Zig WSDB Server — uses generated server dispatch + generic IPC transport. +/// +/// All command handlers return "not implemented" errors. +/// Edit src/generated/server.zig to implement your world-state logic. +/// +/// Usage: zig-wsdb --socket /tmp/wsdb.sock +const server = @import("generated/server_gen.zig"); + +pub fn main() !void { + var args = @import("std").process.args(); + _ = args.next(); + var socket_path: ?[]const u8 = null; + while (args.next()) |arg| { + if (@import("std").mem.eql(u8, arg, "--socket")) { + socket_path = args.next(); + } + } + const path = socket_path orelse { + @import("std").debug.print("Usage: zig-wsdb --socket \n", .{}); + @import("std").process.exit(1); + }; + + try server.serve(path); +} diff --git a/ipc-codegen/schemas/avm_schema.json b/ipc-codegen/schemas/avm_schema.json new file mode 100644 index 000000000000..b1e8e050b4d5 --- /dev/null +++ b/ipc-codegen/schemas/avm_schema.json @@ -0,0 +1,2 @@ +{"__typename":"AvmApi","commands":["named_union",[["AvmSimulate",{"__typename":"AvmSimulate","inputs":["vector",["unsigned char"]]}],["AvmSimulateWithHints",{"__typename":"AvmSimulateWithHints","inputs":["vector",["unsigned char"]]}],["AvmShutdown",{"__typename":"AvmShutdown"}]]],"responses":["named_union",[["AvmErrorResponse",{"__typename":"AvmErrorResponse","message":"string"}],["AvmSimulateResponse",{"__typename":"AvmSimulateResponse","result":["vector",["unsigned char"]]}],["AvmSimulateWithHintsResponse",{"__typename":"AvmSimulateWithHintsResponse","result":["vector",["unsigned char"]]}],["AvmShutdownResponse",{"__typename":"AvmShutdownResponse"}]]]} + diff --git a/ipc-codegen/schemas/bb_curve_constants.json b/ipc-codegen/schemas/bb_curve_constants.json new file mode 100644 index 000000000000..20dab049c505 --- /dev/null +++ b/ipc-codegen/schemas/bb_curve_constants.json @@ -0,0 +1,36 @@ +{ + "bn254_fr_modulus": "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + "bn254_fq_modulus": "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", + "bn254_g1_generator": { + "x": "0000000000000000000000000000000000000000000000000000000000000001", + "y": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "bn254_g2_generator": { + "x": [ + "1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed", + "198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2" + ], + "y": [ + "12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa", + "090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b" + ] + }, + "grumpkin_fr_modulus": "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", + "grumpkin_fq_modulus": "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", + "grumpkin_g1_generator": { + "x": "0000000000000000000000000000000000000000000000000000000000000001", + "y": "0000000000000002cf135e7506a45d632d270d45f1181294833fc48d823f272c" + }, + "secp256k1_fr_modulus": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "secp256k1_fq_modulus": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + "secp256k1_g1_generator": { + "x": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "y": "483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" + }, + "secp256r1_fr_modulus": "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", + "secp256r1_fq_modulus": "ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", + "secp256r1_g1_generator": { + "x": "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", + "y": "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5" + } +} \ No newline at end of file diff --git a/ipc-codegen/schemas/bb_schema.json b/ipc-codegen/schemas/bb_schema.json new file mode 100644 index 000000000000..a8defb4d4052 --- /dev/null +++ b/ipc-codegen/schemas/bb_schema.json @@ -0,0 +1,2 @@ +{"__typename":"Api","commands":["named_union",[["BbCircuitProve",{"__typename":"BbCircuitProve","circuit":{"__typename":"CircuitInput","name":"string","bytecode":["vector",["unsigned char"]],"verification_key":["vector",["unsigned char"]]},"witness":["vector",["unsigned char"]],"settings":{"__typename":"ProofSystemSettings","ipa_accumulation":"bool","oracle_hash_type":"string","disable_zk":"bool","optimized_solidity_verifier":"bool"}}],["BbCircuitComputeVk",{"__typename":"BbCircuitComputeVk","circuit":{"__typename":"CircuitInputNoVK","name":"string","bytecode":["vector",["unsigned char"]]},"settings":"ProofSystemSettings"}],["BbCircuitStats",{"__typename":"BbCircuitStats","circuit":"CircuitInput","include_gates_per_opcode":"bool","settings":"ProofSystemSettings"}],["BbCircuitVerify",{"__typename":"BbCircuitVerify","verification_key":["vector",["unsigned char"]],"public_inputs":["vector",[["vector",["unsigned char"]]]],"proof":["vector",[["vector",["unsigned char"]]]],"settings":"ProofSystemSettings"}],["BbChonkComputeVk",{"__typename":"BbChonkComputeVk","circuit":"CircuitInputNoVK"}],["BbChonkStart",{"__typename":"BbChonkStart","num_circuits":"unsigned int"}],["BbChonkLoad",{"__typename":"BbChonkLoad","circuit":"CircuitInput"}],["BbChonkAccumulate",{"__typename":"BbChonkAccumulate","witness":["vector",["unsigned char"]]}],["BbChonkProve",{"__typename":"BbChonkProve"}],["BbChonkVerify",{"__typename":"BbChonkVerify","proof":{"__typename":"ChonkProof","hiding_oink_proof":["vector",[["array",["unsigned char",32]]]],"merge_proof":["vector",[["array",["unsigned char",32]]]],"eccvm_proof":["vector",[["array",["unsigned char",32]]]],"ipa_proof":["vector",[["array",["unsigned char",32]]]],"joint_proof":["vector",[["array",["unsigned char",32]]]]},"vk":["vector",["unsigned char"]]}],["BbChonkBatchVerify",{"__typename":"BbChonkBatchVerify","proofs":["vector",["ChonkProof"]],"vks":["vector",[["vector",["unsigned char"]]]]}],["BbVkAsFields",{"__typename":"BbVkAsFields","verification_key":["vector",["unsigned char"]]}],["BbMegaVkAsFields",{"__typename":"BbMegaVkAsFields","verification_key":["vector",["unsigned char"]]}],["BbCircuitWriteSolidityVerifier",{"__typename":"BbCircuitWriteSolidityVerifier","verification_key":["vector",["unsigned char"]],"settings":"ProofSystemSettings"}],["BbChonkCheckPrecomputedVk",{"__typename":"BbChonkCheckPrecomputedVk","circuit":"CircuitInput"}],["BbChonkStats",{"__typename":"BbChonkStats","circuit":"CircuitInputNoVK","include_gates_per_opcode":"bool"}],["BbChonkCompressProof",{"__typename":"BbChonkCompressProof","proof":"ChonkProof"}],["BbChonkDecompressProof",{"__typename":"BbChonkDecompressProof","compressed_proof":["vector",["unsigned char"]]}],["BbPoseidon2Hash",{"__typename":"BbPoseidon2Hash","inputs":["vector",[["array",["unsigned char",32]]]]}],["BbPoseidon2Permutation",{"__typename":"BbPoseidon2Permutation","inputs":["array",[["array",["unsigned char",32]],4]]}],["BbPedersenCommit",{"__typename":"BbPedersenCommit","inputs":["vector",[["array",["unsigned char",32]]]],"hash_index":"unsigned int"}],["BbPedersenHash",{"__typename":"BbPedersenHash","inputs":["vector",[["array",["unsigned char",32]]]],"hash_index":"unsigned int"}],["BbPedersenHashBuffer",{"__typename":"BbPedersenHashBuffer","input":["vector",["unsigned char"]],"hash_index":"unsigned int"}],["BbBlake2s",{"__typename":"BbBlake2s","data":["vector",["unsigned char"]]}],["BbBlake2sToField",{"__typename":"BbBlake2sToField","data":["vector",["unsigned char"]]}],["BbAesEncrypt",{"__typename":"BbAesEncrypt","plaintext":["vector",["unsigned char"]],"iv":["vector",["unsigned char"]],"key":["vector",["unsigned char"]],"length":"unsigned int"}],["BbAesDecrypt",{"__typename":"BbAesDecrypt","ciphertext":["vector",["unsigned char"]],"iv":["vector",["unsigned char"]],"key":["vector",["unsigned char"]],"length":"unsigned int"}],["BbGrumpkinMul",{"__typename":"BbGrumpkinMul","point":{"__typename":"GrumpkinPoint","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"scalar":["array",["unsigned char",32]]}],["BbGrumpkinAdd",{"__typename":"BbGrumpkinAdd","point_a":"GrumpkinPoint","point_b":"GrumpkinPoint"}],["BbGrumpkinBatchMul",{"__typename":"BbGrumpkinBatchMul","points":["vector",["GrumpkinPoint"]],"scalar":["array",["unsigned char",32]]}],["BbGrumpkinGetRandomFr",{"__typename":"BbGrumpkinGetRandomFr","points_buf":["vector",["unsigned char"]]}],["BbGrumpkinReduce512",{"__typename":"BbGrumpkinReduce512","input":["vector",["unsigned char"]]}],["BbSecp256k1Mul",{"__typename":"BbSecp256k1Mul","point":{"__typename":"Secp256k1Point","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"scalar":["array",["unsigned char",32]]}],["BbSecp256k1GetRandomFr",{"__typename":"BbSecp256k1GetRandomFr","points_buf":["vector",["unsigned char"]]}],["BbSecp256k1Reduce512",{"__typename":"BbSecp256k1Reduce512","input":["vector",["unsigned char"]]}],["BbBn254FrSqrt",{"__typename":"BbBn254FrSqrt","input":["array",["unsigned char",32]]}],["BbBn254FqSqrt",{"__typename":"BbBn254FqSqrt","input":["array",["unsigned char",32]]}],["BbBn254G1Mul",{"__typename":"BbBn254G1Mul","point":{"__typename":"Bn254G1Point","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"scalar":["array",["unsigned char",32]]}],["BbBn254G2Mul",{"__typename":"BbBn254G2Mul","point":{"__typename":"Bn254G2Point","x":["array",[["array",["unsigned char",32]],2]],"y":["array",[["array",["unsigned char",32]],2]]},"scalar":["array",["unsigned char",32]]}],["BbBn254G1IsOnCurve",{"__typename":"BbBn254G1IsOnCurve","point":"Bn254G1Point"}],["BbBn254G1FromCompressed",{"__typename":"BbBn254G1FromCompressed","compressed":["array",["unsigned char",32]]}],["BbSchnorrComputePublicKey",{"__typename":"BbSchnorrComputePublicKey","private_key":["array",["unsigned char",32]]}],["BbSchnorrConstructSignature",{"__typename":"BbSchnorrConstructSignature","message":["vector",["unsigned char"]],"private_key":["array",["unsigned char",32]]}],["BbSchnorrVerifySignature",{"__typename":"BbSchnorrVerifySignature","message":["vector",["unsigned char"]],"public_key":"GrumpkinPoint","s":["array",["unsigned char",32]],"e":["array",["unsigned char",32]]}],["BbEcdsaSecp256k1ComputePublicKey",{"__typename":"BbEcdsaSecp256k1ComputePublicKey","private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256r1ComputePublicKey",{"__typename":"BbEcdsaSecp256r1ComputePublicKey","private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256k1ConstructSignature",{"__typename":"BbEcdsaSecp256k1ConstructSignature","message":["vector",["unsigned char"]],"private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256r1ConstructSignature",{"__typename":"BbEcdsaSecp256r1ConstructSignature","message":["vector",["unsigned char"]],"private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256k1RecoverPublicKey",{"__typename":"BbEcdsaSecp256k1RecoverPublicKey","message":["vector",["unsigned char"]],"r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256r1RecoverPublicKey",{"__typename":"BbEcdsaSecp256r1RecoverPublicKey","message":["vector",["unsigned char"]],"r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256k1VerifySignature",{"__typename":"BbEcdsaSecp256k1VerifySignature","message":["vector",["unsigned char"]],"public_key":"Secp256k1Point","r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256r1VerifySignature",{"__typename":"BbEcdsaSecp256r1VerifySignature","message":["vector",["unsigned char"]],"public_key":{"__typename":"Secp256r1Point","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbSrsInitSrs",{"__typename":"BbSrsInitSrs","points_buf":["vector",["unsigned char"]],"num_points":"unsigned int","g2_point":["vector",["unsigned char"]]}],["BbChonkBatchVerifierStart",{"__typename":"BbChonkBatchVerifierStart","vks":["vector",[["vector",["unsigned char"]]]],"num_cores":"unsigned int","batch_size":"unsigned int","fifo_path":"string"}],["BbChonkBatchVerifierQueue",{"__typename":"BbChonkBatchVerifierQueue","request_id":"unsigned long","vk_index":"unsigned int","proof_fields":["vector",[["array",["unsigned char",32]]]]}],["BbChonkBatchVerifierStop",{"__typename":"BbChonkBatchVerifierStop"}],["BbSrsInitGrumpkinSrs",{"__typename":"BbSrsInitGrumpkinSrs","points_buf":["vector",["unsigned char"]],"num_points":"unsigned int"}],["BbShutdown",{"__typename":"BbShutdown"}]]],"responses":["named_union",[["BbErrorResponse",{"__typename":"BbErrorResponse","message":"string"}],["BbCircuitProveResponse",{"__typename":"BbCircuitProveResponse","public_inputs":["vector",[["vector",["unsigned char"]]]],"proof":["vector",[["vector",["unsigned char"]]]],"vk":{"__typename":"BbCircuitComputeVkResponse","bytes":["vector",["unsigned char"]],"fields":["vector",[["vector",["unsigned char"]]]],"hash":["vector",["unsigned char"]]}}],["BbCircuitComputeVkResponse","BbCircuitComputeVkResponse"],["BbCircuitInfoResponse",{"__typename":"BbCircuitInfoResponse","num_gates":"unsigned int","num_gates_dyadic":"unsigned int","num_acir_opcodes":"unsigned int","gates_per_opcode":["vector",["unsigned int"]]}],["BbCircuitVerifyResponse",{"__typename":"BbCircuitVerifyResponse","verified":"bool"}],["BbChonkComputeVkResponse",{"__typename":"BbChonkComputeVkResponse","bytes":["vector",["unsigned char"]],"fields":["vector",[["array",["unsigned char",32]]]]}],["BbChonkStartResponse",{"__typename":"BbChonkStartResponse"}],["BbChonkLoadResponse",{"__typename":"BbChonkLoadResponse"}],["BbChonkAccumulateResponse",{"__typename":"BbChonkAccumulateResponse"}],["BbChonkProveResponse",{"__typename":"BbChonkProveResponse","proof":"ChonkProof"}],["BbChonkVerifyResponse",{"__typename":"BbChonkVerifyResponse","valid":"bool"}],["BbChonkBatchVerifyResponse",{"__typename":"BbChonkBatchVerifyResponse","valid":"bool"}],["BbVkAsFieldsResponse",{"__typename":"BbVkAsFieldsResponse","fields":["vector",[["array",["unsigned char",32]]]]}],["BbMegaVkAsFieldsResponse",{"__typename":"BbMegaVkAsFieldsResponse","fields":["vector",[["array",["unsigned char",32]]]]}],["BbCircuitWriteSolidityVerifierResponse",{"__typename":"BbCircuitWriteSolidityVerifierResponse","solidity_code":"string"}],["BbChonkCheckPrecomputedVkResponse",{"__typename":"BbChonkCheckPrecomputedVkResponse","valid":"bool","actual_vk":["vector",["unsigned char"]]}],["BbChonkStatsResponse",{"__typename":"BbChonkStatsResponse","acir_opcodes":"unsigned int","circuit_size":"unsigned int","gates_per_opcode":["vector",["unsigned int"]]}],["BbChonkCompressProofResponse",{"__typename":"BbChonkCompressProofResponse","compressed_proof":["vector",["unsigned char"]]}],["BbChonkDecompressProofResponse",{"__typename":"BbChonkDecompressProofResponse","proof":"ChonkProof"}],["BbPoseidon2HashResponse",{"__typename":"BbPoseidon2HashResponse","hash":["array",["unsigned char",32]]}],["BbPoseidon2PermutationResponse",{"__typename":"BbPoseidon2PermutationResponse","outputs":["array",[["array",["unsigned char",32]],4]]}],["BbPedersenCommitResponse",{"__typename":"BbPedersenCommitResponse","point":"GrumpkinPoint"}],["BbPedersenHashResponse",{"__typename":"BbPedersenHashResponse","hash":["array",["unsigned char",32]]}],["BbPedersenHashBufferResponse",{"__typename":"BbPedersenHashBufferResponse","hash":["array",["unsigned char",32]]}],["BbBlake2sResponse",{"__typename":"BbBlake2sResponse","hash":["array",["unsigned char",32]]}],["BbBlake2sToFieldResponse",{"__typename":"BbBlake2sToFieldResponse","field":["array",["unsigned char",32]]}],["BbAesEncryptResponse",{"__typename":"BbAesEncryptResponse","ciphertext":["vector",["unsigned char"]]}],["BbAesDecryptResponse",{"__typename":"BbAesDecryptResponse","plaintext":["vector",["unsigned char"]]}],["BbGrumpkinMulResponse",{"__typename":"BbGrumpkinMulResponse","point":"GrumpkinPoint"}],["BbGrumpkinAddResponse",{"__typename":"BbGrumpkinAddResponse","point":"GrumpkinPoint"}],["BbGrumpkinBatchMulResponse",{"__typename":"BbGrumpkinBatchMulResponse","points":["vector",["GrumpkinPoint"]]}],["BbGrumpkinGetRandomFrResponse",{"__typename":"BbGrumpkinGetRandomFrResponse","value":["array",["unsigned char",32]]}],["BbGrumpkinReduce512Response",{"__typename":"BbGrumpkinReduce512Response","value":["array",["unsigned char",32]]}],["BbSecp256k1MulResponse",{"__typename":"BbSecp256k1MulResponse","point":"Secp256k1Point"}],["BbSecp256k1GetRandomFrResponse",{"__typename":"BbSecp256k1GetRandomFrResponse","value":["array",["unsigned char",32]]}],["BbSecp256k1Reduce512Response",{"__typename":"BbSecp256k1Reduce512Response","value":["array",["unsigned char",32]]}],["BbBn254FrSqrtResponse",{"__typename":"BbBn254FrSqrtResponse","is_square_root":"bool","value":["array",["unsigned char",32]]}],["BbBn254FqSqrtResponse",{"__typename":"BbBn254FqSqrtResponse","is_square_root":"bool","value":["array",["unsigned char",32]]}],["BbBn254G1MulResponse",{"__typename":"BbBn254G1MulResponse","point":"Bn254G1Point"}],["BbBn254G2MulResponse",{"__typename":"BbBn254G2MulResponse","point":"Bn254G2Point"}],["BbBn254G1IsOnCurveResponse",{"__typename":"BbBn254G1IsOnCurveResponse","is_on_curve":"bool"}],["BbBn254G1FromCompressedResponse",{"__typename":"BbBn254G1FromCompressedResponse","point":"Bn254G1Point"}],["BbSchnorrComputePublicKeyResponse",{"__typename":"BbSchnorrComputePublicKeyResponse","public_key":"GrumpkinPoint"}],["BbSchnorrConstructSignatureResponse",{"__typename":"BbSchnorrConstructSignatureResponse","s":["array",["unsigned char",32]],"e":["array",["unsigned char",32]]}],["BbSchnorrVerifySignatureResponse",{"__typename":"BbSchnorrVerifySignatureResponse","verified":"bool"}],["BbEcdsaSecp256k1ComputePublicKeyResponse",{"__typename":"BbEcdsaSecp256k1ComputePublicKeyResponse","public_key":"Secp256k1Point"}],["BbEcdsaSecp256r1ComputePublicKeyResponse",{"__typename":"BbEcdsaSecp256r1ComputePublicKeyResponse","public_key":"Secp256r1Point"}],["BbEcdsaSecp256k1ConstructSignatureResponse",{"__typename":"BbEcdsaSecp256k1ConstructSignatureResponse","r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256r1ConstructSignatureResponse",{"__typename":"BbEcdsaSecp256r1ConstructSignatureResponse","r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256k1RecoverPublicKeyResponse",{"__typename":"BbEcdsaSecp256k1RecoverPublicKeyResponse","public_key":"Secp256k1Point"}],["BbEcdsaSecp256r1RecoverPublicKeyResponse",{"__typename":"BbEcdsaSecp256r1RecoverPublicKeyResponse","public_key":"Secp256r1Point"}],["BbEcdsaSecp256k1VerifySignatureResponse",{"__typename":"BbEcdsaSecp256k1VerifySignatureResponse","verified":"bool"}],["BbEcdsaSecp256r1VerifySignatureResponse",{"__typename":"BbEcdsaSecp256r1VerifySignatureResponse","verified":"bool"}],["BbSrsInitSrsResponse",{"__typename":"BbSrsInitSrsResponse","points_buf":["vector",["unsigned char"]]}],["BbChonkBatchVerifierStartResponse",{"__typename":"BbChonkBatchVerifierStartResponse"}],["BbChonkBatchVerifierQueueResponse",{"__typename":"BbChonkBatchVerifierQueueResponse"}],["BbChonkBatchVerifierStopResponse",{"__typename":"BbChonkBatchVerifierStopResponse"}],["BbSrsInitGrumpkinSrsResponse",{"__typename":"BbSrsInitGrumpkinSrsResponse","points_buf":["vector",["unsigned char"]]}],["BbShutdownResponse",{"__typename":"BbShutdownResponse"}]]]} + diff --git a/ipc-codegen/schemas/cdb_schema.json b/ipc-codegen/schemas/cdb_schema.json new file mode 100644 index 000000000000..3e54b0e2035f --- /dev/null +++ b/ipc-codegen/schemas/cdb_schema.json @@ -0,0 +1,2 @@ +{"__typename":"CdbApi","commands":["named_union",[["CdbGetContractInstance",{"__typename":"CdbGetContractInstance","address":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbGetContractClass",{"__typename":"CdbGetContractClass","classId":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbGetBytecodeCommitment",{"__typename":"CdbGetBytecodeCommitment","classId":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbGetDebugFunctionName",{"__typename":"CdbGetDebugFunctionName","address":["array",["unsigned char",32]],"selector":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbAddContracts",{"__typename":"CdbAddContracts","contractDeploymentData":{"__typename":"ContractDeploymentData","contractClassLogs":["vector",[{"__typename":"ContractClassLog","contractAddress":["array",["unsigned char",32]],"fields":{"__typename":"ContractClassLogFields","fields":["vector",[["array",["unsigned char",32]]]]},"emittedLength":"unsigned int"}]],"privateLogs":["vector",[{"__typename":"PrivateLog","fields":["vector",[["array",["unsigned char",32]]]],"emittedLength":"unsigned int"}]]},"forkId":"unsigned long"}],["CdbCreateCheckpoint",{"__typename":"CdbCreateCheckpoint","forkId":"unsigned long"}],["CdbCommitCheckpoint",{"__typename":"CdbCommitCheckpoint","forkId":"unsigned long"}],["CdbRevertCheckpoint",{"__typename":"CdbRevertCheckpoint","forkId":"unsigned long"}],["CdbAddContractClass",{"__typename":"CdbAddContractClass","contractClass":{"__typename":"ContractClass","id":["array",["unsigned char",32]],"artifactHash":["array",["unsigned char",32]],"privateFunctionsRoot":["array",["unsigned char",32]],"packedBytecode":["vector",["unsigned char"]]},"bytecodeCommitment":["array",["unsigned char",32]]}],["CdbAddContractInstance",{"__typename":"CdbAddContractInstance","address":["array",["unsigned char",32]],"instance":{"__typename":"ContractInstance","salt":["array",["unsigned char",32]],"deployer":["array",["unsigned char",32]],"currentContractClassId":["array",["unsigned char",32]],"originalContractClassId":["array",["unsigned char",32]],"initializationHash":["array",["unsigned char",32]],"publicKeys":{"__typename":"PublicKeys","masterNullifierPublicKey":{"__typename":"GrumpkinPoint","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"masterIncomingViewingPublicKey":"GrumpkinPoint","masterOutgoingViewingPublicKey":"GrumpkinPoint","masterTaggingPublicKey":"GrumpkinPoint"}}}],["CdbRegisterFunctionSignatures",{"__typename":"CdbRegisterFunctionSignatures","signatures":["vector",["string"]]}],["CdbGetContractClassIds",{"__typename":"CdbGetContractClassIds"}],["CdbShutdown",{"__typename":"CdbShutdown"}]]],"responses":["named_union",[["CdbErrorResponse",{"__typename":"CdbErrorResponse","message":"string"}],["CdbGetContractInstanceResponse",{"__typename":"CdbGetContractInstanceResponse","instance":["optional",["ContractInstance"]]}],["CdbGetContractClassResponse",{"__typename":"CdbGetContractClassResponse","contractClass":["optional",["ContractClass"]]}],["CdbGetBytecodeCommitmentResponse",{"__typename":"CdbGetBytecodeCommitmentResponse","commitment":["optional",[["array",["unsigned char",32]]]]}],["CdbGetDebugFunctionNameResponse",{"__typename":"CdbGetDebugFunctionNameResponse","name":["optional",["string"]]}],["CdbAddContractsResponse",{"__typename":"CdbAddContractsResponse"}],["CdbCreateCheckpointResponse",{"__typename":"CdbCreateCheckpointResponse"}],["CdbCommitCheckpointResponse",{"__typename":"CdbCommitCheckpointResponse"}],["CdbRevertCheckpointResponse",{"__typename":"CdbRevertCheckpointResponse"}],["CdbAddContractClassResponse",{"__typename":"CdbAddContractClassResponse"}],["CdbAddContractInstanceResponse",{"__typename":"CdbAddContractInstanceResponse"}],["CdbRegisterFunctionSignaturesResponse",{"__typename":"CdbRegisterFunctionSignaturesResponse"}],["CdbGetContractClassIdsResponse",{"__typename":"CdbGetContractClassIdsResponse","classIds":["vector",[["array",["unsigned char",32]]]]}],["CdbShutdownResponse",{"__typename":"CdbShutdownResponse"}]]]} + diff --git a/ipc-codegen/schemas/wsdb_schema.json b/ipc-codegen/schemas/wsdb_schema.json new file mode 100644 index 000000000000..8959153dca13 --- /dev/null +++ b/ipc-codegen/schemas/wsdb_schema.json @@ -0,0 +1,2 @@ +{"__typename":"WsdbApi","commands":["named_union",[["WsdbGetTreeInfo",{"__typename":"WsdbGetTreeInfo","treeId":"MerkleTreeId","revision":{"__typename":"WorldStateRevision","forkId":"unsigned long","blockNumber":"unsigned int","includeUncommitted":"bool"}}],["WsdbGetStateReference",{"__typename":"WsdbGetStateReference","revision":"WorldStateRevision"}],["WsdbGetInitialStateReference",{"__typename":"WsdbGetInitialStateReference"}],["WsdbGetLeafValue",{"__typename":"WsdbGetLeafValue","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndex":"unsigned long"}],["WsdbGetLeafPreimage",{"__typename":"WsdbGetLeafPreimage","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndex":"unsigned long"}],["WsdbGetSiblingPath",{"__typename":"WsdbGetSiblingPath","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndex":"unsigned long"}],["WsdbGetBlockNumbersForLeafIndices",{"__typename":"WsdbGetBlockNumbersForLeafIndices","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndices":["vector",["unsigned long"]]}],["WsdbFindLeafIndices",{"__typename":"WsdbFindLeafIndices","treeId":"MerkleTreeId","revision":"WorldStateRevision","leaves":["vector",[["vector",["unsigned char"]]]],"startIndex":"unsigned long"}],["WsdbFindLowLeaf",{"__typename":"WsdbFindLowLeaf","treeId":"MerkleTreeId","revision":"WorldStateRevision","key":["array",["unsigned char",32]]}],["WsdbFindSiblingPaths",{"__typename":"WsdbFindSiblingPaths","treeId":"MerkleTreeId","revision":"WorldStateRevision","leaves":["vector",[["vector",["unsigned char"]]]]}],["WsdbAppendLeaves",{"__typename":"WsdbAppendLeaves","treeId":"MerkleTreeId","leaves":["vector",[["vector",["unsigned char"]]]],"forkId":"unsigned long"}],["WsdbBatchInsert",{"__typename":"WsdbBatchInsert","treeId":"MerkleTreeId","leaves":["vector",[["vector",["unsigned char"]]]],"subtreeDepth":"unsigned int","forkId":"unsigned long"}],["WsdbSequentialInsert",{"__typename":"WsdbSequentialInsert","treeId":"MerkleTreeId","leaves":["vector",[["vector",["unsigned char"]]]],"forkId":"unsigned long"}],["WsdbUpdateArchive",{"__typename":"WsdbUpdateArchive","blockStateRef":"unordered_map","blockHeaderHash":["array",["unsigned char",32]],"forkId":"unsigned long"}],["WsdbCommit",{"__typename":"WsdbCommit"}],["WsdbRollback",{"__typename":"WsdbRollback"}],["WsdbSyncBlock",{"__typename":"WsdbSyncBlock","blockNumber":"unsigned int","blockStateRef":"unordered_map","blockHeaderHash":["array",["unsigned char",32]],"paddedNoteHashes":["vector",[["array",["unsigned char",32]]]],"paddedL1ToL2Messages":["vector",[["array",["unsigned char",32]]]],"paddedNullifiers":["vector",[{"__typename":"NullifierLeafValue","nullifier":["array",["unsigned char",32]]}]],"publicDataWrites":["vector",[{"__typename":"PublicDataLeafValue","slot":["array",["unsigned char",32]],"value":["array",["unsigned char",32]]}]]}],["WsdbCreateFork",{"__typename":"WsdbCreateFork","latest":"bool","blockNumber":"unsigned int"}],["WsdbDeleteFork",{"__typename":"WsdbDeleteFork","forkId":"unsigned long"}],["WsdbFinalizeBlocks",{"__typename":"WsdbFinalizeBlocks","toBlockNumber":"unsigned int"}],["WsdbUnwindBlocks",{"__typename":"WsdbUnwindBlocks","toBlockNumber":"unsigned int"}],["WsdbRemoveHistoricalBlocks",{"__typename":"WsdbRemoveHistoricalBlocks","toBlockNumber":"unsigned int"}],["WsdbGetStatus",{"__typename":"WsdbGetStatus"}],["WsdbCreateCheckpoint",{"__typename":"WsdbCreateCheckpoint","forkId":"unsigned long"}],["WsdbCommitCheckpoint",{"__typename":"WsdbCommitCheckpoint","forkId":"unsigned long"}],["WsdbRevertCheckpoint",{"__typename":"WsdbRevertCheckpoint","forkId":"unsigned long"}],["WsdbCommitAllCheckpoints",{"__typename":"WsdbCommitAllCheckpoints","forkId":"unsigned long"}],["WsdbRevertAllCheckpoints",{"__typename":"WsdbRevertAllCheckpoints","forkId":"unsigned long"}],["WsdbCopyStores",{"__typename":"WsdbCopyStores","dstPath":"string","compact":["optional",["bool"]]}],["WsdbShutdown",{"__typename":"WsdbShutdown"}]]],"responses":["named_union",[["WsdbErrorResponse",{"__typename":"WsdbErrorResponse","message":"string"}],["WsdbGetTreeInfoResponse",{"__typename":"WsdbGetTreeInfoResponse","treeId":"MerkleTreeId","root":["array",["unsigned char",32]],"size":"unsigned long","depth":"unsigned int"}],["WsdbGetStateReferenceResponse",{"__typename":"WsdbGetStateReferenceResponse","state":"unordered_map"}],["WsdbGetInitialStateReferenceResponse",{"__typename":"WsdbGetInitialStateReferenceResponse","state":"unordered_map"}],["WsdbGetLeafValueResponse",{"__typename":"WsdbGetLeafValueResponse","value":["optional",[["vector",["unsigned char"]]]]}],["WsdbGetLeafPreimageResponse",{"__typename":"WsdbGetLeafPreimageResponse","preimage":["optional",[["vector",["unsigned char"]]]]}],["WsdbGetSiblingPathResponse",{"__typename":"WsdbGetSiblingPathResponse","path":["vector",[["array",["unsigned char",32]]]]}],["WsdbGetBlockNumbersForLeafIndicesResponse",{"__typename":"WsdbGetBlockNumbersForLeafIndicesResponse","blockNumbers":["vector",[["optional",["unsigned int"]]]]}],["WsdbFindLeafIndicesResponse",{"__typename":"WsdbFindLeafIndicesResponse","indices":["vector",[["optional",["unsigned long"]]]]}],["WsdbFindLowLeafResponse",{"__typename":"WsdbFindLowLeafResponse","alreadyPresent":"bool","index":"unsigned long"}],["WsdbFindSiblingPathsResponse",{"__typename":"WsdbFindSiblingPathsResponse","paths":["vector",[["optional",[{"__typename":"SiblingPathAndIndex","index":"unsigned long","path":["vector",[["array",["unsigned char",32]]]]}]]]]}],["WsdbAppendLeavesResponse",{"__typename":"WsdbAppendLeavesResponse"}],["WsdbBatchInsertResponse",{"__typename":"WsdbBatchInsertResponse","result":["vector",["unsigned char"]]}],["WsdbSequentialInsertResponse",{"__typename":"WsdbSequentialInsertResponse","result":["vector",["unsigned char"]]}],["WsdbUpdateArchiveResponse",{"__typename":"WsdbUpdateArchiveResponse"}],["WsdbCommitResponse",{"__typename":"WsdbCommitResponse","status":{"__typename":"WorldStateStatusFull","summary":{"__typename":"WorldStateStatusSummary","unfinalizedBlockNumber":"unsigned long","finalizedBlockNumber":"unsigned long","oldestHistoricalBlock":"unsigned long","treesAreSynched":"bool"},"dbStats":{"__typename":"WorldStateDBStats","noteHashTreeStats":{"__typename":"TreeDBStats","mapSize":"unsigned long","physicalFileSize":"unsigned long","blocksDBStats":{"__typename":"DBStats","name":"string","numDataItems":"unsigned long","totalUsedSize":"unsigned long"},"nodesDBStats":"DBStats","leafPreimagesDBStats":"DBStats","leafIndicesDBStats":"DBStats","blockIndicesDBStats":"DBStats"},"messageTreeStats":"TreeDBStats","archiveTreeStats":"TreeDBStats","publicDataTreeStats":"TreeDBStats","nullifierTreeStats":"TreeDBStats"},"meta":{"__typename":"WorldStateMeta","noteHashTreeMeta":{"__typename":"TreeMeta","name":"string","depth":"unsigned int","size":"unsigned long","committedSize":"unsigned long","root":["array",["unsigned char",32]],"initialSize":"unsigned long","initialRoot":["array",["unsigned char",32]],"oldestHistoricBlock":"unsigned int","unfinalizedBlockHeight":"unsigned int","finalizedBlockHeight":"unsigned int"},"messageTreeMeta":"TreeMeta","archiveTreeMeta":"TreeMeta","publicDataTreeMeta":"TreeMeta","nullifierTreeMeta":"TreeMeta"}}}],["WsdbRollbackResponse",{"__typename":"WsdbRollbackResponse"}],["WsdbSyncBlockResponse",{"__typename":"WsdbSyncBlockResponse","status":"WorldStateStatusFull"}],["WsdbCreateForkResponse",{"__typename":"WsdbCreateForkResponse","forkId":"unsigned long"}],["WsdbDeleteForkResponse",{"__typename":"WsdbDeleteForkResponse"}],["WsdbFinalizeBlocksResponse",{"__typename":"WsdbFinalizeBlocksResponse","status":"WorldStateStatusSummary"}],["WsdbUnwindBlocksResponse",{"__typename":"WsdbUnwindBlocksResponse","status":"WorldStateStatusFull"}],["WsdbRemoveHistoricalBlocksResponse",{"__typename":"WsdbRemoveHistoricalBlocksResponse","status":"WorldStateStatusFull"}],["WsdbGetStatusResponse",{"__typename":"WsdbGetStatusResponse","status":"WorldStateStatusSummary"}],["WsdbCreateCheckpointResponse",{"__typename":"WsdbCreateCheckpointResponse"}],["WsdbCommitCheckpointResponse",{"__typename":"WsdbCommitCheckpointResponse"}],["WsdbRevertCheckpointResponse",{"__typename":"WsdbRevertCheckpointResponse"}],["WsdbCommitAllCheckpointsResponse",{"__typename":"WsdbCommitAllCheckpointsResponse"}],["WsdbRevertAllCheckpointsResponse",{"__typename":"WsdbRevertAllCheckpointsResponse"}],["WsdbCopyStoresResponse",{"__typename":"WsdbCopyStoresResponse"}],["WsdbShutdownResponse",{"__typename":"WsdbShutdownResponse"}]]]} + diff --git a/ipc-codegen/scripts/update_schemas.sh b/ipc-codegen/scripts/update_schemas.sh new file mode 100755 index 000000000000..5e230356ce6e --- /dev/null +++ b/ipc-codegen/scripts/update_schemas.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Update committed schema JSON files from C++ binaries. +# Run this after changing C++ command structs. +# +# Usage: ./scripts/update_schemas.sh +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CODEGEN_DIR="$(dirname "$SCRIPT_DIR")" +BB_BIN="${CODEGEN_DIR}/../cpp/build/bin" + +echo "Updating schemas from C++ binaries..." + +for service in bb:bb wsdb:aztec-wsdb cdb:aztec-cdb avm:aztec-avm; do + IFS=: read -r name binary <<< "$service" + bin_path="${BB_BIN}/${binary}" + + if [ ! -x "$bin_path" ]; then + echo " [skip] ${name}: binary not found at ${bin_path}" + echo " Build C++ first: cd barretenberg/cpp && cmake --preset default && cd build && ninja ${binary}" + continue + fi + + "$bin_path" msgpack schema 2>/dev/null > "${CODEGEN_DIR}/schemas/${name}_schema.json" + echo " [updated] ${name}_schema.json" +done + +# Curve constants: export as msgpack then convert to JSON +bb_path="${BB_BIN}/bb" +if [ -x "$bb_path" ]; then + tmpfile=$(mktemp) + "$bb_path" msgpack curve_constants 2>/dev/null > "$tmpfile" + # Convert msgpack to JSON (msgpackr from barretenberg/ts node_modules) + NODE_PATH="${CODEGEN_DIR}/../ts/node_modules" node -e " + const {unpack} = require('msgpackr'); + const fs = require('fs'); + const buf = fs.readFileSync('$tmpfile'); + const c = unpack(buf); + const toHex = (a) => Buffer.from(a).toString('hex'); + const cvt = (p) => Array.isArray(p.x) ? {x:p.x.map(toHex),y:p.y.map(toHex)} : {x:toHex(p.x),y:toHex(p.y)}; + const out = {}; + for (const [k,v] of Object.entries(c)) { + out[k] = k.endsWith('_modulus') ? toHex(v) : cvt(v); + } + fs.writeFileSync('${CODEGEN_DIR}/schemas/bb_curve_constants.json', JSON.stringify(out, null, 2)); + " + rm -f "$tmpfile" + echo " [updated] bb_curve_constants.json" +fi + +echo "" +echo "Done. Run 'npx tsx src/generate.ts' to regenerate bindings, then commit." diff --git a/ipc-codegen/scripts/validate_schemas.sh b/ipc-codegen/scripts/validate_schemas.sh new file mode 100755 index 000000000000..f9c49a5395a1 --- /dev/null +++ b/ipc-codegen/scripts/validate_schemas.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Validate committed schema JSON files match C++ binary output. +# Run in CI after C++ build to catch schema drift. +# +# Usage: ./scripts/validate_schemas.sh +# Exit 0: schemas match. Exit 1: schemas out of date. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CODEGEN_DIR="$(dirname "$SCRIPT_DIR")" +BB_BIN="${CODEGEN_DIR}/../cpp/build/bin" + +FAIL=0 + +for service in bb:bb wsdb:aztec-wsdb cdb:aztec-cdb avm:aztec-avm; do + IFS=: read -r name binary <<< "$service" + bin_path="${BB_BIN}/${binary}" + schema_path="${CODEGEN_DIR}/schemas/${name}_schema.json" + + if [ ! -x "$bin_path" ]; then + echo " [skip] ${name}: binary not found at ${bin_path}" + continue + fi + + if [ ! -f "$schema_path" ]; then + echo " [FAIL] ${name}: committed schema not found at ${schema_path}" + FAIL=1 + continue + fi + + # Export current schema from binary + current=$("$bin_path" msgpack schema 2>/dev/null) + + # Compare with committed + committed=$(cat "$schema_path") + + if [ "$current" = "$committed" ]; then + echo " [ok] ${name}_schema.json matches binary" + else + echo " [FAIL] ${name}_schema.json is out of date!" + echo " Run: cd ipc-codegen && ./scripts/update_schemas.sh" + FAIL=1 + fi +done + +if [ "$FAIL" -gt 0 ]; then + echo "" + echo "Schema validation failed. Committed schemas are out of sync with C++ code." + echo "Fix: cd ipc-codegen && ./scripts/update_schemas.sh && git add schemas/" + exit 1 +fi + +echo "" +echo "All schemas are up to date." diff --git a/ipc-codegen/src/README.md b/ipc-codegen/src/README.md new file mode 100644 index 000000000000..892a9ea1d6e8 --- /dev/null +++ b/ipc-codegen/src/README.md @@ -0,0 +1,86 @@ +# Multi-Language IPC Code Generation + +Generates type-safe client and server bindings from Aztec IPC service schemas +in four languages: **C++**, **TypeScript**, **Rust**, and **Zig**. + +## Architecture + +``` +C++ Service Binaries (aztec-wsdb, aztec-cdb, aztec-avm, bb) + │ + │ `./binary msgpack schema` → JSON to stdout + ▼ +Raw Schema JSON + │ + │ SchemaVisitor (schema_visitor.ts) + ▼ +CompiledSchema IR (language-neutral) + │ + ├──► TypeScriptCodegen → types, async client, server dispatch + ├──► CppCodegen → IPC client class, server handler + ├──► RustCodegen → types, API struct, Handler trait + └──► ZigCodegen → types, client struct, handler vtable +``` + +## Files + +| File | Purpose | +|------|---------| +| `generate.ts` | Unified entry point — runs all services and languages | +| `service_codegen.ts` | Service configs, language target wiring, `generateForService()` | +| `schema_visitor.ts` | Compiles raw JSON schema to `CompiledSchema` IR | +| `typescript_codegen.ts` | TypeScript types, async/sync client, server dispatch | +| `cpp_codegen.ts` | C++ IPC client class, server handler function | +| `rust_codegen.ts` | Rust types/enums, API struct, Handler trait | +| `zig_codegen.ts` | Zig structs, client, handler vtable | +| `naming.ts` | Shared naming utilities (snake_case, PascalCase) | +| `SCHEMA_SPEC.md` | Wire protocol and schema format specification | + +## Services + +| Service | Binary | Languages | Client | Server | +|---------|--------|-----------|--------|--------| +| bb | `bb` | TS, Rust | yes | no | +| wsdb | `aztec-wsdb` | TS, C++, Rust, Zig | yes | yes | +| cdb | `aztec-cdb` | TS, C++, Rust, Zig | yes | yes | +| avm | `aztec-avm` | TS, Rust, Zig | yes | no | + +## Usage + +```bash +# Generate all services, all languages +yarn generate + +# Generate a single service +yarn generate:wsdb + +# Generate specific services via unified entry point +npx tsx src/cbind/generate.ts wsdb cdb +``` + +## Adding a New Command + +1. Define the command struct in C++ with `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS` +2. Add a nested `Response` struct +3. Add both to the service's `Command` and `CommandResponse` NamedUnion types +4. Run `yarn generate` +5. All language bindings regenerate automatically + +## Adding a New Language + +1. Create `_codegen.ts` implementing `generateTypes()`, `generateClient()`, `generateServer()` +2. Add a target helper function in `service_codegen.ts` +3. Wire it into the relevant service configs +4. See `SCHEMA_SPEC.md` for the wire protocol contract + +## Output Locations + +- **TypeScript**: `src/aztec-{wsdb,cdb,avm}/generated/` +- **C++**: `cpp/src/barretenberg/{wsdb,cdb}/*_generated.{hpp,cpp}` +- **Rust**: `rust/aztec-ipc/src/{wsdb,cdb,avm}/` +- **Zig**: `zig/aztec-ipc/src/{wsdb,cdb,avm}/` + +## Schema Versioning + +Each generated file includes a `SCHEMA_HASH` constant (SHA-256 of the raw schema JSON). +Clients can check this at connection time to detect incompatible schema changes. diff --git a/ipc-codegen/src/SCHEMA_SPEC.md b/ipc-codegen/src/SCHEMA_SPEC.md new file mode 100644 index 000000000000..d8969daa53a2 --- /dev/null +++ b/ipc-codegen/src/SCHEMA_SPEC.md @@ -0,0 +1,242 @@ +# Aztec IPC Schema Format Specification + +This document specifies the JSON schema format used for cross-language code generation +in the Aztec IPC system. The schema is the contract between the C++ binary's +`msgpack schema` command and all language code generators. + +## Overview + +Each IPC service binary (aztec-wsdb, aztec-cdb, aztec-avm, bb) exports its schema via: + +```bash +./aztec-wsdb msgpack schema # Outputs JSON to stdout +``` + +The output is a JSON object representing the service's API, derived at compile time +from C++ type metadata via the `MsgpackSchemaPacker` infrastructure. + +## Top-Level Structure + +```json +{ + "commands": ["named_union", [ + ["CommandNameA", { "__typename": "CommandNameA", "field1": , ... }], + ["CommandNameB", { "__typename": "CommandNameB", "field1": , ... }] + ]], + "responses": ["named_union", [ + ["ResponseNameA", { "__typename": "ResponseNameA", "field1": , ... }], + ["ErrorResponse", { "__typename": "ErrorResponse", "message": "string" }] + ]] +} +``` + +- `commands` and `responses` are both **NamedUnion** types (see below). +- Commands and responses are positionally paired: the Nth command corresponds to the Nth + non-error response. The error response (ending in `ErrorResponse`) is shared across all commands. + +## Type Encodings + +Types in the schema are represented as one of: + +### Primitive Types (JSON strings) + +| Schema String | C++ Type | Description | +|---------------|----------|-------------| +| `"bool"` | `bool` | Boolean | +| `"int"` | `int` | Signed 32-bit integer | +| `"unsigned int"` | `unsigned int` / `uint32_t` | Unsigned 32-bit integer | +| `"unsigned short"` | `unsigned short` / `uint16_t` | Unsigned 16-bit integer | +| `"unsigned long"` | `unsigned long` / `uint64_t` | Unsigned 64-bit integer | +| `"unsigned char"` | `unsigned char` / `uint8_t` | Unsigned 8-bit integer | +| `"double"` | `double` | 64-bit floating point | +| `"string"` | `std::string` | UTF-8 string | +| `"bin32"` | Fixed-size byte arrays | Raw binary data (e.g., field elements) | +| `"field2"` | `Fq2` | Extension field: pair of 32-byte field elements | +| `"MerkleTreeId"` | `MerkleTreeId` enum | C++ enum, serialized as uint32 | +| `"unordered_map"` | `std::unordered_map` | Map type (special-cased per usage) | + +### Container Types (JSON arrays) + +Container types are encoded as 2-element arrays: `[kind, [args...]]` + +#### `vector` +```json +["vector", []] +``` +Example: `["vector", ["unsigned char"]]` = `std::vector` = byte array + +**Special case**: `["vector", ["unsigned char"]]` is treated as raw bytes, not an array of integers. + +#### `array` +```json +["array", [, ]] +``` +Example: `["array", ["unsigned char", 32]]` = `std::array` = 32-byte fixed buffer + +**Special case**: `["array", ["unsigned char", N]]` is treated as raw bytes (like `vector`). + +#### `optional` +```json +["optional", []] +``` +Example: `["optional", ["string"]]` = `std::optional` + +#### `shared_ptr` +```json +["shared_ptr", []] +``` +Treated as a transparent wrapper; the inner type is used directly. + +#### `tuple` +```json +["tuple", [, , ...]] +``` +Example: `["tuple", ["string", "unsigned long"]]` = `std::tuple` + +#### `alias` +```json +["alias", [, ]] +``` +Alias for a type that serializes as another msgpack type (e.g., `uint256_t` serializes as raw bytes). +Treated as raw bytes in code generation. + +### Struct Types (JSON objects) + +Structs are JSON objects with a `__typename` field and named fields: + +```json +{ + "__typename": "SomeStruct", + "field_a": "unsigned int", + "field_b": ["vector", ["unsigned char"]], + "field_c": { + "__typename": "NestedStruct", + "x": "unsigned long" + } +} +``` + +- `__typename` identifies the struct for deduplication and named reference. +- Field names are the original C++ field names (snake_case by convention). +- Field values are type encodings (primitives, containers, or nested structs). +- Nested structs are inlined on first occurrence and referenced by `__typename` string thereafter. + +### NamedUnion Type + +```json +["named_union", [ + ["VariantName1", ], + ["VariantName2", ] +]] +``` + +A tagged union where each variant has a string name and a type schema. +This is the top-level type for both `commands` and `responses`. + +## Wire Protocol + +The schema defines the types; this section specifies how they are serialized on the wire. + +### Framing + +All messages use length-prefix framing: + +``` +[4 bytes: payload length, little-endian uint32][payload: msgpack bytes] +``` + +### Request Wire Format + +A request is a 1-element msgpack **array** containing a NamedUnion: + +``` +msgpack array(1) [ + msgpack array(2) [ + msgpack string: "CommandName", + msgpack map: { field1: value1, field2: value2, ... } + ] +] +``` + +In msgpack terms: `[[command_name, {fields...}]]` + +The outer array (tuple wrapper) exists for extensibility. The inner 2-element array +is the NamedUnion encoding. + +### Response Wire Format + +A response is a NamedUnion (no tuple wrapper): + +``` +msgpack array(2) [ + msgpack string: "ResponseName" | "ErrorResponse", + msgpack map: { field1: value1, field2: value2, ... } +] +``` + +If the response variant name ends with `ErrorResponse`, the response indicates an error. +The error struct always has a `message` field (string). + +### NamedUnion Wire Encoding + +A NamedUnion value is always encoded as a **2-element msgpack array**: +- Element 0: `string` — the variant name (matches `MSGPACK_SCHEMA_NAME` in C++) +- Element 1: `map` — the variant's fields, encoded as a msgpack map with string keys + +### Struct Wire Encoding + +Structs are encoded as msgpack **maps** with string keys matching the original C++ field names. +The `__typename` field from the schema is NOT included in the wire encoding — it is only +used for schema identification. + +### Type Wire Encoding Summary + +| Schema Type | msgpack Encoding | +|-------------|------------------| +| `bool` | msgpack bool | +| `unsigned int`, `int` | msgpack integer (smallest encoding that fits) | +| `unsigned short` | msgpack integer | +| `unsigned long` | msgpack integer | +| `unsigned char` | msgpack integer | +| `double` | msgpack float64 | +| `string` | msgpack str | +| `bin32`, `bytes` | msgpack bin | +| `vector` | msgpack bin (NOT array of integers) | +| `array` | msgpack bin | +| `vector` | msgpack array | +| `array` | msgpack array (fixed length) | +| `optional` | msgpack nil (if absent) or value | +| `field2` | msgpack ext type or array of 2 bin values | +| `enum` | msgpack integer (uint32) | +| struct | msgpack map with string keys | +| NamedUnion | msgpack array(2): [string, map] | + +### Integer Encoding Note + +msgpack uses the **smallest encoding that fits the value**, not the declared type. +A `uint64_t` value of `5` encodes as a single byte (positive fixint), not as a +uint64 encoding. Decoders MUST accept any integer encoding width for any integer field. + +## Schema Versioning + +Schema compatibility can be validated by computing a SHA-256 hash of the raw JSON schema +output. This hash should be checked at connection time when possible. A mismatch indicates +that the service binary and client were generated from different schema versions. + +## Adding a New Command + +To add a new command to a service: + +1. Define the command struct in C++ with `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS` +2. Add a nested `Response` struct with its own `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS` +3. Add both to the service's `Command` and `CommandResponse` NamedUnion types +4. Run `yarn generate` to regenerate all language bindings +5. Verify generated code compiles in all target languages + +## Source Files + +- Schema export: `barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp` +- Schema naming: `barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_name.hpp` +- NamedUnion: `barretenberg/cpp/src/barretenberg/common/named_union.hpp` +- Schema visitor (IR compiler): `barretenberg/ts/src/cbind/schema_visitor.ts` +- Service codegen orchestrator: `barretenberg/ts/src/cbind/service_codegen.ts` diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts new file mode 100644 index 000000000000..c823913df738 --- /dev/null +++ b/ipc-codegen/src/cpp_codegen.ts @@ -0,0 +1,1109 @@ +/** + * C++ IPC Client Code Generator + * + * Generates a C++ IPC client from a CompiledSchema. The generated client: + * - Connects to a server over Unix Domain Socket via ipc::IpcClient + * - Wraps each command in a NamedUnion, serializes with msgpack, sends, receives, deserializes + * - Has one method per command, returning the typed response + * + * Usage: + * const gen = new CppCodegen({ namespace: 'bb::cdb', prefix: 'Cdb' }); + * const header = gen.generateHeader(schema); + * const impl = gen.generateImpl(schema); + */ + +import type { CompiledSchema, Type, Struct, Field, Command } from './schema_visitor.ts'; +import { toSnakeCase } from './naming.ts'; + +export interface CppCodegenOptions { + /** C++ namespace for generated code, e.g. 'bb::cdb' */ + namespace: string; + /** Prefix for command/response types, e.g. 'Cdb' */ + prefix: string; + /** Header path for the *_execute.hpp file that defines Command/CommandResponse NamedUnions */ + executeHeader: string; + /** Header path for the *_commands.hpp file that defines the command structs */ + commandsHeader: string; + /** + * External types: types that already exist in barretenberg headers. + * Map from type name → header include path. + * These types will be #included instead of generated. + */ + externals?: Record; + /** + * Using-namespace declarations to add at the top of generated commands file. + * E.g. ['bb::world_state', 'bb::crypto::merkle_tree'] + */ + usingNamespaces?: string[]; + /** + * Additional headers to include in generated commands file. + */ + additionalIncludes?: string[]; + /** + * Override for the generated output directory include path. + * Used when commandsHeader doesn't point to the generated/ directory + * (e.g. bb keeps hand-written commands but generates server dispatch). + */ + generatedIncludeDir?: string; + /** + * Sub-namespace for wire types (e.g. 'wire' → types in ns::wire). + * When set, standalone types are wrapped in this sub-namespace, + * and the server dispatch deserializes into wire types then converts to domain types. + */ + wireNamespace?: string; +} + +export class CppCodegen { + constructor(private opts: CppCodegenOptions) {} + + /** Convert a command name to a C++ method name (snake_case without prefix) */ + private methodName(commandName: string): string { + // Strip prefix: "CdbGetContractInstance" -> "GetContractInstance" -> "get_contract_instance" + const withoutPrefix = commandName.startsWith(this.opts.prefix) + ? commandName.slice(this.opts.prefix.length) + : commandName; + return toSnakeCase(withoutPrefix); + } + + /** Check if the response has fields (non-void return) */ + private hasResponseFields(command: Command, schema: CompiledSchema): boolean { + const resp = schema.responses.get(command.responseType); + return !!resp && resp.fields.length > 0; + } + + /** Generate the method signature using command struct types directly */ + private generateMethodSignature(command: Command, schema: CompiledSchema, className?: string): string { + const method = this.methodName(command.name); + const hasFields = this.hasResponseFields(command, schema); + // Wire types use top-level response names (BbFooResponse). + // Command types with nested Response use Cmd::Response. + const retType = hasFields + ? (this.opts.wireNamespace ? command.responseType : `${command.name}::Response`) + : 'void'; + + // If the command has fields, take the whole command struct by value + const params = command.fields.length > 0 ? `${command.name} cmd` : ''; + + const prefix = className ? `${className}::` : ''; + const constSuffix = !this.isWriteCommand(command) ? ' const' : ''; + + return `${retType} ${prefix}${method}(${params})${constSuffix}`; + } + + /** Check if a command modifies state (non-const) */ + private isWriteCommand(command: Command): boolean { + const name = command.name.toLowerCase(); + return name.includes('add') || name.includes('create') || + name.includes('commit') || name.includes('revert') || + name.includes('register') || name.includes('shutdown') || + name.includes('delete') || name.includes('sync') || + name.includes('rollback') || name.includes('unwind'); + } + + /** Generate the header file */ + generateHeader(schema: CompiledSchema, schemaHash?: string): string { + const { namespace: ns, prefix } = this.opts; + const wireNs = this.opts.wireNamespace; + const className = `${prefix}IpcClient`; + + const methods = schema.commands.map(cmd => { + const sig = this.generateMethodSignature(cmd, schema); + return ` ${sig};`; + }).join('\n'); + + const hashConstant = schemaHash + ? `\n/** Schema version hash for compatibility checking */\nstatic constexpr const char SCHEMA_HASH[] = "${schemaHash}";\n` + : ''; + + // When wireNamespace is set, include wire types and bring them into scope + const wireInclude = wireNs + ? `#include "${this.generatedDir()}/${toSnakeCase(prefix)}_types.hpp"\n` + : ''; + const wireUsing = wireNs + ? `using namespace ${wireNs};\n` + : ''; + + const typesInclude = `${this.generatedDir()}/${toSnakeCase(prefix)}_types.hpp`; + + return `// AUTOGENERATED FILE - DO NOT EDIT +#pragma once + +#include "${typesInclude}" +#include "${this.generatedDir()}/ipc_client.hpp" +// clang-format on + +#include +#include + +namespace ${ns} { +${wireUsing}${hashConstant} +/** + * @brief Auto-generated IPC client. + * + * Each method sends a msgpack-serialized command to the server over UDS + * and returns the typed response. All methods block until the response arrives. + */ +class ${className} { + public: + explicit ${className}(const std::string& socket_path); + ~${className}(); + + ${className}(const ${className}&) = delete; + ${className}& operator=(const ${className}&) = delete; + +${methods} + + private: + template + Resp send(Cmd&& cmd) const; + + mutable std::unique_ptr<::ipc::IpcClient> client_; +}; + +} // namespace ${ns} +`; + } + + /** Generate the implementation file — string-based serialization, no NamedUnion */ + generateImpl(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const className = `${prefix}IpcClient`; + const errorType = schema.errorTypeName || `${prefix}ErrorResponse`; + + const methods = schema.commands.map(cmd => { + return this.generateMethodImpl(cmd, schema, className); + }).join('\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT + +#include "${this.headerIncludePath()}" +#include "barretenberg/serialize/msgpack.hpp" +#include "barretenberg/serialize/msgpack_impl.hpp" + +#include +#include + +namespace ${ns} { + +${className}::${className}(const std::string& socket_path) + : client_(std::make_unique<::ipc::IpcClient>(socket_path.c_str())) +{} + +${className}::~${className}() = default; + +template +Resp ${className}::send(Cmd&& cmd) const +{ + // Serialize as [[CommandName, {payload}]] + msgpack::sbuffer send_buffer; + msgpack::packer pk(send_buffer); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(Cmd::MSGPACK_SCHEMA_NAME)); + pk.pack(std::forward(cmd)); + + // Send request, receive response + std::vector request_bytes(send_buffer.data(), send_buffer.data() + send_buffer.size()); + auto response_bytes = client_->call(request_bytes); + + if (response_bytes.empty()) { + throw std::runtime_error("Empty response from server"); + } + + // Parse response: [ResponseName, {payload}] + auto unpacked = msgpack::unpack( + reinterpret_cast(response_bytes.data()), response_bytes.size()); + auto obj = unpacked.get(); + + if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 2 || + obj.via.array.ptr[0].type != msgpack::type::STR) { + throw std::runtime_error("Invalid response format from server"); + } + + std::string resp_name(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); + if (resp_name == "${errorType}") { + std::string message; + auto& payload = obj.via.array.ptr[1]; + // Extract message field from the error map + if (payload.type == msgpack::type::MAP) { + for (uint32_t i = 0; i < payload.via.map.size; ++i) { + auto& kv = payload.via.map.ptr[i]; + if (kv.key.type == msgpack::type::STR) { + std::string key(kv.key.via.str.ptr, kv.key.via.str.size); + if (key == "message" && kv.val.type == msgpack::type::STR) { + message = std::string(kv.val.via.str.ptr, kv.val.via.str.size); + } + } + } + } + throw std::runtime_error("Server error: " + message); + } + + Resp result; + obj.via.array.ptr[1].convert(result); + return result; +} + +${methods} +} // namespace ${ns} +`; + } + + /** Generate a single method implementation */ + private generateMethodImpl(command: Command, schema: CompiledSchema, className: string): string { + const sig = this.generateMethodSignature(command, schema, className); + const hasFields = this.hasResponseFields(command, schema); + const respType = this.opts.wireNamespace ? command.responseType : `${command.name}::Response`; + + const cmdExpr = command.fields.length > 0 ? 'std::move(cmd)' : `${command.name}{}`; + + if (!hasFields) { + return `${sig} +{ + send<${command.name}, ${respType}>(${cmdExpr}); +} +`; + } + + return `${sig} +{ + return send<${command.name}, ${respType}>(${cmdExpr}); +} +`; + } + + /** Get the generated/ directory include path */ + private generatedDir(): string { + if (this.opts.generatedIncludeDir) { + return this.opts.generatedIncludeDir; + } + return this.opts.commandsHeader.substring(0, this.opts.commandsHeader.lastIndexOf('/')); + } + + /** Compute the include path for the generated client header */ + private headerIncludePath(): string { + return `${this.generatedDir()}/${toSnakeCase(this.opts.prefix)}_ipc_client.hpp`; + } + + // ----------------------------------------------------------------------- + // Standalone types (no barretenberg dependencies) + // ----------------------------------------------------------------------- + + /** Generate standalone C++ types with MSGPACK_DEFINE_MAP — no barretenberg deps */ + generateStandaloneTypes(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + + // Map schema types to C++ types + const mapType = (type: import('./schema_visitor.ts').Type): string => { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return 'bool'; + case 'u8': return 'uint8_t'; + case 'u16': return 'uint16_t'; + case 'u32': return 'uint32_t'; + case 'u64': return 'uint64_t'; + case 'f64': return 'double'; + case 'string': return 'std::string'; + case 'bytes': return 'std::vector'; + case 'fr': return 'Fr'; // std::array + case 'field2': return 'std::array'; + case 'enum_u32': return 'uint32_t'; + case 'map_u32_pair': return 'std::unordered_map, uint64_t>>'; + } + break; + case 'vector': return `std::vector<${mapType(type.element!)}>`; + case 'array': return `std::array<${mapType(type.element!)}, ${type.size}>`; + case 'optional': return `std::optional<${mapType(type.element!)}>`; + case 'struct': return type.struct!.name; + } + return 'void'; + }; + + const allStructs = [...schema.structs.values(), ...schema.responses.values()]; + const structs = allStructs.map(s => { + const fields = s.fields.map(f => ` ${mapType(f.type)} ${f.name};`).join('\n'); + const fieldNames = s.fields.map(f => f.name).join(', '); + const schemaName = ` static constexpr const char MSGPACK_SCHEMA_NAME[] = "${s.name}";`; + const serialization = fieldNames + ? ` SERIALIZATION_FIELDS(${fieldNames})` + : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; + return `struct ${s.name} {\n${schemaName}\n${fields}\n${serialization}\n bool operator==(const ${s.name}&) const = default;\n};`; + }).join('\n\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT +// Standalone types for ${prefix} service. +#pragma once + +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Self-contained serialization macro. +// Defines a msgpack() method that enumerates field name/value pairs. +// Works with msgpack packers (serialization) and schema reflectors. +// If barretenberg's SERIALIZATION_FIELDS is already available, use it instead. +// --------------------------------------------------------------------------- +#ifndef SERIALIZATION_FIELDS +#define _SF_E1(x) #x, x +#define _SF_E2(x, ...) #x, x, _SF_E1(__VA_ARGS__) +#define _SF_E3(x, ...) #x, x, _SF_E2(__VA_ARGS__) +#define _SF_E4(x, ...) #x, x, _SF_E3(__VA_ARGS__) +#define _SF_E5(x, ...) #x, x, _SF_E4(__VA_ARGS__) +#define _SF_E6(x, ...) #x, x, _SF_E5(__VA_ARGS__) +#define _SF_E7(x, ...) #x, x, _SF_E6(__VA_ARGS__) +#define _SF_E8(x, ...) #x, x, _SF_E7(__VA_ARGS__) +#define _SF_E9(x, ...) #x, x, _SF_E8(__VA_ARGS__) +#define _SF_E10(x, ...) #x, x, _SF_E9(__VA_ARGS__) +#define _SF_E11(x, ...) #x, x, _SF_E10(__VA_ARGS__) +#define _SF_E12(x, ...) #x, x, _SF_E11(__VA_ARGS__) +#define _SF_E13(x, ...) #x, x, _SF_E12(__VA_ARGS__) +#define _SF_E14(x, ...) #x, x, _SF_E13(__VA_ARGS__) +#define _SF_E15(x, ...) #x, x, _SF_E14(__VA_ARGS__) +#define _SF_E16(x, ...) #x, x, _SF_E15(__VA_ARGS__) +#define _SF_E17(x, ...) #x, x, _SF_E16(__VA_ARGS__) +#define _SF_E18(x, ...) #x, x, _SF_E17(__VA_ARGS__) +#define _SF_E19(x, ...) #x, x, _SF_E18(__VA_ARGS__) +#define _SF_E20(x, ...) #x, x, _SF_E19(__VA_ARGS__) +#define _SF_CNT(_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,_13,_14,_15,_16,_17,_18,_19,_20,N,...) N +#define _SF_NUM(...) _SF_CNT(__VA_ARGS__,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1) +#define _SF_CAT(a, b) a##b +#define _SF_SEL(n) _SF_CAT(_SF_E, n) +#define _SF_NVP(...) _SF_SEL(_SF_NUM(__VA_ARGS__))(__VA_ARGS__) +#define SERIALIZATION_FIELDS(...) \\ + template void msgpack(_PackFn pack_fn) { pack_fn(_SF_NVP(__VA_ARGS__)); } +#endif + +/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. +using Fr = std::array; + +namespace ${ns}${this.opts.wireNamespace ? '::' + this.opts.wireNamespace : ''} { + +${structs} + +} // namespace ${ns}${this.opts.wireNamespace ? '::' + this.opts.wireNamespace : ''} +`; + } + + /** Generate standalone server dispatch (no barretenberg deps) */ + generateStandaloneServer(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const errorType = schema.errorTypeName || `${prefix}ErrorResponse`; + + const dispatchCases = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + return ` if (cmd_name == "${c.name}") { + ${c.name} cmd; cmd_payload.convert(cmd); + auto resp = handle_${toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name)}(cmd); + pk.pack_array(2); pk.pack(std::string("${c.responseType}")); pk.pack(resp); + }`; + }).join(' else '); + + const stubs = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + return `// TODO: implement ${c.name} +inline ${c.responseType} handle_${method}(const ${c.name}& /*cmd*/) { + throw std::runtime_error("not implemented: ${c.name}"); +}`; + }).join('\n\n'); + + const shutdownName = schema.commands.find(c => c.name.endsWith('Shutdown'))?.name || `${prefix}Shutdown`; + const shutdownResp = shutdownName + 'Response'; + + return `// AUTOGENERATED FILE - DO NOT EDIT +// ${prefix} server dispatch — standalone, no barretenberg dependencies. +// Implement the handle_* functions to build your ${prefix} service. +#pragma once + +#include "types_gen.hpp" +#include "${this.generatedDir()}/ipc_server.hpp" +#include + +namespace ${ns} { + +// --------------------------------------------------------------------------- +// Dispatch: routes commands to handler functions +// --------------------------------------------------------------------------- + +inline std::vector dispatch(const std::vector& payload) { + auto oh = msgpack::unpack(reinterpret_cast(payload.data()), payload.size()); + auto obj = oh.get(); + auto& inner = obj.via.array.ptr[0]; + std::string cmd_name(inner.via.array.ptr[0].via.str.ptr, inner.via.array.ptr[0].via.str.size); + auto& cmd_payload = inner.via.array.ptr[1]; + + msgpack::sbuffer resp_buf; + msgpack::packer pk(resp_buf); + + try { + if (cmd_name == "${shutdownName}") { + pk.pack_array(2); pk.pack(std::string("${shutdownResp}")); pk.pack_map(0); + } else ${dispatchCases} else { + pk.pack_array(2); pk.pack(std::string("${errorType}")); + pk.pack_map(1); pk.pack(std::string("message")); pk.pack(std::string("unknown command: ") + cmd_name); + } + } catch (const std::exception& e) { + resp_buf.clear(); + msgpack::packer epk(resp_buf); + epk.pack_array(2); epk.pack(std::string("${errorType}")); + epk.pack_map(1); epk.pack(std::string("message")); epk.pack(std::string(e.what())); + } + + return std::vector(resp_buf.data(), resp_buf.data() + resp_buf.size()); +} + +/// Start the server on the given socket path. +inline void serve(const char* socket_path) { + ipc::serve(socket_path, dispatch); +} + +// --------------------------------------------------------------------------- +// Handler stubs — implement these to build your ${prefix} service. +// --------------------------------------------------------------------------- + +${stubs} + +} // namespace ${ns} +`; + } + + /** Generate standalone client wrapper (no barretenberg deps) */ + generateStandaloneClient(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const errorType = schema.errorTypeName || `${prefix}ErrorResponse`; + + const methods = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + const hasFields = c.fields.length > 0; + const param = hasFields ? `const ${c.name}& cmd` : ''; + const packCmd = hasFields ? 'cmd' : `${c.name}{}`; + return ` ${c.responseType} ${method}(${param}) { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); pk.pack_array(2); pk.pack(std::string("${c.name}")); pk.pack(${packCmd}); + auto resp = client_.call(std::vector(buf.data(), buf.data() + buf.size())); + auto oh = msgpack::unpack(reinterpret_cast(resp.data()), resp.size()); + auto obj = oh.get(); + std::string resp_name(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); + if (resp_name == "${errorType}") throw std::runtime_error("server error"); + ${c.responseType} result; obj.via.array.ptr[1].convert(result); + return result; + }`; + }).join('\n\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT +// ${prefix} typed IPC client — standalone, no barretenberg dependencies. +#pragma once + +#include "types_gen.hpp" +#include "${this.generatedDir()}/ipc_client.hpp" +#include + +namespace ${ns} { + +class ${prefix}Client { + public: + explicit ${prefix}Client(const char* socket_path) : client_(socket_path) {} + +${methods} + + void shutdown() { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); pk.pack_array(2); pk.pack(std::string("${prefix}Shutdown")); pk.pack_map(0); + client_.call(std::vector(buf.data(), buf.data() + buf.size())); + } + + private: + ipc::IpcClient client_; +}; + +} // namespace ${ns} +`; + } + + // ----------------------------------------------------------------------- + // Barretenberg commands generation (uses native bb types, has execute()) + // ----------------------------------------------------------------------- + + /** + * Map an IR type to a barretenberg C++ type. + * Uses native types (bb::fr, MerkleTreeId, StateReference) instead of + * standalone equivalents (Fr, uint32_t, std::unordered_map<...>). + */ + private mapTypeBb(type: import('./schema_visitor.ts').Type): string { + const externals = this.opts.externals || {}; + + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return 'bool'; + case 'u8': return 'uint8_t'; + case 'u16': return 'uint16_t'; + case 'u32': return 'uint32_t'; + case 'u64': return 'uint64_t'; + case 'f64': return 'double'; + case 'string': return 'std::string'; + case 'bytes': return 'std::vector'; + case 'fr': return 'bb::fr'; + case 'field2': return 'std::array'; + case 'enum_u32': + // Preserve original enum name if available + return type.originalName || 'uint32_t'; + case 'map_u32_pair': + // StateReference is the only map type we've seen + return 'StateReference'; + } + break; + case 'vector': return `std::vector<${this.mapTypeBb(type.element!)}>`; + case 'array': return `std::array<${this.mapTypeBb(type.element!)}, ${type.size}>`; + case 'optional': return `std::optional<${this.mapTypeBb(type.element!)}>`; + case 'struct': return type.struct!.name; + } + return 'void'; + } + + /** + * Generate barretenberg-specific commands header. + * + * This generates command structs that use native barretenberg types + * (bb::fr, MerkleTreeId, etc.) and include execute() declarations. + * External types (NullifierLeafValue, WorldStateRevision, etc.) are + * imported via #include instead of being generated. + */ + generateCommands(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const externals = this.opts.externals || {}; + + // Collect unique include paths from externals + const externalIncludes = new Set(); + for (const header of Object.values(externals)) { + externalIncludes.add(header); + } + for (const inc of this.opts.additionalIncludes || []) { + externalIncludes.add(inc); + } + + const includeLines = [...externalIncludes].sort().map(h => `#include "${h}"`).join('\n'); + const usingLines = (this.opts.usingNamespaces || []).map(ns => `using namespace ${ns};`).join('\n'); + + // Generate non-external struct types (only SiblingPathAndIndex-like types + // that are NOT commands/responses and NOT external) + const helperStructs: string[] = []; + for (const [name, struct] of schema.structs) { + if (name.startsWith(prefix)) continue; // Commands are generated below + if (externals[name]) continue; // External — imported via #include + // Generate this helper struct + const fields = struct.fields.map(f => ` ${this.mapTypeBb(f.type)} ${f.name};`).join('\n'); + const fieldNames = struct.fields.map(f => f.name).join(', '); + const serialization = fieldNames + ? ` SERIALIZATION_FIELDS(${fieldNames});` + : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; + helperStructs.push( + `struct ${name} {\n static constexpr const char MSGPACK_SCHEMA_NAME[] = "${name}";\n${fields}\n${serialization}\n bool operator==(const ${name}&) const = default;\n};` + ); + } + for (const [name, struct] of schema.responses) { + if (name.startsWith(prefix)) continue; // Responses generated nested inside commands + if (externals[name]) continue; + // Non-prefixed response types (shouldn't normally happen) + } + + // Generate command structs with nested Response and execute() + const commandStructs = schema.commands.map(cmd => { + const respStruct = schema.responses.get(cmd.responseType); + + // Command fields + const cmdFields = cmd.fields.map(f => ` ${this.mapTypeBb(f.type)} ${f.name};`).join('\n'); + const cmdFieldNames = cmd.fields.map(f => f.name).join(', '); + const cmdSerialization = cmdFieldNames + ? ` SERIALIZATION_FIELDS(${cmdFieldNames});` + : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; + + // Response fields + let responseBlock: string; + if (respStruct && respStruct.fields.length > 0) { + const respFields = respStruct.fields.map(f => ` ${this.mapTypeBb(f.type)} ${f.name};`).join('\n'); + const respFieldNames = respStruct.fields.map(f => f.name).join(', '); + responseBlock = ` struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "${cmd.responseType}"; +${respFields} + SERIALIZATION_FIELDS(${respFieldNames}); + bool operator==(const Response&) const = default; + };`; + } else { + responseBlock = ` struct Response { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "${cmd.responseType}"; + template void msgpack(_PackFn&& pack_fn) { pack_fn(); } + bool operator==(const Response&) const = default; + };`; + } + + return `struct ${cmd.name} { + static constexpr const char MSGPACK_SCHEMA_NAME[] = "${cmd.name}"; +${responseBlock} +${cmdFields} + Response execute(${prefix}Request& request) &&; +${cmdSerialization} + bool operator==(const ${cmd.name}&) const = default; +};`; + }).join('\n\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT +#pragma once + +${includeLines} +#include +#include +#include +#include + +namespace ${ns} { + +${usingLines} + +// Forward declaration +struct ${prefix}Request; + +${helperStructs.join('\n\n')}${helperStructs.length > 0 ? '\n\n' : ''}// --------------------------------------------------------------------------- +// Commands +// --------------------------------------------------------------------------- + +${commandStructs} + +} // namespace ${ns} +`; + } + + // ----------------------------------------------------------------------- + // Server-side code generation (uses standalone ipc_server.hpp template) + // ----------------------------------------------------------------------- + + /** Generate the server dispatch — header-only, template */ + generateServerHeader(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const errorTypeName = schema.errorTypeName || `${prefix}ErrorResponse`; + const typesHeader = `${toSnakeCase(prefix)}_types.hpp`; + + // Handler declarations — template + const handlerDecls = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + return `template\nwire::${c.responseType} handle_${method}(Ctx& ctx, wire::${c.name}&& cmd);`; + }).join('\n\n'); + + // Handler entries for dispatch map + const handlerEntries = schema.commands.map(cmd => { + const isShutdown = cmd.name.endsWith('Shutdown'); + const method = toSnakeCase(cmd.name.startsWith(prefix) ? cmd.name.slice(prefix.length) : cmd.name); + + if (isShutdown) { + return ` { "${cmd.name}", []([[maybe_unused]] Ctx& ctx, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack_map(0); + THROW ::ipc::ShutdownRequested(std::vector(buf.data(), buf.data() + buf.size())); + } }`; + } + + const deserialize = cmd.fields.length > 0 + ? `wire::${cmd.name} wire_cmd; payload.convert(wire_cmd);` + : `wire::${cmd.name} wire_cmd;`; + + return ` { "${cmd.name}", [](Ctx& ctx, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + ${deserialize} + auto wire_resp = handle_${method}(ctx, std::move(wire_cmd)); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack(wire_resp); + return std::vector(buf.data(), buf.data() + buf.size()); + } }`; + }).join(',\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT +// Header-only server dispatch — template for service context. +#pragma once + +#include "${typesHeader}" +#include "ipc_server.hpp" + +// msgpack headers needed for dispatch implementation. +// Includers within barretenberg must include try_catch_shim.hpp before this header. +#ifndef THROW +#define THROW throw +#define RETHROW throw +#endif +#include + +#include +#include +#include +#include +#include +#include + +namespace ${ns} { + +// Wire types are in the 'wire' sub-namespace (from ${typesHeader}) +// Handler declarations — implement these in your handler file. +// Template specializations must be visible before make_handler() is instantiated. + +${handlerDecls} + +// --------------------------------------------------------------------------- +// Dispatch — template on service context type +// --------------------------------------------------------------------------- + +namespace detail { + +inline std::vector make_error(const std::string& message) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("${errorTypeName}")); + pk.pack_map(1); + pk.pack(std::string("message")); + pk.pack(message); + return std::vector(buf.data(), buf.data() + buf.size()); +} + +} // namespace detail + +template +::ipc::Handler make_${toSnakeCase(prefix)}_handler(Ctx& ctx) +{ + using HandlerFn = std::function(Ctx&, const msgpack::object&)>; + static const std::unordered_map table = { +${handlerEntries}, + }; + + return [&ctx](const std::vector& raw_request) -> std::vector { + auto unpacked = msgpack::unpack( + reinterpret_cast(raw_request.data()), raw_request.size()); + auto obj = unpacked.get(); + + if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 1) { + std::cerr << "Error: Expected array of size 1\\n"; + return {}; + } + + auto& inner = obj.via.array.ptr[0]; + if (inner.type != msgpack::type::ARRAY || inner.via.array.size != 2 || + inner.via.array.ptr[0].type != msgpack::type::STR) { + std::cerr << "Error: Expected [CommandName, {payload}]\\n"; + return {}; + } + + std::string cmd_name(inner.via.array.ptr[0].via.str.ptr, inner.via.array.ptr[0].via.str.size); + auto& cmd_payload = inner.via.array.ptr[1]; + + auto it = table.find(cmd_name); + if (it == table.end()) { + return detail::make_error("unknown command: " + cmd_name); + } +#ifdef BB_NO_EXCEPTIONS + return it->second(ctx, cmd_payload); +#else + try { + return it->second(ctx, cmd_payload); + } catch (const ::ipc::ShutdownRequested&) { + throw; + } catch (const std::exception& e) { + std::cerr << "Error processing " << cmd_name << ": " << e.what() << '\\n'; + return detail::make_error(e.what()); + } +#endif + }; +} + +template +void serve(const char* socket_path, Ctx& ctx, std::atomic* shutdown_flag = nullptr) +{ + ::ipc::serve(socket_path, make_${toSnakeCase(prefix)}_handler(ctx), shutdown_flag); +} + +} // namespace ${ns} +`; + } + + /** Generate the server dispatch implementation — map-based O(1) lookup */ + generateServerImpl(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const requestType = `${prefix}Request`; + const errorTypeName = schema.errorTypeName || `${prefix}ErrorResponse`; + + const serverHeaderPath = `${this.generatedDir()}/${toSnakeCase(prefix)}_ipc_server.hpp`; + + // Generate handler lambdas for each command + const wireNs = this.opts.wireNamespace; + const handlerEntries = schema.commands.map(cmd => { + const isShutdown = cmd.name.endsWith('Shutdown'); + + // When wireNamespace is set: deserialize wire type, call handle_xxx() which returns wire response + // When not set: wire types ARE domain types, call cmd.execute(request) directly + const method = toSnakeCase(cmd.name.startsWith(prefix) ? cmd.name.slice(prefix.length) : cmd.name); + let body: string; + + if (wireNs) { + if (isShutdown) { + // Shutdown: no handler call, just serialize empty response and throw + body = `msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack_map(0);`; + } else { + const wireType = `${wireNs}::${cmd.name}`; + const deserialize = cmd.fields.length > 0 + ? `${wireType} wire_cmd; payload.convert(wire_cmd);` + : `${wireType} wire_cmd;`; + body = `${deserialize} + auto wire_resp = handle_${method}(request, std::move(wire_cmd)); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack(wire_resp);`; + } + } else { + const deserialize = cmd.fields.length > 0 + ? `${cmd.name} cmd; payload.convert(cmd);` + : `${cmd.name} cmd;`; + body = `${deserialize} + auto resp = std::move(cmd).execute(request); + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack(resp);`; + } + + if (isShutdown) { + return ` { "${cmd.name}", []([[maybe_unused]] ${requestType}& request, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + ${body} + throw ::ipc::ShutdownRequested(std::vector(buf.data(), buf.data() + buf.size())); + } }`; + } + return ` { "${cmd.name}", [](${requestType}& request, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + ${body} + return std::vector(buf.data(), buf.data() + buf.size()); + } }`; + }).join(',\n'); + + // Include wire types header when wire/domain split is used + const wireTypesInclude = wireNs + ? `#include "${this.generatedDir()}/${toSnakeCase(prefix)}_types.hpp"\n` + : ''; + + return `// AUTOGENERATED FILE - DO NOT EDIT + +#include "${serverHeaderPath}" +${wireTypesInclude}#include "barretenberg/serialize/msgpack.hpp" +#include "barretenberg/serialize/msgpack_impl.hpp" + +#include +#include +#include +#include + +namespace ${ns} { + +using CommandHandler = std::function(${requestType}&, const msgpack::object&)>; + +static const std::unordered_map& get_dispatch_table() +{ + static const std::unordered_map table = { +${handlerEntries}, + }; + return table; +} + +static std::vector make_error(const std::string& message) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(2); + pk.pack(std::string("${errorTypeName}")); + pk.pack_map(1); + pk.pack(std::string("message")); + pk.pack(message); + return std::vector(buf.data(), buf.data() + buf.size()); +} + +::ipc::Handler make_${toSnakeCase(prefix)}_handler(${requestType}& request) +{ + return [&request](const std::vector& raw_request) -> std::vector { + // Parse: [[CommandName, {payload}]] + auto unpacked = msgpack::unpack( + reinterpret_cast(raw_request.data()), raw_request.size()); + auto obj = unpacked.get(); + + if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 1) { + std::cerr << "Error: Expected array of size 1\\n"; + return {}; + } + + auto& inner = obj.via.array.ptr[0]; + if (inner.type != msgpack::type::ARRAY || inner.via.array.size != 2 || + inner.via.array.ptr[0].type != msgpack::type::STR) { + std::cerr << "Error: Expected [CommandName, {payload}]\\n"; + return {}; + } + + std::string cmd_name(inner.via.array.ptr[0].via.str.ptr, inner.via.array.ptr[0].via.str.size); + auto& cmd_payload = inner.via.array.ptr[1]; + + try { + auto& table = get_dispatch_table(); + auto it = table.find(cmd_name); + if (it == table.end()) { + return make_error("unknown command: " + cmd_name); + } + return it->second(request, cmd_payload); + } catch (const ::ipc::ShutdownRequested&) { + throw; + } catch (const std::exception& e) { + std::cerr << "Error processing " << cmd_name << ": " << e.what() << '\\n'; + return make_error(e.what()); + } + }; +} + +} // namespace ${ns} +`; + } + + // ----------------------------------------------------------------------- + // Skeleton generation (one-time handler stubs + main) + // ----------------------------------------------------------------------- + + /** Generate handler stub implementations that throw "not implemented" */ + generateHandlerStubs(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const typesHeader = `${toSnakeCase(prefix)}_ipc_server.hpp`; + const ctxName = `${prefix}Context`; + + const stubs = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + return `template<> +wire::${c.responseType} handle_${method}(${ctxName}& /*ctx*/, wire::${c.name}&& /*cmd*/) +{ + throw std::runtime_error("not implemented: ${c.name}"); +}`; + }).join('\n\n'); + + return `// Handler stubs — implement your service logic here. +// This file is generated ONCE. Edit freely — it will not be overwritten. +#include "generated/${typesHeader}" +#include + +struct ${ctxName} { + // Add your shared state here (database connection, etc.) +}; + +namespace ${ns} { + +${stubs} + +// Explicit template instantiation — must be at the bottom after all handlers. +template ::ipc::Handler make_${toSnakeCase(prefix)}_handler(${ctxName}& ctx); + +} // namespace ${ns} +`; + } + + /** Generate a main.cpp entry point for a standalone service */ + generateMain(schema: CompiledSchema): string { + const { namespace: ns, prefix } = this.opts; + const ctxName = `${prefix}Context`; + + return `// Entry point for ${prefix} service. +// This file is generated ONCE. Edit freely — it will not be overwritten. +#include "generated/${toSnakeCase(prefix)}_ipc_server.hpp" +#include "${toSnakeCase(prefix)}_handlers.cpp" + +#include +#include +#include + +static std::atomic shutdown_flag{ false }; + +int main(int argc, char* argv[]) +{ + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " \\n"; + return 1; + } + + ${ctxName} ctx{}; + std::signal(SIGTERM, [](int) { shutdown_flag.store(true); }); + std::signal(SIGINT, [](int) { shutdown_flag.store(true); }); + + std::cerr << "${prefix} server starting on " << argv[1] << "\\n"; + ::ipc::serve(argv[1], ${ns}::make_${toSnakeCase(prefix)}_handler(ctx), &shutdown_flag); + return 0; +} +`; + } + + /** Generate CMakeLists.txt for a standalone service */ + generateBuildFile(schema: CompiledSchema): string { + const { prefix } = this.opts; + const snakePrefix = toSnakeCase(prefix); + + return `cmake_minimum_required(VERSION 3.20) +project(${snakePrefix}_service CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Generated IPC code +file(GLOB GENERATED_SOURCES generated/*.cpp generated/*.hpp) + +add_executable(${snakePrefix} + main.cpp + \${GENERATED_SOURCES} +) + +target_include_directories(${snakePrefix} PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(${snakePrefix} PRIVATE pthread) +`; + } + + /** Generate .gitignore for the skeleton project */ + generateGitignore(): string { + return `# Generated IPC code — do not edit, re-run generate.sh instead +generated/ +build/ +`; + } + + /** Generate a shell script to re-run codegen */ + generateGenerateScript(schemaPath: string): string { + const { prefix, namespace: ns } = this.opts; + return `#!/usr/bin/env bash +# Re-generate IPC types, server, and client from schema. +# Run from the project root directory. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" +SCHEMA="${schemaPath}" + +node --experimental-strip-types "$(dirname "$SCRIPT_DIR")/codegen/src/generate.ts" \\ + --schema "$SCHEMA" \\ + --lang cpp \\ + --out "$SCRIPT_DIR/generated" \\ + --prefix ${prefix} \\ + --cpp-namespace ${ns} \\ + --server +`; + } +} + diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts new file mode 100644 index 000000000000..5af70dbc0f56 --- /dev/null +++ b/ipc-codegen/src/generate.ts @@ -0,0 +1,448 @@ +// CI trigger +/** + * IPC code generation CLI. + * + * Usage: + * generate.ts --schema --lang --out [flags] + * + * Required: + * --schema JSON schema file + * --lang Target language + * --out Output directory for always-regenerated code + * + * Optional: + * --prefix Type prefix (auto-detected if omitted) + * --server Generate server dispatch + * --client Generate client + * --skeleton Generate handler stubs + main (one-time, not regenerated) + * --cpp-namespace C++ namespace (e.g. bb::wsdb) + * --cpp-wire-namespace Wire types sub-namespace (default: wire) + * --curve-constants Generate TS curve constants (bb-only special case) + * + * Zero npm dependencies — runs with Node.js 22+ via --experimental-strip-types. + */ + +import { createHash } from 'crypto'; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { execSync } from 'child_process'; +import { dirname, join, resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { SchemaVisitor, type CompiledSchema } from './schema_visitor.ts'; +import { TypeScriptCodegen } from './typescript_codegen.ts'; +import { RustCodegen } from './rust_codegen.ts'; +import { ZigCodegen } from './zig_codegen.ts'; +import { CppCodegen } from './cpp_codegen.ts'; +import { toSnakeCase } from './naming.ts'; + +// @ts-ignore +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Args { + schema: string; + lang: string; + out: string; + prefix: string; + server: boolean; + client: boolean; + skeleton: string; + cppNamespace: string; + cppWireNamespace: string; + cppIncludeDir: string; + uds: boolean; + ffi: boolean; + curveConstants: boolean; + stripMethodPrefix: boolean; +} + +function parseArgs(argv: string[]): Args { + const args: Args = { + schema: '', lang: '', out: '', prefix: '', + server: false, client: false, skeleton: '', + cppNamespace: '', cppWireNamespace: 'wire', cppIncludeDir: '', + uds: false, ffi: false, + curveConstants: false, stripMethodPrefix: false, + }; + + for (let i = 0; i < argv.length; i++) { + switch (argv[i]) { + case '--schema': args.schema = argv[++i]; break; + case '--lang': args.lang = argv[++i]; break; + case '--out': args.out = argv[++i]; break; + case '--prefix': args.prefix = argv[++i]; break; + case '--server': args.server = true; break; + case '--client': args.client = true; break; + case '--skeleton': args.skeleton = argv[++i]; break; + case '--cpp-namespace': args.cppNamespace = argv[++i]; break; + case '--cpp-wire-namespace': args.cppWireNamespace = argv[++i]; break; + case '--cpp-include-dir': args.cppIncludeDir = argv[++i]; break; + case '--uds': args.uds = true; break; + case '--ffi': args.ffi = true; break; + case '--curve-constants': args.curveConstants = true; break; + case '--strip-method-prefix': args.stripMethodPrefix = true; break; + default: + console.error(`Unknown flag: ${argv[i]}`); + process.exit(1); + } + } + + if (!args.schema || !args.lang || !args.out) { + console.error(`Usage: generate.ts --schema --lang --out [flags] + +Required: + --schema JSON schema file + --lang Target language (ts, rust, zig, cpp) + --out Output directory + +Optional: + --server Generate server dispatch + --client Generate client + --skeleton Generate handler stubs + main (one-time) + --prefix Type prefix (auto-detected if omitted) + --cpp-namespace C++ namespace (e.g. bb::wsdb) + --cpp-wire-namespace Wire types sub-namespace (default: wire) + --cpp-include-dir Include path for generated dir (e.g. barretenberg/wsdb/generated) + --curve-constants Generate TS curve constants + --strip-method-prefix Strip prefix from TS method names (e.g. BbCircuitProve -> circuitProve)`); + process.exit(1); + } + + return args; +} + +// --------------------------------------------------------------------------- +// Schema loading +// --------------------------------------------------------------------------- + +function computeSchemaHash(schemaJson: string): string { + return createHash('sha256').update(schemaJson).digest('hex'); +} + +function loadSchema(schemaPath: string): { compiled: CompiledSchema; schemaHash: string } { + const rawJson = readFileSync(schemaPath, 'utf-8').trim(); + const schema = JSON.parse(rawJson); + const visitor = new SchemaVisitor(); + const compiled = visitor.visit(schema.commands, schema.responses); + const schemaHash = computeSchemaHash(rawJson); + return { compiled, schemaHash }; +} + +/** Detect common prefix from command names (e.g. WsdbGetTreeInfo, WsdbCreateFork → Wsdb) */ +function detectPrefix(compiled: CompiledSchema): string { + const names = compiled.commands.map(c => c.name); + if (names.length === 0) return ''; + let prefix = names[0]; + for (const name of names.slice(1)) { + while (prefix && !name.startsWith(prefix)) { + prefix = prefix.slice(0, -1); + } + } + const words = prefix.match(/[A-Z][a-z]*/g) || []; + let result = ''; + for (const word of words) { + const candidate = result + word; + if (names.every(n => n.startsWith(candidate))) { + result = candidate; + } else { + break; + } + } + return result; +} + +// --------------------------------------------------------------------------- +// Template copying +// --------------------------------------------------------------------------- + +function copyTemplate(lang: string, filename: string, outDir: string) { + const templatePath = join(__dirname, '..', 'templates', lang, filename); + const destPath = join(outDir, filename); + writeFileSync(destPath, readFileSync(templatePath, 'utf-8')); + console.log(` ${destPath} (template)`); +} + +/** Copy template only if destination doesn't exist (idempotent, one-time) */ +function copyTemplateOnce(lang: string, filename: string, outDir: string) { + const destPath = join(outDir, filename); + if (existsSync(destPath)) { + console.log(` ${destPath} (exists, skipped)`); + return; + } + copyTemplate(lang, filename, outDir); +} + +// --------------------------------------------------------------------------- +// C++ clang-format +// --------------------------------------------------------------------------- + +function formatCpp(files: string[]) { + if (files.length === 0) return; + try { + execSync(`clang-format-20 -i ${files.join(' ')}`, { stdio: 'ignore' }); + } catch { + // clang-format-20 may not be available + } +} + +// --------------------------------------------------------------------------- +// Generation +// --------------------------------------------------------------------------- + +function generate(args: Args) { + const absSchema = resolve(args.schema); + const absOut = resolve(args.out); + mkdirSync(absOut, { recursive: true }); + + const { compiled, schemaHash } = loadSchema(absSchema); + const prefix = args.prefix || detectPrefix(compiled); + + console.log(`Schema: ${absSchema} (${compiled.commands.length} commands, prefix=${prefix})`); + + function writeFile(name: string, content: string) { + const path = join(absOut, name); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content); + console.log(` ${path}`); + return path; + } + + const cppFiles: string[] = []; + + switch (args.lang) { + case 'ts': { + const gen = new TypeScriptCodegen({ stripMethodPrefix: args.stripMethodPrefix ? prefix : undefined }); + writeFile('api_types.ts', gen.generateTypes(compiled, schemaHash)); + if (args.server) { + writeFile('server.ts', gen.generateServerApi(compiled)); + copyTemplate('ts', 'ipc_server.ts', absOut); + } + if (args.client) { + writeFile('async.ts', gen.generateAsyncApi(compiled)); + writeFile('sync.ts', gen.generateSyncApi(compiled)); + copyTemplate('ts', 'ipc_client.ts', absOut); + } + if (args.curveConstants) { + generateCurveConstants(absOut); + } + // Skeleton (one-time handler stubs + main + build files) + if (args.skeleton) { + const skelDir = resolve(args.skeleton); + mkdirSync(skelDir, { recursive: true }); + const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const path = join(skelDir, name); + if (existsSync(path)) { + console.log(` ${path} (exists, skipped)`); + return; + } + writeFileSync(path, content); + if (opts?.executable) { + try { execSync(`chmod +x ${path}`); } catch {} + } + console.log(` ${path} (skeleton)`); + }; + writeSkeleton(`${toSnakeCase(prefix)}_handlers.ts`, gen.generateHandlerStubs(compiled, prefix)); + writeSkeleton('main.ts', gen.generateMain(compiled, prefix)); + writeSkeleton('package.json', gen.generateBuildFile(prefix)); + writeSkeleton('.gitignore', gen.generateGitignore()); + writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema, prefix), { executable: true }); + } + break; + } + case 'rust': { + const gen = new RustCodegen({ prefix }); + writeFile(`${toSnakeCase(prefix)}_types.rs`, gen.generateTypes(compiled, schemaHash)); + if (args.server) { + writeFile(`${toSnakeCase(prefix)}_server.rs`, gen.generateServer(compiled)); + copyTemplate('rust', 'ipc_server.rs', absOut); + } + if (args.client) { + writeFile(`${toSnakeCase(prefix)}_client.rs`, gen.generateApi(compiled)); + } + // Backend templates (copied once, not overwritten) + if (args.uds || args.ffi) { + copyTemplateOnce('rust', 'backend.rs', absOut); + copyTemplateOnce('rust', 'error.rs', absOut); + } + if (args.uds) { + copyTemplateOnce('rust', 'uds_backend.rs', absOut); + } + if (args.ffi) { + copyTemplateOnce('rust', 'ffi_backend.rs', absOut); + } + // Skeleton (one-time handler stubs + main + build files) + if (args.skeleton) { + const skelDir = resolve(args.skeleton); + mkdirSync(skelDir, { recursive: true }); + const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const path = join(skelDir, name); + if (existsSync(path)) { + console.log(` ${path} (exists, skipped)`); + return; + } + writeFileSync(path, content); + if (opts?.executable) { + try { execSync(`chmod +x ${path}`); } catch {} + } + console.log(` ${path} (skeleton)`); + }; + writeSkeleton(`${toSnakeCase(prefix)}_handlers.rs`, gen.generateHandlerStubs(compiled)); + writeSkeleton('main.rs', gen.generateMain(compiled)); + writeSkeleton('Cargo.toml', gen.generateBuildFile(compiled)); + writeSkeleton('.gitignore', gen.generateGitignore()); + writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema), { executable: true }); + } + break; + } + case 'zig': { + const gen = new ZigCodegen({ prefix, clientName: `${prefix}Client` }); + writeFile(`${toSnakeCase(prefix)}_types.zig`, gen.generateTypes(compiled, schemaHash)); + if (args.server) { + writeFile(`${toSnakeCase(prefix)}_server.zig`, gen.generateServer(compiled)); + copyTemplate('zig', 'ipc_server.zig', absOut); + } + if (args.client) { + writeFile(`${toSnakeCase(prefix)}_client.zig`, gen.generateClient(compiled)); + } + // Backend templates (copied once, not overwritten) + if (args.uds || args.ffi) { + copyTemplateOnce('zig', 'backend.zig', absOut); + } + if (args.uds) { + copyTemplateOnce('zig', 'uds_backend.zig', absOut); + } + if (args.ffi) { + copyTemplateOnce('zig', 'ffi_backend.zig', absOut); + } + // Skeleton (one-time handler stubs + main + build files) + if (args.skeleton) { + const skelDir = resolve(args.skeleton); + mkdirSync(skelDir, { recursive: true }); + const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const path = join(skelDir, name); + if (existsSync(path)) { + console.log(` ${path} (exists, skipped)`); + return; + } + writeFileSync(path, content); + if (opts?.executable) { + try { execSync(`chmod +x ${path}`); } catch {} + } + console.log(` ${path} (skeleton)`); + }; + writeSkeleton(`${toSnakeCase(prefix)}_handlers.zig`, gen.generateHandlerStubs(compiled)); + writeSkeleton('main.zig', gen.generateMain(compiled)); + writeSkeleton('build.zig', gen.generateBuildFile(compiled)); + writeSkeleton('build.zig.zon', gen.generateBuildZon(compiled)); + writeSkeleton('.gitignore', gen.generateGitignore()); + writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema), { executable: true }); + } + break; + } + case 'cpp': { + const ns = args.cppNamespace || prefix.toLowerCase(); + const wireNs = args.cppWireNamespace; + const gen = new CppCodegen({ + namespace: ns, + prefix, + executeHeader: '', + commandsHeader: '', + wireNamespace: wireNs, + generatedIncludeDir: args.cppIncludeDir, + }); + + cppFiles.push(writeFile(`${toSnakeCase(prefix)}_types.hpp`, gen.generateStandaloneTypes(compiled))); + if (args.server) { + cppFiles.push(writeFile(`${toSnakeCase(prefix)}_ipc_server.hpp`, gen.generateServerHeader(compiled))); + copyTemplate('cpp', 'ipc_server.hpp', absOut); + } + if (args.client) { + cppFiles.push(writeFile(`${toSnakeCase(prefix)}_ipc_client.hpp`, gen.generateHeader(compiled, schemaHash))); + cppFiles.push(writeFile(`${toSnakeCase(prefix)}_ipc_client.cpp`, gen.generateImpl(compiled))); + copyTemplate('cpp', 'ipc_client.hpp', absOut); + } + + // Skeleton (one-time handler stubs + main + build files) + if (args.skeleton) { + const skelDir = resolve(args.skeleton); + mkdirSync(skelDir, { recursive: true }); + const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const path = join(skelDir, name); + if (existsSync(path)) { + console.log(` ${path} (exists, skipped)`); + return; + } + writeFileSync(path, content); + if (opts?.executable) { + try { execSync(`chmod +x ${path}`); } catch {} + } + console.log(` ${path} (skeleton)`); + if (path.endsWith('.cpp') || path.endsWith('.hpp')) { + cppFiles.push(path); + } + }; + writeSkeleton(`${toSnakeCase(prefix)}_handlers.cpp`, gen.generateHandlerStubs(compiled)); + writeSkeleton('main.cpp', gen.generateMain(compiled)); + writeSkeleton('CMakeLists.txt', gen.generateBuildFile(compiled)); + writeSkeleton('.gitignore', gen.generateGitignore()); + writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema), { executable: true }); + } + + formatCpp(cppFiles); + break; + } + default: + console.error(`Unknown language: ${args.lang}. Available: ts, rust, zig, cpp`); + process.exit(1); + } + + console.log('Done.'); +} + +// --------------------------------------------------------------------------- +// Curve constants (special case for bb) +// --------------------------------------------------------------------------- + +function hexToBigInt(hex: string): bigint { return BigInt('0x' + hex); } + +function hexToByteList(hex: string): string { + const bytes: number[] = []; + for (let i = 0; i < hex.length; i += 2) bytes.push(parseInt(hex.substring(i, i + 2), 16)); + return `new Uint8Array([${bytes.join(', ')}])`; +} + +function serializeCoordinate(coord: string | string[]): string { + return Array.isArray(coord) ? `[${coord.map(c => hexToByteList(c)).join(', ')}]` : hexToByteList(coord); +} + +function generateCurveConstants(outputDir: string) { + const constantsPath = join(__dirname, '../schemas/bb_curve_constants.json'); + const constants = JSON.parse(readFileSync(constantsPath, 'utf-8')); + const content = `// AUTOGENERATED FILE - DO NOT EDIT +export const BN254_FR_MODULUS = ${hexToBigInt(constants.bn254_fr_modulus)}n; +export const BN254_FQ_MODULUS = ${hexToBigInt(constants.bn254_fq_modulus)}n; +export const BN254_G1_GENERATOR = { x: ${serializeCoordinate(constants.bn254_g1_generator.x)}, y: ${serializeCoordinate(constants.bn254_g1_generator.y)} } as const; +export const BN254_G2_GENERATOR = { x: ${serializeCoordinate(constants.bn254_g2_generator.x)}, y: ${serializeCoordinate(constants.bn254_g2_generator.y)} } as const; +export const GRUMPKIN_FR_MODULUS = ${hexToBigInt(constants.grumpkin_fr_modulus)}n; +export const GRUMPKIN_FQ_MODULUS = ${hexToBigInt(constants.grumpkin_fq_modulus)}n; +export const GRUMPKIN_G1_GENERATOR = { x: ${serializeCoordinate(constants.grumpkin_g1_generator.x)}, y: ${serializeCoordinate(constants.grumpkin_g1_generator.y)} } as const; +export const SECP256K1_FR_MODULUS = ${hexToBigInt(constants.secp256k1_fr_modulus)}n; +export const SECP256K1_FQ_MODULUS = ${hexToBigInt(constants.secp256k1_fq_modulus)}n; +export const SECP256K1_G1_GENERATOR = { x: ${serializeCoordinate(constants.secp256k1_g1_generator.x)}, y: ${serializeCoordinate(constants.secp256k1_g1_generator.y)} } as const; +export const SECP256R1_FR_MODULUS = ${hexToBigInt(constants.secp256r1_fr_modulus)}n; +export const SECP256R1_FQ_MODULUS = ${hexToBigInt(constants.secp256r1_fq_modulus)}n; +export const SECP256R1_G1_GENERATOR = { x: ${serializeCoordinate(constants.secp256r1_g1_generator.x)}, y: ${serializeCoordinate(constants.secp256r1_g1_generator.y)} } as const; +`; + mkdirSync(outputDir, { recursive: true }); + writeFileSync(join(outputDir, 'curve_constants.ts'), content); + console.log(` ${join(outputDir, 'curve_constants.ts')}`); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const args = parseArgs(process.argv.slice(2)); +generate(args); diff --git a/ipc-codegen/src/naming.ts b/ipc-codegen/src/naming.ts new file mode 100644 index 000000000000..7ae896683fda --- /dev/null +++ b/ipc-codegen/src/naming.ts @@ -0,0 +1,27 @@ +/** + * Shared naming utilities for code generators + */ + +/** + * Convert camelCase or PascalCase to snake_case + * @example toSnakeCase("Blake2s") -> "blake2s" + * @example toSnakeCase("poseidonHash") -> "poseidon_hash" + */ +export function toSnakeCase(name: string): string { + return name.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); +} + +/** + * Convert snake_case to PascalCase + * @example toPascalCase("blake2s") -> "Blake2s" + * @example toPascalCase("poseidon_hash") -> "PoseidonHash" + */ +export function toPascalCase(name: string): string { + // Already PascalCase (no underscores and starts with uppercase) + if (!name.includes('_') && name[0] === name[0].toUpperCase()) { + return name; + } + return name.split('_').map(part => + part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() + ).join(''); +} diff --git a/ipc-codegen/src/rust_codegen.ts b/ipc-codegen/src/rust_codegen.ts new file mode 100644 index 000000000000..6cbae852964d --- /dev/null +++ b/ipc-codegen/src/rust_codegen.ts @@ -0,0 +1,783 @@ +/** + * Rust Code Generator - String template based + * + * Philosophy: + * - String templates for file structure + * - Simple type mapping + * - Idiomatic Rust conventions + * - No complex abstraction + */ + +import type { CompiledSchema, Type, Struct, Field } from './schema_visitor.ts'; +import { toSnakeCase, toPascalCase } from './naming.ts'; + +export interface RustCodegenOptions { + /** Prefix for stripping from method names, e.g. 'Wsdb' makes WsdbGetTreeInfo -> get_tree_info */ + prefix?: string; + /** API struct name, e.g. 'WsdbApi'. Defaults to 'BarretenbergApi' */ + apiStructName?: string; + /** Import path for Backend trait. Defaults to 'crate::backend::Backend' */ + backendImport?: string; + /** Import path for error types. Defaults to 'crate::error::{BarretenbergError, Result}' */ + errorImport?: string; + /** Import path for generated types. Defaults to 'crate::types_gen::*' */ + typesImport?: string; + /** Module doc comment for types file */ + typesDocComment?: string; + /** Module doc comment for api file */ + apiDocComment?: string; +} + +export class RustCodegen { + private errorTypeName: string = 'ErrorResponse'; + private opts: Required; + + constructor(options?: RustCodegenOptions) { + const prefix = options?.prefix ?? ''; + const name = prefix || 'Barretenberg'; + this.opts = { + prefix, + apiStructName: options?.apiStructName ?? `${name}Api`, + backendImport: options?.backendImport ?? 'super::backend::Backend', + errorImport: options?.errorImport ?? `super::error::{BarretenbergError, Result}`, + typesImport: options?.typesImport ?? `super::${toSnakeCase(prefix || 'bb')}_types::*`, + typesDocComment: options?.typesDocComment ?? `Generated types for ${name} IPC protocol`, + apiDocComment: options?.apiDocComment ?? `${name} IPC client API`, + }; + } + + // Type mapping: Schema type -> Rust type + private mapType(type: Type): string { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return 'bool'; + case 'u8': return 'u8'; + case 'u16': return 'u16'; + case 'u32': return 'u32'; + case 'u64': return 'u64'; + case 'f64': return 'f64'; + case 'string': return 'String'; + case 'bytes': return 'Vec'; + case 'fr': return 'Fr'; // 32-byte field element + case 'field2': return '[Fr; 2]'; // Extension field (Fq2) + } + break; + + case 'vector': + return `Vec<${this.mapType(type.element!)}>`; + + case 'array': + const elemType = this.mapType(type.element!); + // Large arrays become Vec for ergonomics + return type.size! > 32 ? `Vec<${elemType}>` : `[${elemType}; ${type.size}]`; + + case 'optional': + return `Option<${this.mapType(type.element!)}>`; + + case 'struct': + // Convert struct names to PascalCase for Rust conventions + return toPascalCase(type.struct!.name); + } + + return 'Unknown'; + } + + // Check if field needs serde(with = "serde_bytes") + private needsSerdeBytes(type: Type): boolean { + return type.kind === 'primitive' && type.primitive === 'bytes'; + } + + // Check if field needs serde(with = "serde_vec_bytes") + private needsSerdeVecBytes(type: Type): boolean { + return type.kind === 'vector' && this.needsSerdeBytes(type.element!); + } + + // Check if field needs serde(with = "serde_array4_bytes") - for [Vec; 4] (Poseidon2 state) + private needsSerdeArray4Bytes(type: Type): boolean { + return type.kind === 'array' && type.size === 4 && this.needsSerdeBytes(type.element!); + } + + // Generate struct field + private generateField(field: Field): string { + const rustName = toSnakeCase(field.name); + const rustType = this.mapType(field.type); + let attrs = ''; + + // Add serde rename if needed + if (field.name !== rustName) { + attrs += ` #[serde(rename = "${field.name}")]\n`; + } + + // Add serde bytes handling + if (this.needsSerdeArray4Bytes(field.type)) { + attrs += ` #[serde(with = "serde_array4_bytes")]\n`; + } else if (this.needsSerdeVecBytes(field.type)) { + attrs += ` #[serde(with = "serde_vec_bytes")]\n`; + } else if (this.needsSerdeBytes(field.type)) { + attrs += ` #[serde(with = "serde_bytes")]\n`; + } + + return `${attrs} pub ${rustName}: ${rustType},`; + } + + // Generate a struct definition + private generateStruct(struct: Struct, isCommand: boolean): string { + const rustName = toPascalCase(struct.name); + const fields = struct.fields.map(f => this.generateField(f)).join('\n'); + + // Add serde rename if struct name changed + const serdeRename = struct.name !== rustName + ? `\n#[serde(rename = "${struct.name}")]` + : ''; + + // Commands have a __typename used for NamedUnion identification, but it's handled + // by the Command enum's custom serde, not by the struct itself. + const typenameField = isCommand + ? ` #[serde(rename = "__typename", skip, default)]\n pub type_name: String,\n` + : ''; + + // Generate constructor for commands + const constructor = isCommand ? this.generateConstructor(struct, rustName) : ''; + + return `/// ${struct.name} +#[derive(Debug, Clone, Serialize, Deserialize)]${serdeRename} +pub struct ${rustName} { +${typenameField}${fields} +}${constructor}`; + } + + // Generate constructor for command structs + private generateConstructor(struct: Struct, rustName: string): string { + const params = struct.fields.map(f => + `${toSnakeCase(f.name)}: ${this.mapType(f.type)}` + ).join(', '); + + const fieldInits = [ + ` type_name: "${struct.name}".to_string(),`, + ...struct.fields.map(f => ` ${toSnakeCase(f.name)},`), + ].join('\n'); + + return ` + +impl ${rustName} { + pub fn new(${params}) -> Self { + Self { +${fieldInits} + } + } +}`; + } + + // Generate Command enum + private generateCommandEnum(schema: CompiledSchema): string { + const names = schema.commands.map(c => c.name); + const variants = names + .map(name => { + const rustName = toPascalCase(name); + return ` ${rustName}(${rustName}),`; + }) + .join('\n'); + + const serializeCases = names + .map(name => { + const rustName = toPascalCase(name); + return ` Command::${rustName}(data) => { + tuple.serialize_element("${name}")?; + tuple.serialize_element(data)?; + }`; + }) + .join('\n'); + + const deserializeCases = names + .map(name => { + const rustName = toPascalCase(name); + return ` "${name}" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Command::${rustName}(data)) + }`; + }) + .join('\n'); + + const variantNames = names + .map(name => `"${name}"`) + .join(', '); + + return `/// Command enum - wraps all possible commands +#[derive(Debug, Clone)] +pub enum Command { +${variants} +} + +impl Serialize for Command { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + use serde::ser::SerializeTuple; + let mut tuple = serializer.serialize_tuple(2)?; + match self { +${serializeCases} + } + tuple.end() + } +} + +impl<'de> Deserialize<'de> for Command { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + use serde::de::{SeqAccess, Visitor}; + struct CommandVisitor; + + impl<'de> Visitor<'de> for CommandVisitor { + type Value = Command; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a 2-element array [name, payload]") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let name: String = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + match name.as_str() { +${deserializeCases} + _ => Err(serde::de::Error::unknown_variant(&name, &[${variantNames}])), + } + } + } + deserializer.deserialize_tuple(2, CommandVisitor) + } +}`; + } + + // Generate Response enum + private generateResponseEnum(schema: CompiledSchema): string { + // Include all response types from commands plus ErrorResponse if it exists + const commandResponseTypes = Array.from(new Set(schema.commands.map(c => c.responseType))); + const errorName = schema.errorTypeName || 'ErrorResponse'; + const responseTypes = schema.responses.has(errorName) + ? [...commandResponseTypes, errorName] + : commandResponseTypes; + const variants = responseTypes + .map(name => { + const rustName = toPascalCase(name); + return ` ${rustName}(${rustName}),`; + }) + .join('\n'); + + const serializeCases = responseTypes + .map(name => { + const rustName = toPascalCase(name); + return ` Response::${rustName}(data) => { + tuple.serialize_element("${name}")?; + tuple.serialize_element(data)?; + }`; + }) + .join('\n'); + + const deserializeCases = responseTypes + .map(name => { + const rustName = toPascalCase(name); + return ` "${name}" => { + let data = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + Ok(Response::${rustName}(data)) + }`; + }) + .join('\n'); + + const variantNames = responseTypes.map(name => `"${name}"`).join(', '); + + return `/// Response enum - wraps all possible responses +#[derive(Debug, Clone)] +pub enum Response { +${variants} +} + +impl Serialize for Response { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + use serde::ser::SerializeTuple; + let mut tuple = serializer.serialize_tuple(2)?; + match self { +${serializeCases} + } + tuple.end() + } +} + +impl<'de> Deserialize<'de> for Response { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + use serde::de::{SeqAccess, Visitor}; + struct ResponseVisitor; + + impl<'de> Visitor<'de> for ResponseVisitor { + type Value = Response; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a 2-element array [name, payload]") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let name: String = seq.next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + match name.as_str() { +${deserializeCases} + _ => Err(serde::de::Error::unknown_variant(&name, &[${variantNames}])), + } + } + } + deserializer.deserialize_tuple(2, ResponseVisitor) + } +}`; + } + + // Generate serde helper modules + private generateSerdeHelpers(): string { + return `mod serde_bytes { + use serde::{Deserialize, Deserializer, Serializer}; + pub fn serialize(bytes: &Vec, serializer: S) -> Result + where S: Serializer { serializer.serialize_bytes(bytes) } + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { >::deserialize(deserializer) } +} + +mod serde_vec_bytes { + use serde::{Deserialize, Deserializer, Serializer, Serialize}; + use serde::ser::SerializeSeq; + use serde::de::{SeqAccess, Visitor}; + + #[derive(Serialize, Deserialize)] + struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); + + pub fn serialize(vec: &Vec>, serializer: S) -> Result + where S: Serializer { + let mut seq = serializer.serialize_seq(Some(vec.len()))?; + for bytes in vec { + seq.serialize_element(&BytesWrapper(bytes.clone()))?; + } + seq.end() + } + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where D: Deserializer<'de> { + struct VecVecU8Visitor; + impl<'de> Visitor<'de> for VecVecU8Visitor { + type Value = Vec>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a sequence of byte arrays") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let mut vec = Vec::new(); + while let Some(wrapper) = seq.next_element::()? { + vec.push(wrapper.0); + } + Ok(vec) + } + } + deserializer.deserialize_seq(VecVecU8Visitor) + } +} + +mod serde_array4_bytes { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use serde::ser::SerializeTuple; + use serde::de::{SeqAccess, Visitor}; + + #[derive(Serialize, Deserialize)] + struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); + + pub fn serialize(arr: &[Vec; 4], serializer: S) -> Result + where S: Serializer { + let mut tup = serializer.serialize_tuple(4)?; + for bytes in arr { + tup.serialize_element(&BytesWrapper(bytes.clone()))?; + } + tup.end() + } + pub fn deserialize<'de, D>(deserializer: D) -> Result<[Vec; 4], D::Error> + where D: Deserializer<'de> { + struct Array4Visitor; + impl<'de> Visitor<'de> for Array4Visitor { + type Value = [Vec; 4]; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an array of 4 byte arrays") + } + fn visit_seq(self, mut seq: A) -> Result + where A: SeqAccess<'de> { + let mut arr: [Vec; 4] = Default::default(); + for (i, item) in arr.iter_mut().enumerate() { + *item = seq.next_element::()? + .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?.0; + } + Ok(arr) + } + } + deserializer.deserialize_tuple(4, Array4Visitor) + } +}`; + } + + // Generate types file + generateTypes(schema: CompiledSchema, schemaHash?: string): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + // Create set of top-level command struct names (only these need __typename) + const commandNames = new Set(schema.commands.map(c => c.name)); + + // Generate all structs (commands first, then responses) + const commandStructs = Array.from(schema.structs.values()) + .map(s => this.generateStruct(s, commandNames.has(s.name))) + .join('\n\n'); + + const responseStructs = Array.from(schema.responses.values()) + .map(s => this.generateStruct(s, false)) + .join('\n\n'); + + const hashLine = schemaHash ? `\n/// Schema version hash for compatibility checking\npub const SCHEMA_HASH: &str = "${schemaHash}";\n` : ''; + + return `//! AUTOGENERATED - DO NOT EDIT +//! ${this.opts.typesDocComment} + +use serde::{Deserialize, Serialize}; +${hashLine} +/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated, no heap. +/// Serializes as msgpack bin32 on the wire. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Fr(pub [u8; 32]); + +impl Fr { + pub fn from_bytes(bytes: [u8; 32]) -> Self { Self(bytes) } + pub fn to_bytes(&self) -> &[u8; 32] { &self.0 } + pub fn as_slice(&self) -> &[u8] { &self.0 } +} + +impl Serialize for Fr { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de> Deserialize<'de> for Fr { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + let bytes: Vec = >::deserialize(deserializer)?; + let arr: [u8; 32] = bytes.try_into() + .map_err(|v: Vec| serde::de::Error::invalid_length(v.len(), &"32 bytes"))?; + Ok(Fr(arr)) + } +} + +${this.generateSerdeHelpers()} + +${commandStructs} + +${responseStructs} + +${this.generateCommandEnum(schema)} + +${this.generateResponseEnum(schema)} +`; + } + + /** Strip the service prefix from a command name for the method name */ + private methodName(commandName: string): string { + const withoutPrefix = this.opts.prefix && commandName.startsWith(this.opts.prefix) + ? commandName.slice(this.opts.prefix.length) + : commandName; + return toSnakeCase(withoutPrefix); + } + + // Generate API method + private generateApiMethod(command: {name: string, fields: Field[], responseType: string}): string { + const methodName = this.methodName(command.name); + const cmdRustName = toPascalCase(command.name); + const respRustName = toPascalCase(command.responseType); + + const params = command.fields.map(f => { + const rustType = this.mapType(f.type); + // Only convert simple Vec to &[u8], not nested types + const apiType = rustType === 'Vec' ? '&[u8]' : rustType; + return `${toSnakeCase(f.name)}: ${apiType}`; + }).join(', '); + + const paramConversions = command.fields.map(f => { + const name = toSnakeCase(f.name); + const rustType = this.mapType(f.type); + // Only convert slices back to Vec + if (rustType === 'Vec') { + return `${name}.to_vec()`; + } + return name; + }).join(', '); + + // Extract error type name from the error import (e.g., 'BarretenbergError' from 'crate::error::{BarretenbergError, Result}') + const errorType = this.opts.errorImport.match(/\{(\w+),/)?.[1] ?? 'BarretenbergError'; + + return ` /// Execute ${command.name} + pub fn ${methodName}(&mut self, ${params}) -> Result<${respRustName}> { + let cmd = Command::${cmdRustName}(${cmdRustName}::new(${paramConversions})); + match self.execute(cmd)? { + Response::${respRustName}(resp) => Ok(resp), + Response::${toPascalCase(this.errorTypeName)}(err) => Err(${errorType}::Backend( + err.message + )), + _ => Err(${errorType}::InvalidResponse( + "Expected ${command.responseType}".to_string() + )), + } + }`; + } + + // Generate API file + generateApi(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const { apiStructName, backendImport, errorImport, typesImport, apiDocComment } = this.opts; + + // Find shutdown command name (may be prefixed, e.g. WsdbShutdown) + const shutdownCmd = schema.commands.find(c => c.name.endsWith('Shutdown')); + const shutdownName = shutdownCmd ? toPascalCase(shutdownCmd.name) : null; + + const apiMethods = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => this.generateApiMethod(c)) + .join('\n\n'); + + const shutdownMethod = shutdownName ? ` + /// Shutdown backend gracefully + pub fn shutdown(&mut self) -> Result<()> { + let cmd = Command::${shutdownName}(${shutdownName}::new()); + let _ = self.execute(cmd)?; + self.backend.destroy() + } +` : ''; + + const errorType = errorImport.match(/\{(\w+),/)?.[1] ?? 'BarretenbergError'; + + return `//! AUTOGENERATED - DO NOT EDIT +//! ${apiDocComment} + +use ${backendImport}; +use ${errorImport}; +use ${typesImport}; + +/// ${apiDocComment} +pub struct ${apiStructName} { + backend: B, +} + +impl ${apiStructName} { + /// Create API with custom backend + pub fn new(backend: B) -> Self { + Self { backend } + } + + fn execute(&mut self, command: Command) -> Result { + let input_buffer = rmp_serde::to_vec_named(&vec![command]) + .map_err(|e| ${errorType}::Serialization(e.to_string()))?; + + let output_buffer = self.backend.call(&input_buffer)?; + + let response: Response = rmp_serde::from_slice(&output_buffer) + .map_err(|e| ${errorType}::Deserialization(e.to_string()))?; + + Ok(response) + } + +${apiMethods} +${shutdownMethod} + /// Destroy backend without shutdown command + pub fn destroy(&mut self) -> Result<()> { + self.backend.destroy() + } +} +`; + } + + // ----------------------------------------------------------------------- + // Server-side code generation + // ----------------------------------------------------------------------- + + /** Generate a Handler trait and serve() function */ + generateServer(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const { prefix, errorImport, typesImport } = this.opts; + const errorType = errorImport.match(/\{(\w+),/)?.[1] ?? 'IpcError'; + const errorRespType = toPascalCase(this.errorTypeName); + + const traitMethods = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const cmdRustName = toPascalCase(c.name); + const respRustName = toPascalCase(c.responseType); + return ` fn ${methodName}(&mut self, cmd: ${cmdRustName}) -> Result<${respRustName}>;`; + }) + .join('\n'); + + const dispatchArms = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const cmdRustName = toPascalCase(c.name); + const respRustName = toPascalCase(c.responseType); + return ` Command::${cmdRustName}(cmd) => { + match handler.${methodName}(cmd) { + Ok(resp) => Response::${respRustName}(resp), + Err(e) => Response::${errorRespType}(${errorRespType} { message: e.to_string() }), + } + }`; + }) + .join('\n'); + + // Handle shutdown arm + const shutdownCmd = schema.commands.find(c => c.name.endsWith('Shutdown')); + const shutdownArm = shutdownCmd + ? ` Command::${toPascalCase(shutdownCmd.name)}(_) => { + return Err(${errorType}::Backend("shutdown requested".to_string())); + }` + : ''; + + return `//! AUTOGENERATED - DO NOT EDIT +//! Server-side dispatch for ${prefix || 'service'} IPC protocol + +use ${errorImport}; +use ${typesImport}; + +/// Handler trait — implement this to serve ${prefix || 'service'} commands. +pub trait Handler { +${traitMethods} +} + +/// Dispatch a single command to the handler and return the response. +pub fn dispatch(handler: &mut dyn Handler, command: Command) -> Result { + let response = match command { +${dispatchArms} +${shutdownArm} + }; + Ok(response) +} +`; + } + + // ----------------------------------------------------------------------- + // Skeleton generation (one-time handler stubs + main + build files) + // ----------------------------------------------------------------------- + + /** Generate handler stub implementations that return unimplemented errors */ + generateHandlerStubs(schema: CompiledSchema): string { + const { prefix } = this.opts; + const typesModule = `${toSnakeCase(prefix)}_types`; + const serverModule = `${toSnakeCase(prefix)}_server`; + const ctxName = `${prefix}Context`; + + const stubs = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const cmdRustName = toPascalCase(c.name); + const respRustName = toPascalCase(c.responseType); + return ` fn ${methodName}(&mut self, _cmd: ${typesModule}::${cmdRustName}) -> Result<${typesModule}::${respRustName}> { + unimplemented!("${c.name}") + }`; + }).join('\n\n'); + + return `// Handler stubs — implement your service logic here. +// This file is generated ONCE. Edit freely — it will not be overwritten. + +mod generated { + pub mod ${typesModule}; + pub mod ${serverModule}; + pub mod ipc_server; +} + +use generated::${typesModule}; +use generated::${serverModule}; + +/// Shared context for your service — add database connections, state, etc. +pub struct ${ctxName} { + // Add your shared state here +} + +/// Handler implementation +pub struct ${prefix}Handler { + pub ctx: ${ctxName}, +} + +impl ${serverModule}::Handler for ${prefix}Handler { +${stubs} +} +`; + } + + /** Generate a main.rs entry point for a standalone service */ + generateMain(schema: CompiledSchema): string { + const { prefix } = this.opts; + const ctxName = `${prefix}Context`; + const serverModule = `${toSnakeCase(prefix)}_server`; + + return `// Entry point for ${prefix} service. +// This file is generated ONCE. Edit freely — it will not be overwritten. + +mod ${toSnakeCase(prefix)}_handlers; + +use ${toSnakeCase(prefix)}_handlers::{${ctxName}, ${prefix}Handler}; + +fn main() { + let socket_path = std::env::args().nth(1).expect("Usage: ${toSnakeCase(prefix)} "); + + let ctx = ${ctxName} {}; + let mut handler = ${prefix}Handler { ctx }; + + eprintln!("${prefix} server starting on {}", socket_path); + generated::ipc_server::serve(&socket_path, &mut handler); +} +`; + } + + /** Generate Cargo.toml for a standalone service */ + generateBuildFile(schema: CompiledSchema): string { + const { prefix } = this.opts; + const pkgName = toSnakeCase(prefix).replace(/_/g, '-'); + + return `[package] +name = "${pkgName}-service" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "${pkgName}" +path = "main.rs" + +[dependencies] +rmp-serde = "1" +serde = { version = "1", features = ["derive"] } +`; + } + + /** Generate .gitignore for the skeleton project */ + generateGitignore(): string { + return `# Generated IPC code — do not edit, re-run generate.sh instead +generated/ +target/ +`; + } + + /** Generate a shell script to re-run codegen */ + generateGenerateScript(schemaPath: string): string { + const { prefix } = this.opts; + return `#!/usr/bin/env bash +# Re-generate IPC types, server, and client from schema. +# Run from the project root directory. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" +SCHEMA="${schemaPath}" + +node --experimental-strip-types "$(dirname "$SCRIPT_DIR")/codegen/src/generate.ts" \\ + --schema "$SCHEMA" \\ + --lang rust \\ + --out "$SCRIPT_DIR/generated" \\ + --prefix ${prefix} \\ + --server +`; + } +} diff --git a/ipc-codegen/src/schema_visitor.ts b/ipc-codegen/src/schema_visitor.ts new file mode 100644 index 000000000000..572c0d6330a4 --- /dev/null +++ b/ipc-codegen/src/schema_visitor.ts @@ -0,0 +1,234 @@ +/** + * Schema Visitor - Minimal abstraction over raw msgpack schema + * + * Philosophy: + * - Keep raw schema structure + * - Resolve type references into a graph + * - No normalization - languages handle their own conventions + * - Output is "compiled schema" with resolved types + */ + +export type PrimitiveType = 'bool' | 'u8' | 'u16' | 'u32' | 'u64' | 'f64' | 'string' | 'bytes' | 'fr' | 'field2' | 'enum_u32' | 'map_u32_pair'; + +export interface Type { + kind: 'primitive' | 'vector' | 'array' | 'optional' | 'struct'; + primitive?: PrimitiveType; + element?: Type; // For vector, array, optional + size?: number; // For array + struct?: Struct; // For struct types + originalName?: string; // Original type name from schema (e.g. 'MerkleTreeId', 'unordered_map') +} + +export interface Field { + name: string; + type: Type; +} + +export interface Struct { + name: string; + fields: Field[]; +} + +export interface Command { + name: string; + fields: Field[]; + responseType: string; +} + +export interface CompiledSchema { + // All unique struct types discovered + structs: Map; + + // Command -> Response mappings + commands: Command[]; + + // Response types + responses: Map; + + // Error response type name (e.g. 'WsdbErrorResponse') + errorTypeName?: string; +} + +/** + * SchemaVisitor - Walks raw msgpack schema and resolves references + */ +export class SchemaVisitor { + private structs = new Map(); + private responses = new Map(); + + visit(commandsSchema: any, responsesSchema: any): CompiledSchema { + // Reset state + this.structs.clear(); + this.responses.clear(); + + const commands: Command[] = []; + + // Schema format: ["named_union", [[name, schema], ...]] + const commandPairs = commandsSchema[1] as Array<[string, any]>; + const responsePairs = responsesSchema[1] as Array<[string, any]>; + + // First, visit all response types (including ErrorResponse) + for (const [respName, respSchema] of responsePairs) { + if (typeof respSchema !== 'string') { + const respStruct = this.visitStruct(respName, respSchema); + this.responses.set(respName, respStruct); + } + } + + // Find the error response type name (e.g. 'WsdbErrorResponse') + const errorResponses = responsePairs.filter(([name]: [string, any]) => name.endsWith('ErrorResponse')); + const errorTypeName = errorResponses.length > 0 ? errorResponses[0][0] : undefined; + + // Visit all commands and pair with responses + const normalResponses = responsePairs.filter(([name]: [string, any]) => !name.endsWith('ErrorResponse')); + for (let i = 0; i < commandPairs.length; i++) { + const [cmdName, cmdSchema] = commandPairs[i]; + const [respName] = normalResponses[i]; + + // Discover command structure + const cmdStruct = this.visitStruct(cmdName, cmdSchema); + this.structs.set(cmdName, cmdStruct); + + // Create command mapping + commands.push({ + name: cmdName, + fields: cmdStruct.fields, + responseType: respName, + }); + } + + return { + structs: this.structs, + commands, + responses: this.responses, + errorTypeName, + }; + } + + private visitStruct(name: string, schema: any): Struct { + const fields: Field[] = []; + + // Schema is an object with __typename and fields + for (const [key, value] of Object.entries(schema)) { + if (key === '__typename') continue; + + fields.push({ + name: key, + type: this.visitType(value), + }); + } + + return { name, fields }; + } + + private visitType(schema: any): Type { + // Primitive string type + if (typeof schema === 'string') { + return this.resolvePrimitive(schema); + } + + // Array type descriptor: ['vector', [elementType]] + if (Array.isArray(schema)) { + const [kind, args] = schema; + + switch (kind) { + case 'vector': { + const [elemType] = args as [any]; + // Special case: vector = bytes + if (elemType === 'unsigned char') { + return { kind: 'primitive', primitive: 'bytes' }; + } + return { + kind: 'vector', + element: this.visitType(elemType), + }; + } + + case 'array': { + const [elemType, size] = args as [any, number]; + // Special case: array = field element (Fr/Fq) + if (elemType === 'unsigned char' && size === 32) { + return { kind: 'primitive', primitive: 'fr' }; + } + // Special case: array (other sizes) = bytes + if (elemType === 'unsigned char') { + return { kind: 'primitive', primitive: 'bytes' }; + } + return { + kind: 'array', + element: this.visitType(elemType), + size, + }; + } + + case 'optional': { + const [elemType] = args as [any]; + return { + kind: 'optional', + element: this.visitType(elemType), + }; + } + + case 'shared_ptr': { + // Dereference shared_ptr - just use inner type + const [innerType] = args as [any]; + return this.visitType(innerType); + } + + case 'alias': { + // Alias types (like uint256_t) are treated as bytes + return { kind: 'primitive', primitive: 'bytes' }; + } + + default: + throw new Error(`Unknown type kind: ${kind}`); + } + } + + // Inline struct definition + if (typeof schema === 'object' && schema.__typename) { + const structName = schema.__typename as string; + // Check if already visited + if (!this.structs.has(structName)) { + const struct = this.visitStruct(structName, schema); + this.structs.set(structName, struct); + } + return { + kind: 'struct', + struct: this.structs.get(structName)!, + }; + } + + throw new Error(`Cannot resolve type: ${JSON.stringify(schema)}`); + } + + private resolvePrimitive(name: string): Type { + const primitiveMap: Record = { + 'bool': 'bool', + 'int': 'u32', + 'unsigned int': 'u32', + 'unsigned short': 'u16', + 'unsigned long': 'u64', + 'unsigned long long': 'u64', + 'unsigned char': 'u8', + 'double': 'f64', + 'string': 'string', + 'bin32': 'bytes', + 'field2': 'field2', // Extension field (Fq2) - pair of field elements + 'MerkleTreeId': 'enum_u32', // C++ enum serialized as uint32 + 'unordered_map': 'map_u32_pair', // StateReference: map> + }; + + const primitive = primitiveMap[name]; + if (primitive) { + return { kind: 'primitive', primitive, originalName: name }; + } + + // Unknown primitive - treat as struct reference + // This will be resolved later if it's a real struct + return { + kind: 'struct', + struct: { name, fields: [] }, // Placeholder + }; + } +} diff --git a/ipc-codegen/src/typescript_codegen.ts b/ipc-codegen/src/typescript_codegen.ts new file mode 100644 index 000000000000..22c27c7203a8 --- /dev/null +++ b/ipc-codegen/src/typescript_codegen.ts @@ -0,0 +1,662 @@ +/** + * TypeScript Code Generator - String template based + * + * Philosophy: + * - String templates for file structure + * - Simple type mapping + * - Idiomatic TypeScript conventions + * - No complex abstraction + */ + +import type { CompiledSchema, Type, Struct, Field, Command } from './schema_visitor.ts'; +import { toPascalCase, toSnakeCase } from './naming.ts'; + +function toCamelCase(name: string): string { + // If no underscores, assume already camelCase (e.g. forkId, classId) + if (!name.includes('_')) { + return name.charAt(0).toLowerCase() + name.slice(1); + } + const pascal = toPascalCase(name); + return pascal.charAt(0).toLowerCase() + pascal.slice(1); +} + +export class TypeScriptCodegen { + private errorTypeName: string = 'ErrorResponse'; + /** Prefix to strip from command names when generating method names (e.g. "Bb" -> BbCircuitProve becomes circuitProve) */ + private methodPrefix: string = ''; + + constructor(options?: { stripMethodPrefix?: string }) { + if (options?.stripMethodPrefix) { + this.methodPrefix = options.stripMethodPrefix; + } + } + + /** Strip the method prefix and convert to camelCase for API method names */ + private toMethodName(commandName: string): string { + let name = commandName; + if (this.methodPrefix && name.startsWith(this.methodPrefix)) { + name = name.slice(this.methodPrefix.length); + } + return toCamelCase(name); + } + + // Type mapping: Schema type -> TypeScript type + private mapType(type: Type): string { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return 'boolean'; + case 'u8': return 'number'; + case 'u16': return 'number'; + case 'u32': return 'number'; + case 'u64': return 'number'; + case 'f64': return 'number'; + case 'string': return 'string'; + case 'bytes': return 'Uint8Array'; + case 'fr': return 'Fr'; // 32-byte field element + case 'field2': return '[Fr, Fr]'; // Extension field (Fq2) + case 'enum_u32': return 'number'; // C++ enum as integer + case 'map_u32_pair': return 'Record'; // map> + } + break; + + case 'vector': { + const inner = this.mapType(type.element!); + // Wrap union types in parens to avoid precedence issues: (Foo | undefined)[] + return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + } + + case 'array': { + const inner = this.mapType(type.element!); + return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + } + + case 'optional': + return `${this.mapType(type.element!)} | null`; + + case 'struct': + return toPascalCase(type.struct!.name); + } + + return 'unknown'; + } + + // Type mapping for msgpack interfaces (uses Msgpack* prefix for structs) + private mapMsgpackType(type: Type): string { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return 'boolean'; + case 'u8': return 'number'; + case 'u16': return 'number'; + case 'u32': return 'number'; + case 'u64': return 'number'; + case 'f64': return 'number'; + case 'string': return 'string'; + case 'bytes': return 'Uint8Array'; + case 'fr': return 'Uint8Array'; // Fr on the wire is still 32 bytes + case 'field2': return '[Uint8Array, Uint8Array]'; + case 'enum_u32': return 'number'; + case 'map_u32_pair': return 'Record'; + } + break; + + case 'vector': { + const inner = this.mapMsgpackType(type.element!); + return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + } + + case 'array': { + const inner = this.mapMsgpackType(type.element!); + return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + } + + case 'optional': + return `${this.mapMsgpackType(type.element!)} | null`; + + case 'struct': + return `Msgpack${toPascalCase(type.struct!.name)}`; + } + + return 'unknown'; + } + + // Check if type needs conversion (has nested structs) + private needsConversion(type: Type): boolean { + switch (type.kind) { + case 'primitive': + return false; + case 'vector': + case 'array': + case 'optional': + return this.needsConversion(type.element!); + case 'struct': + return true; + } + return false; + } + + // Generate field + private generateField(field: Field): string { + const tsName = toCamelCase(field.name); + const tsType = this.mapType(field.type); + return ` ${tsName}: ${tsType};`; + } + + // Generate msgpack field (original names, uses Msgpack* types for structs) + private generateMsgpackField(field: Field): string { + const tsType = this.mapMsgpackType(field.type); + return ` ${field.name}: ${tsType};`; + } + + // Generate public interface + private generateInterface(struct: Struct): string { + const tsName = toPascalCase(struct.name); + const fields = struct.fields.map(f => this.generateField(f)).join('\n'); + + return `export interface ${tsName} { +${fields} +}`; + } + + // Generate msgpack interface (internal) + private generateMsgpackInterface(struct: Struct): string { + const tsName = toPascalCase(struct.name); + const fields = struct.fields.map(f => this.generateMsgpackField(f)).join('\n'); + + return `interface Msgpack${tsName} { +${fields} +}`; + } + + // Generate to* conversion function + private generateToFunction(struct: Struct): string { + const tsName = toPascalCase(struct.name); + + if (struct.fields.length === 0) { + return `function to${tsName}(o: Msgpack${tsName}): ${tsName} { + return {}; +}`; + } + + const checks = struct.fields + .map(f => ` if (o.${f.name} === undefined) { throw new Error("Expected ${f.name} in ${tsName} deserialization"); }`) + .join('\n'); + + const conversions = struct.fields + .map(f => { + const tsFieldName = toCamelCase(f.name); + const converter = this.generateToConverter(f.type, `o.${f.name}`); + return ` ${tsFieldName}: ${converter},`; + }) + .join('\n'); + + return `function to${tsName}(o: Msgpack${tsName}): ${tsName} { +${checks}; + return { +${conversions} + }; +}`; + } + + // Generate from* conversion function + private generateFromFunction(struct: Struct): string { + const tsName = toPascalCase(struct.name); + + if (struct.fields.length === 0) { + return `function from${tsName}(o: ${tsName}): Msgpack${tsName} { + return {}; +}`; + } + + const checks = struct.fields + .map(f => { + const tsFieldName = toCamelCase(f.name); + return ` if (o.${tsFieldName} === undefined) { throw new Error("Expected ${tsFieldName} in ${tsName} serialization"); }`; + }) + .join('\n'); + + const conversions = struct.fields + .map(f => { + const tsFieldName = toCamelCase(f.name); + const converter = this.generateFromConverter(f.type, `o.${tsFieldName}`); + return ` ${f.name}: ${converter},`; + }) + .join('\n'); + + return `function from${tsName}(o: ${tsName}): Msgpack${tsName} { +${checks}; + return { +${conversions} + }; +}`; + } + + // Generate converter for to* function + private generateToConverter(type: Type, value: string): string { + if (!this.needsConversion(type)) { + return value; + } + + switch (type.kind) { + case 'vector': + case 'array': + if (this.needsConversion(type.element!)) { + return `${value}.map((v: any) => ${this.generateToConverter(type.element!, 'v')})`; + } + return value; + case 'optional': + if (this.needsConversion(type.element!)) { + return `${value} != null ? ${this.generateToConverter(type.element!, value)} : null`; + } + return value; + case 'struct': + return `to${toPascalCase(type.struct!.name)}(${value})`; + } + return value; + } + + // Generate converter for from* function + private generateFromConverter(type: Type, value: string): string { + if (!this.needsConversion(type)) { + return value; + } + + switch (type.kind) { + case 'vector': + case 'array': + if (this.needsConversion(type.element!)) { + return `${value}.map((v: any) => ${this.generateFromConverter(type.element!, 'v')})`; + } + return value; + case 'optional': + if (this.needsConversion(type.element!)) { + return `${value} != null ? ${this.generateFromConverter(type.element!, value)} : null`; + } + return value; + case 'struct': + return `from${toPascalCase(type.struct!.name)}(${value})`; + } + return value; + } + + // Generate types file (api_types.ts) + generateTypes(schema: CompiledSchema, schemaHash?: string): string { + const allStructs = [...schema.structs.values(), ...schema.responses.values()]; + + // Public interfaces + const publicInterfaces = allStructs + .map(s => this.generateInterface(s)) + .join('\n\n'); + + // Msgpack interfaces + const msgpackInterfaces = allStructs + .map(s => this.generateMsgpackInterface(s)) + .join('\n\n'); + + // Conversion functions + const toFunctions = allStructs + .map(s => 'export ' + this.generateToFunction(s)) + .join('\n\n'); + + const fromFunctions = allStructs + .map(s => 'export ' + this.generateFromFunction(s)) + .join('\n\n'); + + // BbApiBase interface + const apiMethods = schema.commands + .map(c => ` ${this.toMethodName(c.name)}(command: ${toPascalCase(c.name)}): Promise<${toPascalCase(c.responseType)}>;`) + .join('\n'); + + const hashLine = schemaHash ? `\n/** Schema version hash for compatibility checking */\nexport const SCHEMA_HASH = '${schemaHash}';\n` : ''; + + return `// AUTOGENERATED FILE - DO NOT EDIT +${hashLine} +// Type aliases for primitive types +/** 32-byte field element (Fr/Fq). Branded Uint8Array — no arithmetic, just type safety. */ +export type Fr = Uint8Array; +export type Field2 = [Fr, Fr]; + +// Public interfaces (exported) +${publicInterfaces} + +// Private Msgpack interfaces (not exported) +${msgpackInterfaces} + +// Conversion functions (exported) +${toFunctions} + +${fromFunctions} + +// Base API interface +export interface BbApiBase { +${apiMethods} + destroy(): Promise; +} +`; + } + + // Generate API method + private generateAsyncApiMethod(command: Command): string { + const methodName = this.toMethodName(command.name); + const cmdType = toPascalCase(command.name); + const respType = toPascalCase(command.responseType); + + return ` ${methodName}(command: ${cmdType}): Promise<${respType}> { + const msgpackCommand = from${cmdType}(command); + return msgpackCall(this.backend, [["${command.name}", msgpackCommand]]).then(([variantName, result]: [string, any]) => { + if (variantName === '${this.errorTypeName}') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== '${command.responseType}') { + throw new BBApiException(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); + } + return to${respType}(result); + }); + }`; + } + + private generateSyncApiMethod(command: Command): string { + const methodName = this.toMethodName(command.name); + const cmdType = toPascalCase(command.name); + const respType = toPascalCase(command.responseType); + + return ` ${methodName}(command: ${cmdType}): ${respType} { + const msgpackCommand = from${cmdType}(command); + const [variantName, result] = msgpackCall(this.backend, [["${command.name}", msgpackCommand]]); + if (variantName === '${this.errorTypeName}') { + throw new BBApiException(result.message || 'Unknown error from barretenberg'); + } + if (variantName !== '${command.responseType}') { + throw new BBApiException(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); + } + return to${respType}(result); + }`; + } + + // Generate async API file + generateAsyncApi(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const imports = this.generateApiImports(schema); + const methods = schema.commands + .map(c => this.generateAsyncApiMethod(c)) + .join('\n\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT + +import { IMsgpackBackendAsync } from '../../bb_backends/interface.js'; +import { Decoder, Encoder } from 'msgpackr'; +import { BBApiException } from '../../bbapi_exception.js'; +${imports} + +async function msgpackCall(backend: IMsgpackBackendAsync, input: any[]) { + const inputBuffer = new Encoder({ useRecords: false }).pack(input); + const encodedResult = await backend.call(inputBuffer); + return new Decoder({ useRecords: false }).unpack(encodedResult); +} + +export class AsyncApi implements BbApiBase { + constructor(protected backend: IMsgpackBackendAsync) {} + +${methods} + + destroy(): Promise { + return this.backend.destroy ? this.backend.destroy() : Promise.resolve(); + } +} +`; + } + + // Generate sync API file + generateSyncApi(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const imports = this.generateApiImports(schema); + const methods = schema.commands + .map(c => this.generateSyncApiMethod(c)) + .join('\n\n'); + + return `// AUTOGENERATED FILE - DO NOT EDIT + +import { IMsgpackBackendSync } from '../../bb_backends/interface.js'; +import { Decoder, Encoder } from 'msgpackr'; +import { BBApiException } from '../../bbapi_exception.js'; +${imports} + +function msgpackCall(backend: IMsgpackBackendSync, input: any[]) { + const inputBuffer = new Encoder({ useRecords: false }).pack(input); + const encodedResult = backend.call(inputBuffer); + return new Decoder({ useRecords: false }).unpack(encodedResult); +} + +export class SyncApi { + constructor(protected backend: IMsgpackBackendSync) {} + +${methods} + + destroy(): void { + if (this.backend.destroy) this.backend.destroy(); + } +} +`; + } + + // Generate import statement for API files + private generateApiImports(schema: CompiledSchema): string { + const types = new Set(); + + // Add command types and their conversion functions + for (const cmd of schema.commands) { + const cmdType = toPascalCase(cmd.name); + const respType = toPascalCase(cmd.responseType); + types.add(cmdType); + types.add(respType); + types.add(`from${cmdType}`); + types.add(`to${respType}`); + } + + // Add BbApiBase + types.add('BbApiBase'); + + const sortedTypes = Array.from(types).sort(); + return `import { ${sortedTypes.join(', ')} } from './api_types.js';`; + } + + // ----------------------------------------------------------------------- + // Server-side code generation + // ----------------------------------------------------------------------- + + /** Generate a server handler interface and dispatch function */ + generateServerApi(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const errorType = toPascalCase(this.errorTypeName); + + // Generate handler interface + const handlerMethods = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.toMethodName(c.name); + const cmdType = toPascalCase(c.name); + const respType = toPascalCase(c.responseType); + return ` ${methodName}(command: ${cmdType}): Promise<${respType}>;`; + }) + .join('\n'); + + // Generate dispatch switch cases + const dispatchCases = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.toMethodName(c.name); + const cmdType = toPascalCase(c.name); + const respType = toPascalCase(c.responseType); + return ` case '${c.name}': { + const cmd = to${cmdType}(payload); + const result = await handler.${methodName}(cmd); + return ['${c.responseType}', from${respType}(result)]; + }`; + }) + .join('\n'); + + // Collect imports + const importTypes = new Set(); + for (const cmd of schema.commands) { + if (cmd.name.endsWith('Shutdown')) continue; + const cmdType = toPascalCase(cmd.name); + const respType = toPascalCase(cmd.responseType); + importTypes.add(cmdType); + importTypes.add(respType); + importTypes.add(`to${cmdType}`); + importTypes.add(`from${respType}`); + } + const sortedImports = Array.from(importTypes).sort(); + + return `// AUTOGENERATED FILE - DO NOT EDIT +// Server-side dispatch for IPC protocol + +import { ${sortedImports.join(', ')} } from './api_types.js'; + +/** Handler interface — implement this to serve commands. */ +export interface Handler { +${handlerMethods} +} + +/** + * Dispatch a [commandName, payload] pair to the handler. + * Returns [responseName, responsePayload] for serialization. + */ +export async function dispatch( + handler: Handler, + commandName: string, + payload: any, +): Promise<[string, any]> { + switch (commandName) { +${dispatchCases} + default: + throw new Error(\`Unknown command: \${commandName}\`); + } +} +`; + } + + // ----------------------------------------------------------------------- + // Skeleton generation (one-time handler stubs + main + build files) + // ----------------------------------------------------------------------- + + /** Generate handler stub implementations that throw "not implemented" */ + generateHandlerStubs(schema: CompiledSchema, prefix: string): string { + const serverModule = `${toSnakeCase(prefix)}_server`; + + // Collect import types + const importTypes = new Set(); + for (const cmd of schema.commands) { + if (cmd.name.endsWith('Shutdown')) continue; + importTypes.add(toPascalCase(cmd.name)); + importTypes.add(toPascalCase(cmd.responseType)); + } + importTypes.add('Handler'); + const sortedImports = Array.from(importTypes).sort(); + + const stubs = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.toMethodName(c.name); + const cmdType = toPascalCase(c.name); + const respType = toPascalCase(c.responseType); + return ` async ${methodName}(command: ${cmdType}): Promise<${respType}> { + throw new Error('not implemented: ${c.name}'); + }`; + }).join('\n\n'); + + return `// Handler stubs — implement your service logic here. +// This file is generated ONCE. Edit freely — it will not be overwritten. + +import { ${sortedImports.join(', ')} } from './generated/${serverModule}.js'; + +/** Shared context for your service — add database connections, state, etc. */ +export interface ${prefix}Context { + // Add your shared state here +} + +/** Handler implementation */ +export class ${prefix}Handler implements Handler { + constructor(public ctx: ${prefix}Context) {} + +${stubs} +} +`; + } + + /** Generate a main.ts entry point for a standalone service */ + generateMain(schema: CompiledSchema, prefix: string): string { + const serverModule = `${toSnakeCase(prefix)}_server`; + + return `// Entry point for ${prefix} service. +// This file is generated ONCE. Edit freely — it will not be overwritten. + +import { serve } from './generated/ipc_server.js'; +import { dispatch } from './generated/${serverModule}.js'; +import { ${prefix}Handler } from './${toSnakeCase(prefix)}_handlers.js'; + +const socketPath = process.argv[2]; +if (!socketPath) { + console.error('Usage: ${toSnakeCase(prefix)} '); + process.exit(1); +} + +const ctx = {}; +const handler = new ${prefix}Handler(ctx); + +console.error(\`${prefix} server starting on \${socketPath}\`); +serve(socketPath, (commandName: string, payload: any) => dispatch(handler, commandName, payload)); +`; + } + + /** Generate package.json for a standalone service */ + generateBuildFile(prefix: string): string { + const pkgName = toSnakeCase(prefix).replace(/_/g, '-'); + + return JSON.stringify({ + name: `${pkgName}-service`, + version: '0.1.0', + type: 'module', + scripts: { + build: 'tsc', + start: 'node --experimental-strip-types main.ts', + generate: 'bash generate.sh', + }, + dependencies: { + msgpackr: '^1.10.0', + }, + devDependencies: { + typescript: '^5.4.0', + }, + }, null, 2) + '\n'; + } + + /** Generate .gitignore for the skeleton project */ + generateGitignore(): string { + return `# Generated IPC code — do not edit, re-run generate.sh instead +generated/ +node_modules/ +dist/ +`; + } + + /** Generate a shell script to re-run codegen */ + generateGenerateScript(schemaPath: string, prefix: string): string { + return `#!/usr/bin/env bash +# Re-generate IPC types, server, and client from schema. +# Run from the project root directory. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" +SCHEMA="${schemaPath}" + +node --experimental-strip-types "$(dirname "$SCRIPT_DIR")/codegen/src/generate.ts" \\ + --schema "$SCHEMA" \\ + --lang ts \\ + --out "$SCRIPT_DIR/generated" \\ + --prefix ${prefix} \\ + --server +`; + } +} diff --git a/ipc-codegen/src/zig_codegen.ts b/ipc-codegen/src/zig_codegen.ts new file mode 100644 index 000000000000..459237cc340f --- /dev/null +++ b/ipc-codegen/src/zig_codegen.ts @@ -0,0 +1,651 @@ +/** + * Zig Code Generator + * + * Generates Zig structs, serialization/deserialization functions, and IPC client + * from a CompiledSchema. Uses zig-msgpack's Payload API for wire encoding. + * + * Since Zig has no reflection-based serde, all serialization code is generated + * explicitly per struct. + */ + +import type { CompiledSchema, Type, Struct, Field, Command } from './schema_visitor.ts'; +import { toSnakeCase, toPascalCase } from './naming.ts'; + +export interface ZigCodegenOptions { + /** Service prefix to strip from method names (e.g., 'Wsdb') */ + prefix?: string; + /** Client struct name (e.g., 'WsdbClient') */ + clientName?: string; +} + +export class ZigCodegen { + private errorTypeName: string = 'ErrorResponse'; + private opts: Required; + + constructor(options?: ZigCodegenOptions) { + this.opts = { + prefix: options?.prefix ?? '', + clientName: options?.clientName ?? 'Client', + }; + } + + /** Map schema type to Zig type */ + private mapType(type: Type): string { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return 'bool'; + case 'u8': return 'u8'; + case 'u16': return 'u16'; + case 'u32': return 'u32'; + case 'u64': return 'u64'; + case 'f64': return 'f64'; + case 'string': return '[]const u8'; + case 'bytes': return '[]const u8'; + case 'fr': return 'Fr'; // [32]u8 + case 'field2': return '[2]Fr'; + case 'enum_u32': return 'u32'; + case 'map_u32_pair': return 'void'; // TODO: proper map support + } + break; + case 'vector': + return `[]const ${this.mapType(type.element!)}`; + case 'array': + return `[${type.size}]${this.mapType(type.element!)}`; + case 'optional': + return `?${this.mapType(type.element!)}`; + case 'struct': + return toPascalCase(type.struct!.name); + } + return 'void'; + } + + /** Generate a Zig field-to-payload conversion expression */ + private fieldToPayload(fieldExpr: string, type: import('./schema_visitor.ts').Type): string { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return `Payload{ .bool = ${fieldExpr} }`; + case 'u8': case 'u16': case 'u32': case 'u64': + return `Payload{ .uint = @intCast(${fieldExpr}) }`; + case 'f64': return `Payload{ .float = ${fieldExpr} }`; + case 'string': return `try Payload.strToPayload(${fieldExpr}, allocator)`; + case 'bytes': return `try Payload.binToPayload(${fieldExpr}, allocator)`; + case 'fr': return `try Payload.binToPayload(&${fieldExpr}, allocator)`; + case 'enum_u32': return `Payload{ .uint = @intCast(${fieldExpr}) }`; + default: return `Payload{ .nil = {} }`; + } + case 'optional': + return `if (${fieldExpr}) |v| ${this.fieldToPayload('v', type.element!)} else Payload{ .nil = {} }`; + case 'vector': { + // For vectors, build an array payload + return `blk: { + var arr = try Payload.arrPayload(${fieldExpr}.len, allocator); + for (${fieldExpr}, 0..) |item, i| { + try arr.setArrElement(i, ${this.fieldToPayload('item', type.element!)}); + } + break :blk arr; + }`; + } + case 'struct': + return `try ${fieldExpr}.toPayload(allocator)`; + default: return `Payload{ .nil = {} }`; + } + } + + /** Generate a Zig payload-to-field conversion expression */ + private fieldFromPayload(payloadExpr: string, type: import('./schema_visitor.ts').Type): string { + switch (type.kind) { + case 'primitive': + switch (type.primitive) { + case 'bool': return `try ${payloadExpr}.asBool()`; + case 'u8': return `@intCast(try ${payloadExpr}.asUint())`; + case 'u16': return `@intCast(try ${payloadExpr}.asUint())`; + case 'u32': return `@intCast(try ${payloadExpr}.asUint())`; + case 'u64': return `try ${payloadExpr}.asUint()`; + case 'f64': return `try ${payloadExpr}.asFloat()`; + case 'string': return `try ${payloadExpr}.asStr()`; + case 'bytes': return `${payloadExpr}.bin.value()`; + case 'fr': return `${payloadExpr}.bin.value()[0..32].*`; + case 'enum_u32': return `@intCast(try ${payloadExpr}.asUint())`; + default: return `undefined`; + } + case 'vector': { + const elemConv = this.fieldFromPayload('elem', type.element!); + return `blk: { + const arr_len = try ${payloadExpr}.getArrLen(); + var result = try std.heap.page_allocator.alloc(${this.mapType(type.element!)}, arr_len); + for (0..arr_len) |i| { + const elem = try ${payloadExpr}.getArrElement(i); + result[i] = ${elemConv}; + } + break :blk result; + }`; + } + case 'optional': + return `if (${payloadExpr} == .nil) null else ${this.fieldFromPayload(payloadExpr, type.element!)}`; + case 'struct': + return `try ${toPascalCase(type.struct!.name)}.fromPayload(${payloadExpr})`; + default: return `undefined`; + } + } + + /** Generate a Zig struct definition with toPayload/fromPayload methods */ + private generateStruct(struct: Struct): string { + const zigName = toPascalCase(struct.name); + const fields = struct.fields.map(f => { + const zigFieldName = toSnakeCase(f.name); + const zigType = this.mapType(f.type); + return ` ${zigFieldName}: ${zigType},`; + }).join('\n'); + + // Treat structs with only void fields as empty (void comes from unmapped types) + const hasFields = struct.fields.length > 0 && struct.fields.some(f => this.mapType(f.type) !== 'void'); + + // toPayload method + const toPayloadFields = struct.fields.map(f => { + const zigFieldName = toSnakeCase(f.name); + return ` try map.mapPut("${f.name}", ${this.fieldToPayload(`self.${zigFieldName}`, f.type)});`; + }).join('\n'); + + // fromPayload method + const fromPayloadFields = struct.fields.map(f => { + const zigFieldName = toSnakeCase(f.name); + return ` .${zigFieldName} = ${this.fieldFromPayload(`(try payload.mapGet("${f.name}")).?`, f.type)},`; + }).join('\n'); + + // Empty structs: suppress unused parameter warnings + if (!hasFields) { + return `/// ${struct.name} +pub const ${zigName} = struct { + + pub fn toPayload(_: ${zigName}, allocator: std.mem.Allocator) !Payload { + return Payload.mapPayload(allocator); + } + + pub fn fromPayload(_: Payload) !${zigName} { + return ${zigName}{}; + } +};`; + } + + return `/// ${struct.name} +pub const ${zigName} = struct { +${fields} + + pub fn toPayload(self: ${zigName}, allocator: std.mem.Allocator) !Payload { + var map = Payload.mapPayload(allocator); +${toPayloadFields} + return map; + } + + pub fn fromPayload(payload: Payload) !${zigName} { + return ${zigName}{ +${fromPayloadFields} + }; + } +};`; + } + + /** Generate serialize function for a struct */ + private generateSerializeFn(struct: Struct): string { + const zigName = toPascalCase(struct.name); + const fieldCount = struct.fields.length; + + const fieldPacks = struct.fields.map(f => { + const zigFieldName = toSnakeCase(f.name); + return ` try packField(packer, "${f.name}", self.${zigFieldName});`; + }).join('\n'); + + return `pub fn serialize${zigName}(self: ${zigName}, packer: anytype) !void { + try packer.writeMapHeader(${fieldCount}); +${fieldPacks} +}`; + } + + /** Generate deserialize function for a struct */ + private generateDeserializeFn(struct: Struct): string { + const zigName = toPascalCase(struct.name); + + const fieldReads = struct.fields.map(f => { + const zigFieldName = toSnakeCase(f.name); + const zigType = this.mapType(f.type); + return ` .${zigFieldName} = try readField(${zigType}, unpacker, "${f.name}"),`; + }).join('\n'); + + return `pub fn deserialize${zigName}(unpacker: anytype, allocator: std.mem.Allocator) !${zigName} { + _ = allocator; + const map_len = try unpacker.readMapHeader(); + _ = map_len; + return ${zigName}{ +${fieldReads} + }; +}`; + } + + /** Generate the Command tagged union */ + private generateCommandUnion(schema: CompiledSchema): string { + const variants = schema.commands.map(c => { + const zigName = toPascalCase(c.name); + return ` ${toSnakeCase(c.name)}: ${zigName},`; + }).join('\n'); + + const nameMap = schema.commands.map(c => { + return ` .${toSnakeCase(c.name)} => "${c.name}",`; + }).join('\n'); + + return `/// Tagged union of all commands +pub const Command = union(enum) { +${variants} + + pub fn schemaName(self: Command) []const u8 { + return switch (self) { +${nameMap} + }; + } +};`; + } + + /** Generate the Response tagged union */ + private generateResponseUnion(schema: CompiledSchema): string { + const commandResponseTypes = Array.from(new Set(schema.commands.map(c => c.responseType))); + const errorName = schema.errorTypeName || 'ErrorResponse'; + const responseTypes = schema.responses.has(errorName) + ? [...commandResponseTypes, errorName] + : commandResponseTypes; + + const variants = responseTypes.map(name => { + const zigName = toPascalCase(name); + return ` ${toSnakeCase(name)}: ${zigName},`; + }).join('\n'); + + return `/// Tagged union of all responses +pub const Response = union(enum) { +${variants} +};`; + } + + /** Generate the types file */ + generateTypes(schema: CompiledSchema, schemaHash?: string): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + + const allStructs = [...schema.structs.values(), ...schema.responses.values()]; + + const structDefs = allStructs.map(s => this.generateStruct(s)).join('\n\n'); + + const hashLine = schemaHash + ? `\n/// Schema version hash for compatibility checking\npub const SCHEMA_HASH = "${schemaHash}";\n` + : ''; + + return `//! AUTOGENERATED - DO NOT EDIT +//! Generated from Aztec IPC msgpack schema +//! +//! Each struct has toPayload() and fromPayload() methods that convert +//! to/from zig-msgpack Payload objects for serialization. + +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const PackerIO = msgpack.PackerIO; +${hashLine} +/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. +pub const Fr = [32]u8; + +// --------------------------------------------------------------------------- +// Type definitions +// --------------------------------------------------------------------------- + +${structDefs} + +// --------------------------------------------------------------------------- +// Command / Response unions +// --------------------------------------------------------------------------- + +${this.generateCommandUnion(schema)} + +${this.generateResponseUnion(schema)} +`; + } + + /** Strip service prefix from command name for method naming */ + private methodName(commandName: string): string { + const withoutPrefix = this.opts.prefix && commandName.startsWith(this.opts.prefix) + ? commandName.slice(this.opts.prefix.length) + : commandName; + return toSnakeCase(withoutPrefix); + } + + /** Generate the client wrapper — typed methods parameterized on backend type */ + generateClient(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const { prefix } = this.opts; + const errorRespName = toPascalCase(this.errorTypeName); + const typesFile = `${toSnakeCase(prefix)}_types.zig`; + + const methods = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const zigCmdName = toPascalCase(c.name); + const zigRespName = toPascalCase(c.responseType); + return ` pub fn ${methodName}(self: *Self, cmd: types.${zigCmdName}) !types.${zigRespName} { + const request_bytes = try Self.encode("${c.name}", try cmd.toPayload(alloc)); + defer alloc.free(request_bytes); + const response_bytes = try self.backend.call(request_bytes); + defer alloc.free(response_bytes); + const resp_name, const resp_payload = try Self.decode(response_bytes); + if (std.mem.eql(u8, resp_name, "${this.errorTypeName}")) return error.ServerError; + return try types.${zigRespName}.fromPayload(resp_payload); + }`; + }).join('\n\n'); + + return `//! AUTOGENERATED - DO NOT EDIT +//! ${prefix} client — typed methods parameterized on a backend type. +//! +//! The backend must satisfy: call(self, request: []const u8) ![]u8 and destroy(self) void. +//! See backend.zig for the interface contract. +//! Implementations: UdsBackend (uds_backend.zig), FfiBackend (ffi_backend.zig). + +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const types = @import("${typesFile}"); +const backend_mod = @import("backend.zig"); + +const alloc = std.heap.page_allocator; + +pub fn Client(comptime BackendType: type) type { + comptime backend_mod.assertBackend(BackendType); + + return struct { + const Self = @This(); + backend: *BackendType, + + pub fn init(backend: *BackendType) Self { + return .{ .backend = backend }; + } + + pub fn destroy(self: *Self) void { + self.backend.destroy(); + } + + pub fn shutdown(self: *Self) !void { + const request_bytes = try Self.encode("${prefix}Shutdown", Payload.mapPayload(alloc)); + defer alloc.free(request_bytes); + const response_bytes = try self.backend.call(request_bytes); + alloc.free(response_bytes); + } + +${methods} + + // --- internal helpers --- + + fn encode(cmd_name: []const u8, cmd_fields: Payload) ![]u8 { + var inner = try Payload.arrPayload(2, alloc); + try inner.setArrElement(0, try Payload.strToPayload(cmd_name, alloc)); + try inner.setArrElement(1, cmd_fields); + var outer = try Payload.arrPayload(1, alloc); + try outer.setArrElement(0, inner); + + var allocating_writer = std.Io.Writer.Allocating.init(alloc); + var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); + try packer.write(outer); + return try allocating_writer.toOwnedSlice(); + } + + fn decode(response_bytes: []const u8) !struct { []const u8, Payload } { + var reader = std.Io.Reader.fixed(response_bytes); + var unpacker = msgpack.PackerIO.init(&reader, undefined); + const resp = try unpacker.read(alloc); + const resp_len = try resp.getArrLen(); + if (resp_len != 2) return error.InvalidResponse; + const name = try (try resp.getArrElement(0)).asStr(); + const payload = try resp.getArrElement(1); + return .{ name, payload }; + } + }; +} +`; + } + + /** Generate the server wrapper — dispatch + stub handlers over generic IPC server */ + generateServer(schema: CompiledSchema): string { + this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + const { prefix } = this.opts; + const errorRespName = toPascalCase(this.errorTypeName); + const typesFile = `${toSnakeCase(prefix)}_types.zig`; + + // Dispatch cases: match command name → deserialize → call handler → serialize response + const dispatchCases = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const zigCmdName = toPascalCase(c.name); + const zigRespName = toPascalCase(c.responseType); + return ` if (std.mem.eql(u8, cmd_name, "${c.name}")) { + const cmd = types.${zigCmdName}.fromPayload(cmd_fields) catch return makeError("deser failed"); + const resp = ${methodName}(cmd) catch return makeError("not implemented: ${c.name}"); + return .{ .resp_name = "${c.responseType}", .resp_payload = resp.toPayload(alloc) }; + }`; + }).join('\n'); + + // Stub handler functions + const stubs = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const zigCmdName = toPascalCase(c.name); + const zigRespName = toPascalCase(c.responseType); + return `/// TODO: implement ${c.name} +fn ${methodName}(cmd: types.${zigCmdName}) !types.${zigRespName} { + _ = cmd; + return error.NotImplemented; +}`; + }).join('\n\n'); + + return `//! AUTOGENERATED - DO NOT EDIT +//! ${prefix} IPC server — dispatch + stub handlers over generic IPC transport. +//! +//! Implement the handler functions below to build a working ${prefix} service. +//! Then call: serve("socket_path") + +const std = @import("std"); +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; +const types = @import("${typesFile}"); +const ipc_server = @import("ipc_server.zig"); + +const alloc = std.heap.page_allocator; + +/// Start the ${prefix} server on the given socket path. +pub fn serve(socket_path: []const u8) !void { + try ipc_server.serve(socket_path, dispatch); +} + +fn dispatch(cmd_name: []const u8, cmd_fields: Payload) ipc_server.DispatchResult { + // Shutdown + if (std.mem.eql(u8, cmd_name, "${prefix}Shutdown")) { + return .{ .resp_name = "${prefix}ShutdownResponse", .resp_payload = Payload.mapPayload(alloc) }; + } + + // Command dispatch +${dispatchCases} + + return makeError("unknown command"); +} + +fn makeError(message: []const u8) ipc_server.DispatchResult { + var err_map = Payload.mapPayload(alloc); + err_map.mapPut("message", Payload.strToPayload(message, alloc) catch return .{ .resp_name = "${errorRespName}", .resp_payload = Payload.mapPayload(alloc) }) catch {}; + return .{ .resp_name = "${errorRespName}", .resp_payload = err_map }; +} + +// --------------------------------------------------------------------------- +// Handler stubs — implement these to build your ${prefix} service. +// --------------------------------------------------------------------------- + +${stubs} +`; + } + + // ----------------------------------------------------------------------- + // Skeleton generation (one-time handler stubs + main + build files) + // ----------------------------------------------------------------------- + + /** Generate handler stub implementations that return error.NotImplemented */ + generateHandlerStubs(schema: CompiledSchema): string { + const { prefix } = this.opts; + const typesFile = `${toSnakeCase(prefix)}_types.zig`; + const serverFile = `${toSnakeCase(prefix)}_server.zig`; + const ctxName = `${prefix}Context`; + + const stubs = schema.commands + .filter(c => !c.name.endsWith('Shutdown')) + .map(c => { + const methodName = this.methodName(c.name); + const zigCmdName = toPascalCase(c.name); + const zigRespName = toPascalCase(c.responseType); + return `pub fn ${methodName}(ctx: *${ctxName}, cmd: types.${zigCmdName}) !types.${zigRespName} { + _ = ctx; + _ = cmd; + return error.NotImplemented; +}`; + }).join('\n\n'); + + return `// Handler stubs — implement your service logic here. +// This file is generated ONCE. Edit freely — it will not be overwritten. + +const std = @import("std"); +const types = @import("generated/${typesFile}"); + +/// Shared context for your service — add database connections, state, etc. +pub const ${ctxName} = struct { + // Add your shared state here +}; + +// --------------------------------------------------------------------------- +// Handler implementations — fill these in with your service logic. +// --------------------------------------------------------------------------- + +${stubs} +`; + } + + /** Generate a main.zig entry point for a standalone service */ + generateMain(schema: CompiledSchema): string { + const { prefix } = this.opts; + const serverFile = `${toSnakeCase(prefix)}_server`; + const handlersFile = `${toSnakeCase(prefix)}_handlers`; + + return `// Entry point for ${prefix} service. +// This file is generated ONCE. Edit freely — it will not be overwritten. + +const std = @import("std"); +const server = @import("generated/${serverFile}.zig"); + +pub fn main() !void { + const args = try std.process.argsAlloc(std.heap.page_allocator); + defer std.process.argsFree(std.heap.page_allocator, args); + + if (args.len < 2) { + std.debug.print("Usage: ${toSnakeCase(prefix)} \\n", .{}); + std.process.exit(1); + } + + const socket_path = args[1]; + std.debug.print("${prefix} server starting on {s}\\n", .{socket_path}); + try server.serve(socket_path); +} +`; + } + + /** Generate build.zig for a standalone service */ + generateBuildFile(schema: CompiledSchema): string { + const { prefix } = this.opts; + const binName = toSnakeCase(prefix); + + return `const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const msgpack_dep = b.dependency("zig-msgpack", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "${binName}", + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("msgpack", msgpack_dep.module("msgpack")); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the ${prefix} service"); + run_step.dependOn(&run_cmd.step); +} +`; + } + + /** Generate build.zig.zon for dependency management */ + generateBuildZon(schema: CompiledSchema): string { + const { prefix } = this.opts; + const binName = toSnakeCase(prefix); + + return `.{ + .name = "${binName}-service", + .version = "0.1.0", + .dependencies = .{ + .@"zig-msgpack" = .{ + .url = "https://github.com/zig-msgpack/zig-msgpack/archive/refs/heads/main.tar.gz", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "main.zig", + "generated", + }, +} +`; + } + + /** Generate .gitignore for the skeleton project */ + generateGitignore(): string { + return `# Generated IPC code — do not edit, re-run generate.sh instead +generated/ +zig-out/ +zig-cache/ +.zig-cache/ +`; + } + + /** Generate a shell script to re-run codegen */ + generateGenerateScript(schemaPath: string): string { + const { prefix } = this.opts; + return `#!/usr/bin/env bash +# Re-generate IPC types, server, and client from schema. +# Run from the project root directory. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" +SCHEMA="${schemaPath}" + +node --experimental-strip-types "$(dirname "$SCRIPT_DIR")/codegen/src/generate.ts" \\ + --schema "$SCHEMA" \\ + --lang zig \\ + --out "$SCRIPT_DIR/generated" \\ + --prefix ${prefix} \\ + --server +`; + } +} diff --git a/ipc-codegen/templates/cpp/ipc_client.hpp b/ipc-codegen/templates/cpp/ipc_client.hpp new file mode 100644 index 000000000000..4e3db6c204c6 --- /dev/null +++ b/ipc-codegen/templates/cpp/ipc_client.hpp @@ -0,0 +1,112 @@ +/** + * Generic IPC client over Unix Domain Sockets. + * Handles: socket connect, length-prefixed framing, send/receive raw bytes. + * Header-only. + */ +#pragma once +#ifndef IPC_CLIENT_HPP_INCLUDED +#define IPC_CLIENT_HPP_INCLUDED + +#ifndef THROW +#define THROW throw +#endif +#ifndef RETHROW +#define RETHROW throw +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace ipc { + +class IpcClient { + public: + explicit IpcClient(const char* socket_path) + { + fd_ = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (fd_ < 0) { + throw std::runtime_error(std::string("socket() failed: ") + strerror(errno)); + } + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + if (::connect(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(fd_); + fd_ = -1; + throw std::runtime_error(std::string("connect() failed: ") + strerror(errno)); + } + } + + ~IpcClient() + { + if (fd_ >= 0) { + ::close(fd_); + } + } + + IpcClient(const IpcClient&) = delete; + IpcClient& operator=(const IpcClient&) = delete; + + std::vector call(const std::vector& request) + { + // Send length-prefixed request + uint32_t len = static_cast(request.size()); + uint8_t header[4] = { + static_cast(len & 0xFF), + static_cast((len >> 8) & 0xFF), + static_cast((len >> 16) & 0xFF), + static_cast((len >> 24) & 0xFF), + }; + write_all(header, 4); + write_all(request.data(), request.size()); + + // Receive length-prefixed response + uint8_t resp_hdr[4]; + read_all(resp_hdr, 4); + uint32_t resp_len = static_cast(resp_hdr[0]) | (static_cast(resp_hdr[1]) << 8) | + (static_cast(resp_hdr[2]) << 16) | (static_cast(resp_hdr[3]) << 24); + std::vector resp(resp_len); + read_all(resp.data(), resp_len); + return resp; + } + + private: + void write_all(const void* data, size_t len) + { + const auto* ptr = static_cast(data); + size_t written = 0; + while (written < len) { + auto n = ::write(fd_, ptr + written, len - written); + if (n <= 0) { + throw std::runtime_error("write failed"); + } + written += static_cast(n); + } + } + + void read_all(void* data, size_t len) + { + auto* ptr = static_cast(data); + size_t got = 0; + while (got < len) { + auto n = ::read(fd_, ptr + got, len - got); + if (n <= 0) { + throw std::runtime_error("read failed"); + } + got += static_cast(n); + } + } + + int fd_ = -1; +}; + +} // namespace ipc +#endif // IPC_CLIENT_HPP_INCLUDED diff --git a/ipc-codegen/templates/cpp/ipc_server.hpp b/ipc-codegen/templates/cpp/ipc_server.hpp new file mode 100644 index 000000000000..d57ad50e7b64 --- /dev/null +++ b/ipc-codegen/templates/cpp/ipc_server.hpp @@ -0,0 +1,252 @@ +/** + * Generic IPC server over Unix Domain Sockets. + * Handles: socket setup, multi-client accept via poll(), length-prefixed framing. + * Service-specific dispatch is injected via the handler function parameter. + * + * Does NOT handle signal handling or parent death monitoring — those are + * the responsibility of the binary that calls serve(). + * + * Header-only — no separate .cpp needed. + */ +#pragma once +#ifndef IPC_SERVER_HPP_INCLUDED +#define IPC_SERVER_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include + +#if defined(__wasm__) +// UDS not available in WASM — provide stub types only +namespace ipc { +struct ShutdownRequested : std::exception { + std::vector final_response; + explicit ShutdownRequested(std::vector resp) : final_response(std::move(resp)) {} + const char* what() const noexcept override { return "shutdown requested"; } +}; +using Handler = std::function(const std::vector&)>; +inline void serve(const char*, Handler, std::atomic* = nullptr, int = 5) {} +} // namespace ipc +#else + +#ifndef THROW +#define THROW throw +#endif +#ifndef RETHROW +#define RETHROW throw +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ipc { + +/// Exception thrown by handlers to trigger graceful shutdown. +/// Carries the final response to send before closing. +struct ShutdownRequested : std::exception { + std::vector final_response; + explicit ShutdownRequested(std::vector resp) + : final_response(std::move(resp)) + {} + const char* what() const noexcept override { return "shutdown requested"; } +}; + +using Handler = std::function(const std::vector&)>; + +// --------------------------------------------------------------------------- +// Framing: 4-byte little-endian length prefix +// --------------------------------------------------------------------------- + +inline bool send_frame(int fd, const std::vector& data) +{ + uint32_t len = static_cast(data.size()); + uint8_t header[4] = { + static_cast(len & 0xFF), + static_cast((len >> 8) & 0xFF), + static_cast((len >> 16) & 0xFF), + static_cast((len >> 24) & 0xFF), + }; + // Write header + size_t written = 0; + while (written < 4) { + auto n = ::write(fd, header + written, 4 - written); + if (n <= 0) { + return false; + } + written += static_cast(n); + } + // Write payload + written = 0; + while (written < data.size()) { + auto n = ::write(fd, data.data() + written, data.size() - written); + if (n <= 0) { + return false; + } + written += static_cast(n); + } + return true; +} + +/// Returns empty vector on EOF/error. +inline std::vector recv_frame(int fd) +{ + uint8_t header[4]; + size_t got = 0; + while (got < 4) { + auto n = ::read(fd, header + got, 4 - got); + if (n <= 0) { + return {}; + } + got += static_cast(n); + } + uint32_t len = static_cast(header[0]) | (static_cast(header[1]) << 8) | + (static_cast(header[2]) << 16) | (static_cast(header[3]) << 24); + std::vector buf(len); + got = 0; + while (got < len) { + auto n = ::read(fd, buf.data() + got, len - got); + if (n <= 0) { + return {}; + } + got += static_cast(n); + } + return buf; +} + +// --------------------------------------------------------------------------- +// Multi-client UDS server +// --------------------------------------------------------------------------- + +/** + * @brief Run a multi-client UDS server. + * + * Accepts multiple client connections via poll(). Handles one request at a time + * (sequential, not concurrent). When a handler throws ShutdownRequested, the + * final response is sent and the server exits cleanly. + * + * The caller should set up signal handlers and parent death monitoring before + * calling this function. To request external shutdown, store true into the + * provided shutdown_flag (or pass nullptr to disable external shutdown). + * + * @param socket_path Path for the Unix domain socket + * @param handler Function that processes a request and returns a response + * @param shutdown_flag Atomic flag checked each poll cycle; serve() exits when true. + * May be nullptr if only ShutdownRequested is used. + * @param backlog listen() backlog (max pending connections) + */ +inline void serve(const char* socket_path, + Handler handler, + std::atomic* shutdown_flag = nullptr, + int backlog = 5) +{ + unlink(socket_path); + int server_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd < 0) { + throw std::runtime_error(std::string("socket() failed: ") + strerror(errno)); + } + + struct sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(server_fd); + throw std::runtime_error(std::string("bind() failed: ") + strerror(errno)); + } + if (::listen(server_fd, backlog) < 0) { + ::close(server_fd); + throw std::runtime_error(std::string("listen() failed: ") + strerror(errno)); + } + + // Poll set: [0] = server_fd, [1..N] = client fds + std::vector fds; + fds.push_back({ server_fd, POLLIN, 0 }); + + auto remove_client = [&](size_t idx) { + ::close(fds[idx].fd); + fds.erase(fds.begin() + static_cast(idx)); + }; + + auto should_shutdown = [&]() { + return shutdown_flag != nullptr && shutdown_flag->load(std::memory_order_acquire); + }; + + while (!should_shutdown()) { + int ready = ::poll(fds.data(), static_cast(fds.size()), 100 /* ms */); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (ready == 0) { + continue; + } + + // Check server fd for new connections + if (fds[0].revents & POLLIN) { + int client_fd = ::accept(server_fd, nullptr, nullptr); + if (client_fd >= 0) { + fds.push_back({ client_fd, POLLIN, 0 }); + } + } + + // Check client fds for data + for (size_t i = 1; i < fds.size(); /* incremented below */) { + if (!(fds[i].revents & POLLIN)) { + ++i; + continue; + } + + auto payload = recv_frame(fds[i].fd); + if (payload.empty()) { + // Client disconnected + remove_client(i); + continue; + } + + try { + auto response = handler(payload); + if (!send_frame(fds[i].fd, response)) { + remove_client(i); + continue; + } + } catch (const ShutdownRequested& shutdown) { + send_frame(fds[i].fd, shutdown.final_response); + goto done; + } catch (const std::exception& e) { + std::cerr << "ipc-server: handler error: " << e.what() << "\n"; + remove_client(i); + continue; + } + ++i; + } + } + +done: + // Close all client connections + for (size_t i = 1; i < fds.size(); ++i) { + ::close(fds[i].fd); + } + ::close(server_fd); + unlink(socket_path); +} + +} // namespace ipc +#endif // !defined(__wasm__) +#endif // IPC_SERVER_HPP_INCLUDED diff --git a/ipc-codegen/templates/rust/backend.rs b/ipc-codegen/templates/rust/backend.rs new file mode 100644 index 000000000000..d35f58dfd3e0 --- /dev/null +++ b/ipc-codegen/templates/rust/backend.rs @@ -0,0 +1,44 @@ +//! Backend trait for msgpack communication +//! +//! This module defines a simple, pluggable interface for Barretenberg backends. +//! Users can easily implement custom backends (FFI, WASM, IPC, etc.). + +use super::error::Result; + +/// Simple interface for msgpack backend implementations. +/// +/// Implement this trait to create a custom backend for Barretenberg. +/// The backend handles msgpack-encoded command/response communication. +/// +/// # Example +/// +/// ```ignore +/// struct MyCustomBackend { +/// // your FFI handle, connection, etc. +/// } +/// +/// impl Backend for MyCustomBackend { +/// fn call(&mut self, input: &[u8]) -> Result> { +/// // Send input to your backend +/// // Return the response +/// } +/// +/// fn destroy(&mut self) -> Result<()> { +/// // Clean up resources +/// Ok(()) +/// } +/// } +/// ``` +pub trait Backend { + /// Execute a msgpack command and return the msgpack response. + /// + /// # Arguments + /// * `input` - Msgpack-encoded command + /// + /// # Returns + /// Msgpack-encoded response + fn call(&mut self, input: &[u8]) -> Result>; + + /// Clean up resources and shutdown the backend. + fn destroy(&mut self) -> Result<()>; +} diff --git a/ipc-codegen/templates/rust/error.rs b/ipc-codegen/templates/rust/error.rs new file mode 100644 index 000000000000..726ac4a9ad2f --- /dev/null +++ b/ipc-codegen/templates/rust/error.rs @@ -0,0 +1,32 @@ +//! Error types for Barretenberg operations + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BarretenbergError { + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Deserialization error: {0}")] + Deserialization(String), + + #[error("Backend error: {0}")] + Backend(String), + + #[error("IPC error: {0}")] + Ipc(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Invalid response: {0}")] + InvalidResponse(String), + + #[error("Connection error: {0}")] + Connection(String), + + #[error("WASM error: {0}")] + Wasm(String), +} + +pub type Result = std::result::Result; diff --git a/ipc-codegen/templates/rust/ffi_backend.rs b/ipc-codegen/templates/rust/ffi_backend.rs new file mode 100644 index 000000000000..490546001feb --- /dev/null +++ b/ipc-codegen/templates/rust/ffi_backend.rs @@ -0,0 +1,163 @@ +//! FFI backend for Barretenberg +//! +//! This backend calls the Barretenberg C API directly via FFI, +//! eliminating process spawn overhead. Ideal for mobile and embedded use cases. +//! +//! # Requirements +//! +//! This backend requires linking against `libbarretenberg`. You must: +//! 1. Build Barretenberg as a static library (`libbarretenberg.a`) +//! 2. Configure the library search path, either via: +//! - `.cargo/config.toml`: `[build] rustflags = ["-L", "/path/to/lib"]` +//! - Environment: `RUSTFLAGS="-L /path/to/lib"` +//! +//! # Example +//! +//! ```ignore +//! use barretenberg_rs::{BbApi, FfiBackend}; +//! +//! let backend = FfiBackend::new()?; +//! let mut api = BbApi::new(backend); +//! +//! let response = api.blake2s(b"hello world")?; +//! println!("Hash: {:?}", response.hash); +//! ``` + +use super::backend::Backend; +use super::error::{BarretenbergError, Result}; +use std::ptr; + +// C API exported by Barretenberg +// See: barretenberg/cpp/src/barretenberg/bbapi/c_bind.hpp +// Link directives are in build.rs to control link order (barretenberg depends on env) +extern "C" { + /// Execute a msgpack-encoded command and return msgpack-encoded response. + /// + /// # Safety + /// - `input_in` must point to valid memory of `input_len_in` bytes + /// - `output_out` and `output_len_out` must be valid pointers + /// - Caller must free `*output_out` using `libc::free` + fn bbapi( + input_in: *const u8, + input_len_in: usize, + output_out: *mut *mut u8, + output_len_out: *mut usize, + ); +} + +/// FFI backend that calls Barretenberg directly via C API. +/// +/// This is the most performant backend option as it avoids process spawning +/// and IPC overhead. However, it requires linking against `libbarretenberg`. +/// +/// # Thread Safety +/// +/// This backend is **not** thread-safe. Each thread should have its own +/// `FfiBackend` instance, or access should be synchronized externally. +pub struct FfiBackend { + _initialized: bool, +} + +impl FfiBackend { + /// Create a new FFI backend. + /// + /// # Errors + /// + /// Returns an error if Barretenberg initialization fails. + pub fn new() -> Result { + // Future: Could add SRS initialization here if needed + // For now, Barretenberg initializes lazily on first use + Ok(Self { _initialized: true }) + } +} + +impl Backend for FfiBackend { + fn call(&mut self, input: &[u8]) -> Result> { + let mut output_ptr: *mut u8 = ptr::null_mut(); + let mut output_len: usize = 0; + + // SAFETY: + // - input.as_ptr() is valid for input.len() bytes + // - output_ptr and output_len are valid stack pointers + // - bbapi allocates output using malloc, which we free below + unsafe { + bbapi( + input.as_ptr(), + input.len(), + &mut output_ptr, + &mut output_len, + ); + } + + if output_ptr.is_null() { + return Err(BarretenbergError::Backend( + "bbapi returned null pointer".to_string(), + )); + } + + if output_len == 0 { + // Free the pointer even if length is 0 + unsafe { + libc::free(output_ptr as *mut libc::c_void); + } + return Err(BarretenbergError::Backend( + "bbapi returned empty response".to_string(), + )); + } + + // SAFETY: output_ptr is valid for output_len bytes, allocated by malloc + let output = unsafe { std::slice::from_raw_parts(output_ptr, output_len).to_vec() }; + + // Free the C-allocated memory + // SAFETY: output_ptr was allocated by bbapi using malloc + unsafe { + libc::free(output_ptr as *mut libc::c_void); + } + + Ok(output) + } + + fn destroy(&mut self) -> Result<()> { + // No cleanup needed - Barretenberg manages its own state + // Future: Could send Shutdown command here if needed + self._initialized = false; + Ok(()) + } +} + +impl Drop for FfiBackend { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +impl Default for FfiBackend { + fn default() -> Self { + Self::new().expect("Failed to initialize FfiBackend") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::generated::bb_client::BbApi; + + #[test] + fn test_ffi_backend_creation() { + let backend = FfiBackend::new(); + assert!(backend.is_ok()); + } + + #[test] + fn test_ffi_blake2s() { + let backend = FfiBackend::new().unwrap(); + let mut api = BbApi::new(backend); + + let response = api.blake2s(b"hello world").unwrap(); + assert_eq!(response.hash.as_slice().len(), 32); + + // Verify deterministic output + let response2 = api.blake2s(b"hello world").unwrap(); + assert_eq!(response.hash, response2.hash); + } +} diff --git a/ipc-codegen/templates/rust/ipc_client.rs b/ipc-codegen/templates/rust/ipc_client.rs new file mode 100644 index 000000000000..35f7094ddef6 --- /dev/null +++ b/ipc-codegen/templates/rust/ipc_client.rs @@ -0,0 +1,42 @@ +//! Generic IPC client over Unix Domain Sockets. +//! Handles: socket connect, length-prefixed framing, send/receive raw bytes. +//! Service-specific typed methods are in the generated wrapper. + +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; + +pub struct IpcClient { + stream: UnixStream, +} + +impl IpcClient { + pub fn connect(socket_path: &str) -> std::io::Result { + let stream = UnixStream::connect(socket_path)?; + Ok(Self { stream }) + } + + /// Send a msgpack request and receive the raw response bytes. + pub fn call(&mut self, request: &[u8]) -> std::io::Result> { + // Send length-prefixed + let len = (request.len() as u32).to_le_bytes(); + self.stream.write_all(&len)?; + self.stream.write_all(request)?; + self.stream.flush()?; + + // Read response length + let mut len_buf = [0u8; 4]; + self.stream.read_exact(&mut len_buf)?; + let resp_len = u32::from_le_bytes(len_buf) as usize; + + // Read response payload + let mut resp = vec![0u8; resp_len]; + self.stream.read_exact(&mut resp)?; + Ok(resp) + } +} + +impl Drop for IpcClient { + fn drop(&mut self) { + let _ = self.stream.shutdown(std::net::Shutdown::Both); + } +} diff --git a/ipc-codegen/templates/rust/ipc_server.rs b/ipc-codegen/templates/rust/ipc_server.rs new file mode 100644 index 000000000000..cfce19cbc26a --- /dev/null +++ b/ipc-codegen/templates/rust/ipc_server.rs @@ -0,0 +1,51 @@ +//! Generic IPC server over Unix Domain Sockets. +//! Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. +//! Service-specific dispatch is injected via the dispatch function parameter. + +use std::io::{Read, Write}; +use std::os::unix::net::UnixListener; + +/// Dispatch function signature: takes raw command name + msgpack bytes, returns response name + bytes +pub type DispatchFn = Box Vec>; + +/// Run an IPC server. Accepts one connection, serves until disconnect or shutdown. +pub fn serve(socket_path: &str, handler: impl Fn(&[u8]) -> Vec) -> std::io::Result<()> { + let _ = std::fs::remove_file(socket_path); + let listener = UnixListener::bind(socket_path)?; + eprintln!("ipc-server(rust): listening on {}", socket_path); + + let (mut stream, _) = listener.accept()?; + + loop { + // Read 4-byte LE length + let mut len_buf = [0u8; 4]; + if stream.read_exact(&mut len_buf).is_err() { + break; + } + let len = u32::from_le_bytes(len_buf) as usize; + + // Read payload + let mut payload = vec![0u8; len]; + stream.read_exact(&mut payload)?; + + // Check for shutdown + let is_shutdown = payload.windows(8).any(|w| w == b"Shutdown"); + + // Dispatch + let response = handler(&payload); + + // Send length-prefixed response + let resp_len = (response.len() as u32).to_le_bytes(); + stream.write_all(&resp_len)?; + stream.write_all(&response)?; + stream.flush()?; + + if is_shutdown { + break; + } + } + + let _ = std::fs::remove_file(socket_path); + eprintln!("ipc-server(rust): shutdown"); + Ok(()) +} diff --git a/ipc-codegen/templates/rust/uds_backend.rs b/ipc-codegen/templates/rust/uds_backend.rs new file mode 100644 index 000000000000..a524391cd687 --- /dev/null +++ b/ipc-codegen/templates/rust/uds_backend.rs @@ -0,0 +1,78 @@ +//! UDS (Unix Domain Socket) backend for Barretenberg +//! +//! Connects to a running BB server over a Unix domain socket, +//! using the standard 4-byte LE length-prefixed msgpack protocol. +//! Same wire format as C++/TS/Zig IPC clients. + +use super::backend::Backend; +use super::error::{BarretenbergError, Result}; +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; + +/// UDS backend — connects to a BB server over Unix domain socket. +pub struct UdsBackend { + stream: UnixStream, +} + +impl UdsBackend { + /// Connect to a BB server at the given socket path. + /// + /// # Arguments + /// * `socket_path` - Path to the Unix domain socket (e.g. "/tmp/bb.sock") + pub fn connect(socket_path: impl AsRef) -> Result { + let stream = UnixStream::connect(socket_path.as_ref()).map_err(|e| { + BarretenbergError::Ipc(format!( + "Failed to connect to {}: {}", + socket_path.as_ref().display(), + e + )) + })?; + Ok(Self { stream }) + } + + fn send_with_prefix(&mut self, data: &[u8]) -> Result<()> { + let len = data.len() as u32; + self.stream + .write_all(&len.to_le_bytes()) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to write length: {}", e)))?; + self.stream + .write_all(data) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to write data: {}", e)))?; + self.stream + .flush() + .map_err(|e| BarretenbergError::Ipc(format!("Failed to flush: {}", e)))?; + Ok(()) + } + + fn receive_with_prefix(&mut self) -> Result> { + let mut len_buf = [0u8; 4]; + self.stream + .read_exact(&mut len_buf) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to read length: {}", e)))?; + let len = u32::from_le_bytes(len_buf) as usize; + let mut data = vec![0u8; len]; + self.stream + .read_exact(&mut data) + .map_err(|e| BarretenbergError::Ipc(format!("Failed to read data: {}", e)))?; + Ok(data) + } +} + +impl Backend for UdsBackend { + fn call(&mut self, input: &[u8]) -> Result> { + self.send_with_prefix(input)?; + self.receive_with_prefix() + } + + fn destroy(&mut self) -> Result<()> { + let _ = self.stream.shutdown(std::net::Shutdown::Both); + Ok(()) + } +} + +impl Drop for UdsBackend { + fn drop(&mut self) { + let _ = self.destroy(); + } +} diff --git a/ipc-codegen/templates/ts/ipc_client.ts b/ipc-codegen/templates/ts/ipc_client.ts new file mode 100644 index 000000000000..1fbdf761b3b1 --- /dev/null +++ b/ipc-codegen/templates/ts/ipc_client.ts @@ -0,0 +1,58 @@ +/** + * Generic IPC client over Unix Domain Sockets. + * Handles: socket connect, length-prefixed framing, msgpack encode/decode. + * Service-specific typed methods are in the generated wrapper. + */ +import * as net from 'node:net'; +import { Decoder, Encoder } from 'msgpackr'; + +const encoder = new Encoder({ useRecords: false }); +const decoder = new Decoder({ useRecords: false }); + +export class IpcClient { + private conn: net.Socket; + private buffer = Buffer.alloc(0); + + private constructor(conn: net.Socket) { + this.conn = conn; + } + + static async connect(socketPath: string): Promise { + const conn = net.createConnection(socketPath); + await new Promise((resolve, reject) => { + conn.on('connect', resolve); + conn.on('error', reject); + }); + return new IpcClient(conn); + } + + close() { + this.conn.destroy(); + } + + /** Send a command and receive the response. */ + async call(commandName: string, fields: any): Promise<[string, any]> { + const packed = encoder.pack([[commandName, fields]]); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32LE(packed.length, 0); + this.conn.write(lenBuf); + this.conn.write(packed); + + return new Promise((resolve, reject) => { + const onData = (data: Buffer) => { + this.buffer = Buffer.concat([this.buffer, data]); + if (this.buffer.length >= 4) { + const len = this.buffer.readUInt32LE(0); + if (this.buffer.length >= 4 + len) { + this.conn.removeListener('data', onData); + const payload = this.buffer.subarray(4, 4 + len); + this.buffer = this.buffer.subarray(4 + len); + resolve(decoder.unpack(payload) as [string, any]); + } + } + }; + this.conn.on('data', onData); + this.conn.on('error', reject); + }); + } +} diff --git a/ipc-codegen/templates/ts/ipc_server.ts b/ipc-codegen/templates/ts/ipc_server.ts new file mode 100644 index 000000000000..3ef981ba2182 --- /dev/null +++ b/ipc-codegen/templates/ts/ipc_server.ts @@ -0,0 +1,82 @@ +/** + * Generic IPC server over Unix Domain Sockets. + * Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. + * Service-specific dispatch is injected via the dispatchFn parameter. + */ +import * as net from 'node:net'; +import * as fs from 'node:fs'; +import { Decoder, Encoder } from 'msgpackr'; + +const encoder = new Encoder({ useRecords: false }); +const decoder = new Decoder({ useRecords: false }); + +export type DispatchFn = (commandName: string, payload: any) => Promise<[string, any]>; + +export function createServer(socketPath: string, dispatchFn: DispatchFn): { close: () => Promise } { + try { fs.unlinkSync(socketPath); } catch {} + + const server = net.createServer((conn) => { + let buffer = Buffer.alloc(0); + let responseChain: Promise = Promise.resolve(); + + conn.on('data', (data: Buffer) => { + buffer = Buffer.concat([buffer, data]); + + while (buffer.length >= 4) { + const len = buffer.readUInt32LE(0); + if (buffer.length < 4 + len) break; + + const payload = buffer.subarray(4, 4 + len); + buffer = buffer.subarray(4 + len); + + const request = decoder.unpack(payload) as any[]; + const [commandName, fields] = request[0] as [string, any]; + + if (commandName.endsWith('Shutdown')) { + sendResponse(conn, [commandName.replace(/^(.*)$/, '$1Response'), {}]); + setTimeout(() => { + server.close(); + try { fs.unlinkSync(socketPath); } catch {} + }, 50); + return; + } + + const prev = responseChain; + const result = dispatchFn(commandName, fields ?? {}); + responseChain = (async () => { + await prev; + try { + const [name, resp] = await result; + sendResponse(conn, [name, resp]); + } catch (err: any) { + sendResponse(conn, ['ErrorResponse', { message: err.message ?? 'Unknown error' }]); + } + })(); + void responseChain.catch(() => {}); + } + }); + + conn.on('error', () => {}); + }); + + server.listen(socketPath, () => { + console.error(`ipc-server(ts): listening on ${socketPath}`); + }); + + return { + close: () => new Promise(resolve => { + server.close(() => { + try { fs.unlinkSync(socketPath); } catch {} + resolve(); + }); + }), + }; +} + +function sendResponse(conn: net.Socket, response: [string, any]) { + const packed = encoder.pack(response); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32LE(packed.length, 0); + conn.write(lenBuf); + conn.write(packed); +} diff --git a/ipc-codegen/templates/zig/backend.zig b/ipc-codegen/templates/zig/backend.zig new file mode 100644 index 000000000000..97a65bdbfa07 --- /dev/null +++ b/ipc-codegen/templates/zig/backend.zig @@ -0,0 +1,27 @@ +/// Backend abstraction — comptime interface for transport. +/// +/// A valid backend type must provide: +/// fn call(self: *T, request: []const u8) ![]u8 +/// fn destroy(self: *T) void +/// +/// Implementations: +/// UdsBackend (uds_backend.zig) — Unix Domain Socket IPC +/// FfiBackend (ffi_backend.zig) — Direct C FFI linking +/// +/// Usage with the generated client: +/// const Client = @import("wsdb_client.zig").Client; +/// const UdsBackend = @import("uds_backend.zig").UdsBackend; +/// var backend = try UdsBackend.connect("/tmp/wsdb.sock"); +/// var client = Client(UdsBackend){ .backend = &backend }; + +/// Compile-time check that a type satisfies the backend interface. +pub fn assertBackend(comptime T: type) void { + // Must have: fn call(self: *T, request: []const u8) ![]u8 + if (!@hasDecl(T, "call")) { + @compileError("Backend type " ++ @typeName(T) ++ " missing 'call' method"); + } + // Must have: fn destroy(self: *T) void + if (!@hasDecl(T, "destroy")) { + @compileError("Backend type " ++ @typeName(T) ++ " missing 'destroy' method"); + } +} diff --git a/ipc-codegen/templates/zig/ffi_backend.zig b/ipc-codegen/templates/zig/ffi_backend.zig new file mode 100644 index 000000000000..0963dfb0589e --- /dev/null +++ b/ipc-codegen/templates/zig/ffi_backend.zig @@ -0,0 +1,22 @@ +/// FFI backend for direct library linking. +/// Calls the C function bbapi() with msgpack bytes — no IPC overhead. +/// Link against libbarretenberg.a to use this backend. +/// Satisfies the backend interface: call(request) -> response, destroy(). +const std = @import("std"); + +extern fn bbapi(input: [*]const u8, input_len: usize, output: *[*]u8, output_len: *usize) void; + +pub const FfiBackend = struct { + /// Send a msgpack command and receive the response via FFI. + pub fn call(self: *FfiBackend, request: []const u8) ![]u8 { + _ = self; + var out_ptr: [*]u8 = undefined; + var out_len: usize = 0; + bbapi(request.ptr, request.len, &out_ptr, &out_len); + return out_ptr[0..out_len]; + } + + pub fn destroy(self: *FfiBackend) void { + _ = self; + } +}; diff --git a/ipc-codegen/templates/zig/ffi_client.zig b/ipc-codegen/templates/zig/ffi_client.zig new file mode 100644 index 000000000000..960a4eee9b21 --- /dev/null +++ b/ipc-codegen/templates/zig/ffi_client.zig @@ -0,0 +1,21 @@ +/// FFI client for direct library linking. +/// Calls the C function bbapi() with msgpack bytes — no IPC overhead. +/// Link against libbarretenberg.a to use this backend. +const std = @import("std"); + +extern fn bbapi(input: [*]const u8, input_len: usize, output: *[*]u8, output_len: *usize) void; + +pub const FfiClient = struct { + /// Send a msgpack command and receive the response via FFI. + pub fn call(self: *FfiClient, request: []const u8) ![]u8 { + _ = self; + var out_ptr: [*]u8 = undefined; + var out_len: usize = 0; + bbapi(request.ptr, request.len, &out_ptr, &out_len); + return out_ptr[0..out_len]; + } + + pub fn destroy(self: *FfiClient) void { + _ = self; + } +}; diff --git a/ipc-codegen/templates/zig/ipc_client.zig b/ipc-codegen/templates/zig/ipc_client.zig new file mode 100644 index 000000000000..db53693cf57c --- /dev/null +++ b/ipc-codegen/templates/zig/ipc_client.zig @@ -0,0 +1,93 @@ +/// Generic IPC client over Unix Domain Sockets. +/// Handles: socket connect, length-prefixed framing, msgpack encode/decode. +/// Service-specific typed methods are in the generated wrapper. +const std = @import("std"); +const posix = std.posix; +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; + +const alloc = std.heap.page_allocator; + +pub const IpcClient = struct { + fd: posix.socket_t, + + /// Connect to a service at the given UDS path. + pub fn connect(socket_path: []const u8) !IpcClient { + const address = try std.net.Address.initUnix(socket_path); + const fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); + try posix.connect(fd, &address.any, address.getOsSockLen()); + return .{ .fd = fd }; + } + + /// Close the connection. + pub fn close(self: *IpcClient) void { + posix.close(self.fd); + } + + /// Send a command and receive a response. + /// Returns [responseName, responsePayload]. + pub fn call(self: *IpcClient, cmd_name: []const u8, fields: Payload) !struct { []const u8, Payload } { + // Encode: [[cmdName, fields]] + var inner = try Payload.arrPayload(2, alloc); + try inner.setArrElement(0, try Payload.strToPayload(cmd_name, alloc)); + try inner.setArrElement(1, fields); + var outer = try Payload.arrPayload(1, alloc); + try outer.setArrElement(0, inner); + + // Serialize + var allocating_writer = std.Io.Writer.Allocating.init(alloc); + var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); + try packer.write(outer); + const request_bytes = try allocating_writer.toOwnedSlice(); + defer alloc.free(request_bytes); + + // Send + try sendFrame(self.fd, request_bytes); + + // Receive + const response_bytes = try recvFrame(self.fd); + defer alloc.free(response_bytes); + + // Decode: [responseName, payload] + var reader = std.Io.Reader.fixed(response_bytes); + var unpacker = msgpack.PackerIO.init(&reader, undefined); + const resp = try unpacker.read(alloc); + const resp_len = try resp.getArrLen(); + if (resp_len != 2) return error.InvalidResponse; + + const name = try (try resp.getArrElement(0)).asStr(); + const payload = try resp.getArrElement(1); + return .{ name, payload }; + } + + fn sendFrame(fd: posix.socket_t, data: []const u8) !void { + const len: u32 = @intCast(data.len); + const header = [4]u8{ + @intCast(len & 0xFF), + @intCast((len >> 8) & 0xFF), + @intCast((len >> 16) & 0xFF), + @intCast((len >> 24) & 0xFF), + }; + _ = try posix.write(fd, &header); + _ = try posix.write(fd, data); + } + + fn recvFrame(fd: posix.socket_t) ![]u8 { + var hdr: [4]u8 = undefined; + var got: usize = 0; + while (got < 4) { + const n = try posix.read(fd, hdr[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); + const data = try alloc.alloc(u8, len); + got = 0; + while (got < len) { + const n = try posix.read(fd, data[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + return data; + } +}; diff --git a/ipc-codegen/templates/zig/ipc_server.zig b/ipc-codegen/templates/zig/ipc_server.zig new file mode 100644 index 000000000000..ba58a09b5a61 --- /dev/null +++ b/ipc-codegen/templates/zig/ipc_server.zig @@ -0,0 +1,112 @@ +/// Generic IPC server over Unix Domain Sockets. +/// Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. +/// Service-specific dispatch is injected via the DispatchFn parameter. +const std = @import("std"); +const posix = std.posix; +const msgpack = @import("msgpack"); +const Payload = msgpack.Payload; + +const alloc = std.heap.page_allocator; + +pub const DispatchFn = *const fn (cmd_name: []const u8, fields: Payload) DispatchResult; +pub const DispatchResult = struct { resp_name: []const u8, resp_payload: anyerror!Payload }; + +/// Run an IPC server on the given UDS path. +/// Accepts one connection, serves requests until shutdown or disconnect. +pub fn serve(socket_path: []const u8, dispatch: DispatchFn) !void { + std.fs.cwd().deleteFile(socket_path) catch {}; + + const address = try std.net.Address.initUnix(socket_path); + const server_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); + defer posix.close(server_fd); + try posix.bind(server_fd, &address.any, address.getOsSockLen()); + try posix.listen(server_fd, 1); + + std.debug.print("ipc-server: listening on {s}\n", .{socket_path}); + + const client_fd = try posix.accept(server_fd, null, null, 0); + defer posix.close(client_fd); + + while (true) { + const frame = recvFrame(client_fd) catch break; + defer alloc.free(frame); + + // Decode msgpack: [[commandName, {fields}]] + var reader = std.Io.Reader.fixed(frame); + var packer = msgpack.PackerIO.init(&reader, undefined); + const request = packer.read(alloc) catch break; + + const outer_len = request.getArrLen() catch break; + if (outer_len != 1) break; + + const inner = request.getArrElement(0) catch break; + const inner_len = inner.getArrLen() catch break; + if (inner_len != 2) break; + + const cmd_name = (inner.getArrElement(0) catch break).asStr() catch break; + const fields = inner.getArrElement(1) catch break; + + // Dispatch + const result = dispatch(cmd_name, fields); + const resp_payload = result.resp_payload catch blk: { + var err_map = Payload.mapPayload(alloc); + const msg = std.fmt.allocPrint(alloc, "error: {s}", .{cmd_name}) catch "error"; + err_map.mapPut("message", Payload.strToPayload(msg, alloc) catch break) catch break; + break :blk err_map; + }; + const is_error = if (result.resp_payload) |_| false else |_| true; + const resp_name = if (is_error) "ErrorResponse" else result.resp_name; + + const response = encodeResponse(resp_name, resp_payload) catch break; + defer alloc.free(response); + sendFrame(client_fd, response) catch break; + + // Check for shutdown + if (std.mem.indexOf(u8, cmd_name, "Shutdown") != null) break; + } + + std.fs.cwd().deleteFile(socket_path) catch {}; + std.debug.print("ipc-server: shutdown\n", .{}); +} + +fn encodeResponse(name: []const u8, payload: Payload) ![]u8 { + var resp_arr = try Payload.arrPayload(2, alloc); + try resp_arr.setArrElement(0, try Payload.strToPayload(name, alloc)); + try resp_arr.setArrElement(1, payload); + + var allocating_writer = std.Io.Writer.Allocating.init(alloc); + var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); + try packer.write(resp_arr); + return try allocating_writer.toOwnedSlice(); +} + +fn recvFrame(fd: posix.socket_t) ![]u8 { + var hdr: [4]u8 = undefined; + var got: usize = 0; + while (got < 4) { + const n = try posix.read(fd, hdr[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); + const data = try alloc.alloc(u8, len); + got = 0; + while (got < len) { + const n = try posix.read(fd, data[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + return data; +} + +fn sendFrame(fd: posix.socket_t, data: []const u8) !void { + const len: u32 = @intCast(data.len); + const header = [4]u8{ + @intCast(len & 0xFF), + @intCast((len >> 8) & 0xFF), + @intCast((len >> 16) & 0xFF), + @intCast((len >> 24) & 0xFF), + }; + _ = try posix.write(fd, &header); + _ = try posix.write(fd, data); +} diff --git a/ipc-codegen/templates/zig/uds_backend.zig b/ipc-codegen/templates/zig/uds_backend.zig new file mode 100644 index 000000000000..af701e2d05c9 --- /dev/null +++ b/ipc-codegen/templates/zig/uds_backend.zig @@ -0,0 +1,62 @@ +/// UDS (Unix Domain Socket) backend for IPC communication. +/// Handles: socket connect, length-prefixed framing, raw byte send/receive. +/// Satisfies the backend interface: call(request) -> response, destroy(). +const std = @import("std"); +const posix = std.posix; + +const alloc = std.heap.page_allocator; + +pub const UdsBackend = struct { + fd: posix.socket_t, + + /// Connect to a service at the given UDS path. + pub fn connect(socket_path: []const u8) !UdsBackend { + const address = try std.net.Address.initUnix(socket_path); + const fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); + try posix.connect(fd, &address.any, address.getOsSockLen()); + return .{ .fd = fd }; + } + + /// Send a raw msgpack request and receive a raw msgpack response. + /// Framing: 4-byte LE length prefix + payload. + pub fn call(self: *UdsBackend, request: []const u8) ![]u8 { + try sendFrame(self.fd, request); + return try recvFrame(self.fd); + } + + /// Close the connection. + pub fn destroy(self: *UdsBackend) void { + posix.close(self.fd); + } + + fn sendFrame(fd: posix.socket_t, data: []const u8) !void { + const len: u32 = @intCast(data.len); + const header = [4]u8{ + @intCast(len & 0xFF), + @intCast((len >> 8) & 0xFF), + @intCast((len >> 16) & 0xFF), + @intCast((len >> 24) & 0xFF), + }; + _ = try posix.write(fd, &header); + _ = try posix.write(fd, data); + } + + fn recvFrame(fd: posix.socket_t) ![]u8 { + var hdr: [4]u8 = undefined; + var got: usize = 0; + while (got < 4) { + const n = try posix.read(fd, hdr[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); + const data = try alloc.alloc(u8, len); + got = 0; + while (got < len) { + const n = try posix.read(fd, data[got..]); + if (n == 0) return error.ConnectionClosed; + got += n; + } + return data; + } +}; From ca1c08d18d5f4e538d6ce0e907afa0d1dd289bb4 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 16:43:19 +0000 Subject: [PATCH 02/11] chore(ipc-codegen): align bootstrap with build/test convention; drop generated files Restructure ipc-codegen/ to follow the same bootstrap.sh shape as other components: - bootstrap.sh: rename generate -> build (matches barretenberg/ts pattern). Add test_cmds emitting the cross-language wire-compat command. Add test wrapping test_cmds | filter_test_cmds | parallelize. - Makefile: add ipc-codegen and ipc-codegen-tests targets, include in fast bootstrap (alongside claude-tests). - Drop committed pure-generated files (echo_types.*, echo_*_ipc_*.*, api_types.ts, async.ts, sync.ts, server.ts, ipc_*.{ts,rs,zig,hpp}); they regenerate on every build. - Keep committed scaffolding files (backend.{rs,zig}, error.rs, uds_backend.{rs,zig}); they're one-time-copied templates hand-customized per example (echo's EchoError vs the template's BarretenbergError). - Refine ipc-codegen/.gitignore to distinguish: ignore pure generated, exempt scaffolding. Net: 23 files changed, +49 / -2492 LOC. Verify: - cd ipc-codegen && ./bootstrap.sh build # regenerate echo bindings - cd ipc-codegen && ./bootstrap.sh test # 18/18 wire-compat pass - make ipc-codegen-tests # via root Makefile --- Makefile | 13 +- ipc-codegen/.gitignore | 11 + ipc-codegen/bootstrap.sh | 50 +-- .../cpp/echo/generated/echo_ipc_client.cpp | 88 ---- .../cpp/echo/generated/echo_ipc_client.hpp | 43 -- .../cpp/echo/generated/echo_ipc_server.hpp | 165 ------- .../cpp/echo/generated/echo_types.hpp | 134 ------ .../cpp/echo/generated/ipc_client.hpp | 112 ----- .../cpp/echo/generated/ipc_server.hpp | 252 ----------- .../rust/echo/src/generated/echo_client.rs | 84 ---- .../rust/echo/src/generated/echo_server.rs | 40 -- .../rust/echo/src/generated/echo_types.rs | 401 ------------------ .../rust/echo/src/generated/ipc_server.rs | 51 --- .../examples/ts/echo/generated/api_types.ts | 259 ----------- .../examples/ts/echo/generated/async.ts | 72 ---- .../examples/ts/echo/generated/ipc_client.ts | 58 --- .../examples/ts/echo/generated/ipc_server.ts | 82 ---- .../examples/ts/echo/generated/server.ts | 41 -- .../examples/ts/echo/generated/sync.ts | 68 --- .../zig/echo/generated/echo_client.zig | 94 ---- .../zig/echo/generated/echo_server.zig | 72 ---- .../zig/echo/generated/echo_types.zig | 239 ----------- .../zig/echo/generated/ipc_server.zig | 112 ----- 23 files changed, 49 insertions(+), 2492 deletions(-) delete mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp delete mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp delete mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp delete mode 100644 ipc-codegen/examples/cpp/echo/generated/echo_types.hpp delete mode 100644 ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp delete mode 100644 ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/echo_client.rs delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/echo_server.rs delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/echo_types.rs delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs delete mode 100644 ipc-codegen/examples/ts/echo/generated/api_types.ts delete mode 100644 ipc-codegen/examples/ts/echo/generated/async.ts delete mode 100644 ipc-codegen/examples/ts/echo/generated/ipc_client.ts delete mode 100644 ipc-codegen/examples/ts/echo/generated/ipc_server.ts delete mode 100644 ipc-codegen/examples/ts/echo/generated/server.ts delete mode 100644 ipc-codegen/examples/ts/echo/generated/sync.ts delete mode 100644 ipc-codegen/examples/zig/echo/generated/echo_client.zig delete mode 100644 ipc-codegen/examples/zig/echo/generated/echo_server.zig delete mode 100644 ipc-codegen/examples/zig/echo/generated/echo_types.zig delete mode 100644 ipc-codegen/examples/zig/echo/generated/ipc_server.zig diff --git a/Makefile b/Makefile index 065fcc43871b..dec6669a36d4 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ endef # Fast bootstrap. fast: release-image barretenberg boxes playground docs aztec-up \ - bb-tests l1-contracts-tests yarn-project-tests boxes-tests playground-tests aztec-up-tests docs-tests noir-protocol-circuits-tests release-image-tests spartan claude-tests + bb-tests l1-contracts-tests yarn-project-tests boxes-tests playground-tests aztec-up-tests docs-tests noir-protocol-circuits-tests release-image-tests spartan claude-tests ipc-codegen-tests # Full bootstrap. full: fast bb-full-tests bb-cpp-full yarn-project-benches @@ -269,6 +269,17 @@ bb-tests: bb-cpp-native-tests bb-acir-tests bb-ts-tests bb-sol-tests bb-bbup-tes bb-full-tests: bb-cpp-wasm-threads-tests bb-cpp-asan-tests bb-cpp-smt-tests +#============================================================================== +# IPC Codegen +#============================================================================== + +.PHONY: ipc-codegen ipc-codegen-tests +ipc-codegen: + $(call build,$@,ipc-codegen) + +ipc-codegen-tests: ipc-codegen + $(call test,$@,ipc-codegen) + #============================================================================== # .claude tooling #============================================================================== diff --git a/ipc-codegen/.gitignore b/ipc-codegen/.gitignore index fa3c40f8e2be..51b7386bd35c 100644 --- a/ipc-codegen/.gitignore +++ b/ipc-codegen/.gitignore @@ -5,3 +5,14 @@ examples/rust/*/target/ examples/zig/*/.zig-cache/ examples/zig/*/zig-out/ examples/ts/*/node_modules/ + +# Generated bindings under examples — regenerated each build by bootstrap.sh, +# never committed. Scaffolding files (backend.rs, error.rs, uds_backend.rs, +# *.zig backends) ARE committed because they're one-time-copied templates the +# example customizes by hand. +examples/cpp/*/generated/ +examples/ts/*/generated/ +examples/rust/*/src/generated/echo_*.rs +examples/rust/*/src/generated/ipc_server.rs +examples/zig/*/generated/echo_*.zig +examples/zig/*/generated/ipc_server.zig diff --git a/ipc-codegen/bootstrap.sh b/ipc-codegen/bootstrap.sh index 892d84f01299..ceea9547356d 100755 --- a/ipc-codegen/bootstrap.sh +++ b/ipc-codegen/bootstrap.sh @@ -1,46 +1,48 @@ #!/usr/bin/env bash -# Codegen tool: generates bindings from committed JSON schemas. -# Zero npm dependencies — runs with just Node.js (v22+). +# IPC codegen package. +# Generates IPC bindings from committed JSON schemas under schemas/, in TS, C++, +# Rust and Zig. Zero npm dependencies — runs with just Node.js (v22+). # -# Usage: -# ./bootstrap.sh # Run codegen (generate all bindings) -# ./bootstrap.sh generate # Same -# ./bootstrap.sh test # Run the cross-language wire-compat test matrix -# ./bootstrap.sh hash # Print content hash +# The build's only consumer is its own cross-language test harness under +# examples/. Service consumers (bb, wsdb, cdb, avm) are wired up by their +# own bootstrap scripts, which invoke `ipc-codegen/bootstrap.sh build` as +# a build-time prerequisite. source $(git rev-parse --show-toplevel)/ci3/source_bootstrap -# Hash includes codegen source AND committed schema files. -export hash=$(cache_content_hash .rebuild_patterns) +hash=$(cache_content_hash .rebuild_patterns) NODE_FLAGS="--experimental-strip-types --experimental-transform-types --no-warnings" gen() { node $NODE_FLAGS src/generate.ts "$@"; } -function generate { - echo_header "codegen generate" +function build { + echo_header "ipc-codegen build (generate echo example bindings)" - # In this PR, only the echo example wires up codegen output. - # Service consumers (bb, wsdb, cdb, avm) are added by later PRs as they migrate - # from the legacy cbind generator. Until then, schemas live committed under - # schemas/ and the only consumer of codegen output is the echo test harness. + # Service generation (bb, wsdb, cdb, avm) is invoked by each service's own + # bootstrap. The build step here only generates the echo example bindings, + # which the test harness consumes. examples/echo-schema/generate.sh } +function test_cmds { + # Single test command: the 4-language echo wire-compat matrix. + # Needs CPUS so the cargo + zig builds inside run_cross_language_tests.sh + # have headroom, and TIMEOUT for the cold-cache first run. + echo "$hash:CPUS=4:TIMEOUT=600s ipc-codegen/examples/scripts/run_cross_language_tests.sh" +} + function test { - echo_header "codegen test" - examples/scripts/run_cross_language_tests.sh + echo_header "ipc-codegen test" + test_cmds | filter_test_cmds | parallelize } case "$cmd" in - ""|generate) - generate - ;; - test) - test + "") + build ;; - hash) - echo $hash + "hash") + echo "$hash" ;; *) default_cmd_handler "$@" diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp deleted file mode 100644 index c12d0571d2f8..000000000000 --- a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.cpp +++ /dev/null @@ -1,88 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT - -#include "echo/echo_ipc_client.hpp" -#include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/serialize/msgpack_impl.hpp" - -#include -#include - -namespace echo { - -EchoIpcClient::EchoIpcClient(const std::string &socket_path) - : client_(std::make_unique<::ipc::IpcClient>(socket_path.c_str())) {} - -EchoIpcClient::~EchoIpcClient() = default; - -template -Resp EchoIpcClient::send(Cmd &&cmd) const { - // Serialize as [[CommandName, {payload}]] - msgpack::sbuffer send_buffer; - msgpack::packer pk(send_buffer); - pk.pack_array(1); - pk.pack_array(2); - pk.pack(std::string(Cmd::MSGPACK_SCHEMA_NAME)); - pk.pack(std::forward(cmd)); - - // Send request, receive response - std::vector request_bytes(send_buffer.data(), - send_buffer.data() + send_buffer.size()); - auto response_bytes = client_->call(request_bytes); - - if (response_bytes.empty()) { - throw std::runtime_error("Empty response from server"); - } - - // Parse response: [ResponseName, {payload}] - auto unpacked = - msgpack::unpack(reinterpret_cast(response_bytes.data()), - response_bytes.size()); - auto obj = unpacked.get(); - - if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 2 || - obj.via.array.ptr[0].type != msgpack::type::STR) { - throw std::runtime_error("Invalid response format from server"); - } - - std::string resp_name(obj.via.array.ptr[0].via.str.ptr, - obj.via.array.ptr[0].via.str.size); - if (resp_name == "EchoErrorResponse") { - std::string message; - auto &payload = obj.via.array.ptr[1]; - // Extract message field from the error map - if (payload.type == msgpack::type::MAP) { - for (uint32_t i = 0; i < payload.via.map.size; ++i) { - auto &kv = payload.via.map.ptr[i]; - if (kv.key.type == msgpack::type::STR) { - std::string key(kv.key.via.str.ptr, kv.key.via.str.size); - if (key == "message" && kv.val.type == msgpack::type::STR) { - message = std::string(kv.val.via.str.ptr, kv.val.via.str.size); - } - } - } - } - throw std::runtime_error("Server error: " + message); - } - - Resp result; - obj.via.array.ptr[1].convert(result); - return result; -} - -EchoBytesResponse EchoIpcClient::bytes(EchoBytes cmd) const { - return send(std::move(cmd)); -} - -EchoFieldsResponse EchoIpcClient::fields(EchoFields cmd) const { - return send(std::move(cmd)); -} - -EchoNestedResponse EchoIpcClient::nested(EchoNested cmd) const { - return send(std::move(cmd)); -} - -void EchoIpcClient::shutdown() { - send(EchoShutdown{}); -} - -} // namespace echo diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp deleted file mode 100644 index 28d680093425..000000000000 --- a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_client.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT -#pragma once - -#include "echo/echo_types.hpp" -#include "echo/ipc_client.hpp" -// clang-format on - -#include -#include - -namespace echo { -using namespace wire; - -/** Schema version hash for compatibility checking */ -static constexpr const char SCHEMA_HASH[] = - "bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc"; - -/** - * @brief Auto-generated IPC client. - * - * Each method sends a msgpack-serialized command to the server over UDS - * and returns the typed response. All methods block until the response arrives. - */ -class EchoIpcClient { -public: - explicit EchoIpcClient(const std::string &socket_path); - ~EchoIpcClient(); - - EchoIpcClient(const EchoIpcClient &) = delete; - EchoIpcClient &operator=(const EchoIpcClient &) = delete; - - EchoBytesResponse bytes(EchoBytes cmd) const; - EchoFieldsResponse fields(EchoFields cmd) const; - EchoNestedResponse nested(EchoNested cmd) const; - void shutdown(); - -private: - template Resp send(Cmd &&cmd) const; - - mutable std::unique_ptr<::ipc::IpcClient> client_; -}; - -} // namespace echo diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp b/ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp deleted file mode 100644 index d24464543fb9..000000000000 --- a/ipc-codegen/examples/cpp/echo/generated/echo_ipc_server.hpp +++ /dev/null @@ -1,165 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT -// Header-only server dispatch — template for service context. -#pragma once - -#include "echo_types.hpp" -#include "ipc_server.hpp" - -// msgpack headers needed for dispatch implementation. -// Includers within barretenberg must include try_catch_shim.hpp before this -// header. -#ifndef THROW -#define THROW throw -#define RETHROW throw -#endif -#include - -#include -#include -#include -#include -#include -#include - -namespace echo { - -// Wire types are in the 'wire' sub-namespace (from echo_types.hpp) -// Handler declarations — implement these in your handler file. -// Template specializations must be visible before make_handler() is -// instantiated. - -template -wire::EchoBytesResponse handle_bytes(Ctx &ctx, wire::EchoBytes &&cmd); - -template -wire::EchoFieldsResponse handle_fields(Ctx &ctx, wire::EchoFields &&cmd); - -template -wire::EchoNestedResponse handle_nested(Ctx &ctx, wire::EchoNested &&cmd); - -// --------------------------------------------------------------------------- -// Dispatch — template on service context type -// --------------------------------------------------------------------------- - -namespace detail { - -inline std::vector make_error(const std::string &message) { - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(2); - pk.pack(std::string("EchoErrorResponse")); - pk.pack_map(1); - pk.pack(std::string("message")); - pk.pack(message); - return std::vector(buf.data(), buf.data() + buf.size()); -} - -} // namespace detail - -template ::ipc::Handler make_echo_handler(Ctx &ctx) { - using HandlerFn = - std::function(Ctx &, const msgpack::object &)>; - static const std::unordered_map table = { - {"EchoBytes", - [](Ctx &ctx, [[maybe_unused]] const msgpack::object &payload) - -> std::vector { - wire::EchoBytes wire_cmd; - payload.convert(wire_cmd); - auto wire_resp = handle_bytes(ctx, std::move(wire_cmd)); - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(2); - pk.pack(std::string("EchoBytesResponse")); - pk.pack(wire_resp); - return std::vector(buf.data(), buf.data() + buf.size()); - }}, - {"EchoFields", - [](Ctx &ctx, [[maybe_unused]] const msgpack::object &payload) - -> std::vector { - wire::EchoFields wire_cmd; - payload.convert(wire_cmd); - auto wire_resp = handle_fields(ctx, std::move(wire_cmd)); - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(2); - pk.pack(std::string("EchoFieldsResponse")); - pk.pack(wire_resp); - return std::vector(buf.data(), buf.data() + buf.size()); - }}, - {"EchoNested", - [](Ctx &ctx, [[maybe_unused]] const msgpack::object &payload) - -> std::vector { - wire::EchoNested wire_cmd; - payload.convert(wire_cmd); - auto wire_resp = handle_nested(ctx, std::move(wire_cmd)); - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(2); - pk.pack(std::string("EchoNestedResponse")); - pk.pack(wire_resp); - return std::vector(buf.data(), buf.data() + buf.size()); - }}, - {"EchoShutdown", - []([[maybe_unused]] Ctx &ctx, - [[maybe_unused]] const msgpack::object &payload) - -> std::vector { - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(2); - pk.pack(std::string("EchoShutdownResponse")); - pk.pack_map(0); - THROW ::ipc::ShutdownRequested( - std::vector(buf.data(), buf.data() + buf.size())); - }}, - }; - - return - [&ctx](const std::vector &raw_request) -> std::vector { - auto unpacked = - msgpack::unpack(reinterpret_cast(raw_request.data()), - raw_request.size()); - auto obj = unpacked.get(); - - if (obj.type != msgpack::type::ARRAY || obj.via.array.size != 1) { - std::cerr << "Error: Expected array of size 1\n"; - return {}; - } - - auto &inner = obj.via.array.ptr[0]; - if (inner.type != msgpack::type::ARRAY || inner.via.array.size != 2 || - inner.via.array.ptr[0].type != msgpack::type::STR) { - std::cerr << "Error: Expected [CommandName, {payload}]\n"; - return {}; - } - - std::string cmd_name(inner.via.array.ptr[0].via.str.ptr, - inner.via.array.ptr[0].via.str.size); - auto &cmd_payload = inner.via.array.ptr[1]; - - auto it = table.find(cmd_name); - if (it == table.end()) { - return detail::make_error("unknown command: " + cmd_name); - } -#ifdef BB_NO_EXCEPTIONS - return it->second(ctx, cmd_payload); -#else - try { - return it->second(ctx, cmd_payload); - } catch (const ::ipc::ShutdownRequested &) { - throw; - } catch (const std::exception &e) { - std::cerr << "Error processing " << cmd_name << ": " << e.what() - << '\n'; - return detail::make_error(e.what()); - } -#endif - }; -} - -template -void serve(const char *socket_path, Ctx &ctx, - std::atomic *shutdown_flag = nullptr) { - ::ipc::serve(socket_path, make_echo_handler(ctx), shutdown_flag); -} - -} // namespace echo diff --git a/ipc-codegen/examples/cpp/echo/generated/echo_types.hpp b/ipc-codegen/examples/cpp/echo/generated/echo_types.hpp deleted file mode 100644 index 6e67fd1218a9..000000000000 --- a/ipc-codegen/examples/cpp/echo/generated/echo_types.hpp +++ /dev/null @@ -1,134 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT -// Standalone types for Echo service. -#pragma once - -#include -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Self-contained serialization macro. -// Defines a msgpack() method that enumerates field name/value pairs. -// Works with msgpack packers (serialization) and schema reflectors. -// If barretenberg's SERIALIZATION_FIELDS is already available, use it instead. -// --------------------------------------------------------------------------- -#ifndef SERIALIZATION_FIELDS -#define _SF_E1(x) #x, x -#define _SF_E2(x, ...) #x, x, _SF_E1(__VA_ARGS__) -#define _SF_E3(x, ...) #x, x, _SF_E2(__VA_ARGS__) -#define _SF_E4(x, ...) #x, x, _SF_E3(__VA_ARGS__) -#define _SF_E5(x, ...) #x, x, _SF_E4(__VA_ARGS__) -#define _SF_E6(x, ...) #x, x, _SF_E5(__VA_ARGS__) -#define _SF_E7(x, ...) #x, x, _SF_E6(__VA_ARGS__) -#define _SF_E8(x, ...) #x, x, _SF_E7(__VA_ARGS__) -#define _SF_E9(x, ...) #x, x, _SF_E8(__VA_ARGS__) -#define _SF_E10(x, ...) #x, x, _SF_E9(__VA_ARGS__) -#define _SF_E11(x, ...) #x, x, _SF_E10(__VA_ARGS__) -#define _SF_E12(x, ...) #x, x, _SF_E11(__VA_ARGS__) -#define _SF_E13(x, ...) #x, x, _SF_E12(__VA_ARGS__) -#define _SF_E14(x, ...) #x, x, _SF_E13(__VA_ARGS__) -#define _SF_E15(x, ...) #x, x, _SF_E14(__VA_ARGS__) -#define _SF_E16(x, ...) #x, x, _SF_E15(__VA_ARGS__) -#define _SF_E17(x, ...) #x, x, _SF_E16(__VA_ARGS__) -#define _SF_E18(x, ...) #x, x, _SF_E17(__VA_ARGS__) -#define _SF_E19(x, ...) #x, x, _SF_E18(__VA_ARGS__) -#define _SF_E20(x, ...) #x, x, _SF_E19(__VA_ARGS__) -#define _SF_CNT(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, \ - _15, _16, _17, _18, _19, _20, N, ...) \ - N -#define _SF_NUM(...) \ - _SF_CNT(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, \ - 5, 4, 3, 2, 1) -#define _SF_CAT(a, b) a##b -#define _SF_SEL(n) _SF_CAT(_SF_E, n) -#define _SF_NVP(...) _SF_SEL(_SF_NUM(__VA_ARGS__))(__VA_ARGS__) -#define SERIALIZATION_FIELDS(...) \ - template void msgpack(_PackFn pack_fn) { \ - pack_fn(_SF_NVP(__VA_ARGS__)); \ - } -#endif - -/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. -using Fr = std::array; - -namespace echo::wire { - -struct EchoInner { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoInner"; - std::vector> values; - std::optional flag; - SERIALIZATION_FIELDS(values, flag) - bool operator==(const EchoInner &) const = default; -}; - -struct EchoBytes { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoBytes"; - std::vector data; - SERIALIZATION_FIELDS(data) - bool operator==(const EchoBytes &) const = default; -}; - -struct EchoFields { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoFields"; - uint32_t a; - uint64_t b; - std::string name; - SERIALIZATION_FIELDS(a, b, name) - bool operator==(const EchoFields &) const = default; -}; - -struct EchoNested { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoNested"; - EchoInner inner; - SERIALIZATION_FIELDS(inner) - bool operator==(const EchoNested &) const = default; -}; - -struct EchoShutdown { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoShutdown"; - - template void msgpack(_PackFn &&pack_fn) { pack_fn(); } - bool operator==(const EchoShutdown &) const = default; -}; - -struct EchoBytesResponse { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoBytesResponse"; - std::vector data; - SERIALIZATION_FIELDS(data) - bool operator==(const EchoBytesResponse &) const = default; -}; - -struct EchoFieldsResponse { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoFieldsResponse"; - uint32_t a; - uint64_t b; - std::string name; - SERIALIZATION_FIELDS(a, b, name) - bool operator==(const EchoFieldsResponse &) const = default; -}; - -struct EchoNestedResponse { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoNestedResponse"; - EchoInner inner; - SERIALIZATION_FIELDS(inner) - bool operator==(const EchoNestedResponse &) const = default; -}; - -struct EchoShutdownResponse { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoShutdownResponse"; - - template void msgpack(_PackFn &&pack_fn) { pack_fn(); } - bool operator==(const EchoShutdownResponse &) const = default; -}; - -struct EchoErrorResponse { - static constexpr const char MSGPACK_SCHEMA_NAME[] = "EchoErrorResponse"; - std::string message; - SERIALIZATION_FIELDS(message) - bool operator==(const EchoErrorResponse &) const = default; -}; - -} // namespace echo::wire diff --git a/ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp b/ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp deleted file mode 100644 index 4e3db6c204c6..000000000000 --- a/ipc-codegen/examples/cpp/echo/generated/ipc_client.hpp +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Generic IPC client over Unix Domain Sockets. - * Handles: socket connect, length-prefixed framing, send/receive raw bytes. - * Header-only. - */ -#pragma once -#ifndef IPC_CLIENT_HPP_INCLUDED -#define IPC_CLIENT_HPP_INCLUDED - -#ifndef THROW -#define THROW throw -#endif -#ifndef RETHROW -#define RETHROW throw -#endif - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace ipc { - -class IpcClient { - public: - explicit IpcClient(const char* socket_path) - { - fd_ = ::socket(AF_UNIX, SOCK_STREAM, 0); - if (fd_ < 0) { - throw std::runtime_error(std::string("socket() failed: ") + strerror(errno)); - } - struct sockaddr_un addr {}; - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); - if (::connect(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { - ::close(fd_); - fd_ = -1; - throw std::runtime_error(std::string("connect() failed: ") + strerror(errno)); - } - } - - ~IpcClient() - { - if (fd_ >= 0) { - ::close(fd_); - } - } - - IpcClient(const IpcClient&) = delete; - IpcClient& operator=(const IpcClient&) = delete; - - std::vector call(const std::vector& request) - { - // Send length-prefixed request - uint32_t len = static_cast(request.size()); - uint8_t header[4] = { - static_cast(len & 0xFF), - static_cast((len >> 8) & 0xFF), - static_cast((len >> 16) & 0xFF), - static_cast((len >> 24) & 0xFF), - }; - write_all(header, 4); - write_all(request.data(), request.size()); - - // Receive length-prefixed response - uint8_t resp_hdr[4]; - read_all(resp_hdr, 4); - uint32_t resp_len = static_cast(resp_hdr[0]) | (static_cast(resp_hdr[1]) << 8) | - (static_cast(resp_hdr[2]) << 16) | (static_cast(resp_hdr[3]) << 24); - std::vector resp(resp_len); - read_all(resp.data(), resp_len); - return resp; - } - - private: - void write_all(const void* data, size_t len) - { - const auto* ptr = static_cast(data); - size_t written = 0; - while (written < len) { - auto n = ::write(fd_, ptr + written, len - written); - if (n <= 0) { - throw std::runtime_error("write failed"); - } - written += static_cast(n); - } - } - - void read_all(void* data, size_t len) - { - auto* ptr = static_cast(data); - size_t got = 0; - while (got < len) { - auto n = ::read(fd_, ptr + got, len - got); - if (n <= 0) { - throw std::runtime_error("read failed"); - } - got += static_cast(n); - } - } - - int fd_ = -1; -}; - -} // namespace ipc -#endif // IPC_CLIENT_HPP_INCLUDED diff --git a/ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp b/ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp deleted file mode 100644 index d57ad50e7b64..000000000000 --- a/ipc-codegen/examples/cpp/echo/generated/ipc_server.hpp +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Generic IPC server over Unix Domain Sockets. - * Handles: socket setup, multi-client accept via poll(), length-prefixed framing. - * Service-specific dispatch is injected via the handler function parameter. - * - * Does NOT handle signal handling or parent death monitoring — those are - * the responsibility of the binary that calls serve(). - * - * Header-only — no separate .cpp needed. - */ -#pragma once -#ifndef IPC_SERVER_HPP_INCLUDED -#define IPC_SERVER_HPP_INCLUDED - -#include -#include -#include -#include -#include -#include - -#if defined(__wasm__) -// UDS not available in WASM — provide stub types only -namespace ipc { -struct ShutdownRequested : std::exception { - std::vector final_response; - explicit ShutdownRequested(std::vector resp) : final_response(std::move(resp)) {} - const char* what() const noexcept override { return "shutdown requested"; } -}; -using Handler = std::function(const std::vector&)>; -inline void serve(const char*, Handler, std::atomic* = nullptr, int = 5) {} -} // namespace ipc -#else - -#ifndef THROW -#define THROW throw -#endif -#ifndef RETHROW -#define RETHROW throw -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace ipc { - -/// Exception thrown by handlers to trigger graceful shutdown. -/// Carries the final response to send before closing. -struct ShutdownRequested : std::exception { - std::vector final_response; - explicit ShutdownRequested(std::vector resp) - : final_response(std::move(resp)) - {} - const char* what() const noexcept override { return "shutdown requested"; } -}; - -using Handler = std::function(const std::vector&)>; - -// --------------------------------------------------------------------------- -// Framing: 4-byte little-endian length prefix -// --------------------------------------------------------------------------- - -inline bool send_frame(int fd, const std::vector& data) -{ - uint32_t len = static_cast(data.size()); - uint8_t header[4] = { - static_cast(len & 0xFF), - static_cast((len >> 8) & 0xFF), - static_cast((len >> 16) & 0xFF), - static_cast((len >> 24) & 0xFF), - }; - // Write header - size_t written = 0; - while (written < 4) { - auto n = ::write(fd, header + written, 4 - written); - if (n <= 0) { - return false; - } - written += static_cast(n); - } - // Write payload - written = 0; - while (written < data.size()) { - auto n = ::write(fd, data.data() + written, data.size() - written); - if (n <= 0) { - return false; - } - written += static_cast(n); - } - return true; -} - -/// Returns empty vector on EOF/error. -inline std::vector recv_frame(int fd) -{ - uint8_t header[4]; - size_t got = 0; - while (got < 4) { - auto n = ::read(fd, header + got, 4 - got); - if (n <= 0) { - return {}; - } - got += static_cast(n); - } - uint32_t len = static_cast(header[0]) | (static_cast(header[1]) << 8) | - (static_cast(header[2]) << 16) | (static_cast(header[3]) << 24); - std::vector buf(len); - got = 0; - while (got < len) { - auto n = ::read(fd, buf.data() + got, len - got); - if (n <= 0) { - return {}; - } - got += static_cast(n); - } - return buf; -} - -// --------------------------------------------------------------------------- -// Multi-client UDS server -// --------------------------------------------------------------------------- - -/** - * @brief Run a multi-client UDS server. - * - * Accepts multiple client connections via poll(). Handles one request at a time - * (sequential, not concurrent). When a handler throws ShutdownRequested, the - * final response is sent and the server exits cleanly. - * - * The caller should set up signal handlers and parent death monitoring before - * calling this function. To request external shutdown, store true into the - * provided shutdown_flag (or pass nullptr to disable external shutdown). - * - * @param socket_path Path for the Unix domain socket - * @param handler Function that processes a request and returns a response - * @param shutdown_flag Atomic flag checked each poll cycle; serve() exits when true. - * May be nullptr if only ShutdownRequested is used. - * @param backlog listen() backlog (max pending connections) - */ -inline void serve(const char* socket_path, - Handler handler, - std::atomic* shutdown_flag = nullptr, - int backlog = 5) -{ - unlink(socket_path); - int server_fd = ::socket(AF_UNIX, SOCK_STREAM, 0); - if (server_fd < 0) { - throw std::runtime_error(std::string("socket() failed: ") + strerror(errno)); - } - - struct sockaddr_un addr {}; - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); - - if (::bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { - ::close(server_fd); - throw std::runtime_error(std::string("bind() failed: ") + strerror(errno)); - } - if (::listen(server_fd, backlog) < 0) { - ::close(server_fd); - throw std::runtime_error(std::string("listen() failed: ") + strerror(errno)); - } - - // Poll set: [0] = server_fd, [1..N] = client fds - std::vector fds; - fds.push_back({ server_fd, POLLIN, 0 }); - - auto remove_client = [&](size_t idx) { - ::close(fds[idx].fd); - fds.erase(fds.begin() + static_cast(idx)); - }; - - auto should_shutdown = [&]() { - return shutdown_flag != nullptr && shutdown_flag->load(std::memory_order_acquire); - }; - - while (!should_shutdown()) { - int ready = ::poll(fds.data(), static_cast(fds.size()), 100 /* ms */); - if (ready < 0) { - if (errno == EINTR) { - continue; - } - break; - } - if (ready == 0) { - continue; - } - - // Check server fd for new connections - if (fds[0].revents & POLLIN) { - int client_fd = ::accept(server_fd, nullptr, nullptr); - if (client_fd >= 0) { - fds.push_back({ client_fd, POLLIN, 0 }); - } - } - - // Check client fds for data - for (size_t i = 1; i < fds.size(); /* incremented below */) { - if (!(fds[i].revents & POLLIN)) { - ++i; - continue; - } - - auto payload = recv_frame(fds[i].fd); - if (payload.empty()) { - // Client disconnected - remove_client(i); - continue; - } - - try { - auto response = handler(payload); - if (!send_frame(fds[i].fd, response)) { - remove_client(i); - continue; - } - } catch (const ShutdownRequested& shutdown) { - send_frame(fds[i].fd, shutdown.final_response); - goto done; - } catch (const std::exception& e) { - std::cerr << "ipc-server: handler error: " << e.what() << "\n"; - remove_client(i); - continue; - } - ++i; - } - } - -done: - // Close all client connections - for (size_t i = 1; i < fds.size(); ++i) { - ::close(fds[i].fd); - } - ::close(server_fd); - unlink(socket_path); -} - -} // namespace ipc -#endif // !defined(__wasm__) -#endif // IPC_SERVER_HPP_INCLUDED diff --git a/ipc-codegen/examples/rust/echo/src/generated/echo_client.rs b/ipc-codegen/examples/rust/echo/src/generated/echo_client.rs deleted file mode 100644 index 8118e5557651..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/echo_client.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! AUTOGENERATED - DO NOT EDIT -//! Echo IPC client API - -use super::backend::Backend; -use super::error::{BarretenbergError, Result}; -use super::echo_types::*; - -/// Echo IPC client API -pub struct EchoApi { - backend: B, -} - -impl EchoApi { - /// Create API with custom backend - pub fn new(backend: B) -> Self { - Self { backend } - } - - fn execute(&mut self, command: Command) -> Result { - let input_buffer = rmp_serde::to_vec_named(&vec![command]) - .map_err(|e| BarretenbergError::Serialization(e.to_string()))?; - - let output_buffer = self.backend.call(&input_buffer)?; - - let response: Response = rmp_serde::from_slice(&output_buffer) - .map_err(|e| BarretenbergError::Deserialization(e.to_string()))?; - - Ok(response) - } - - /// Execute EchoBytes - pub fn bytes(&mut self, data: &[u8]) -> Result { - let cmd = Command::EchoBytes(EchoBytes::new(data.to_vec())); - match self.execute(cmd)? { - Response::EchoBytesResponse(resp) => Ok(resp), - Response::EchoErrorResponse(err) => Err(BarretenbergError::Backend( - err.message - )), - _ => Err(BarretenbergError::InvalidResponse( - "Expected EchoBytesResponse".to_string() - )), - } - } - - /// Execute EchoFields - pub fn fields(&mut self, a: u32, b: u64, name: String) -> Result { - let cmd = Command::EchoFields(EchoFields::new(a, b, name)); - match self.execute(cmd)? { - Response::EchoFieldsResponse(resp) => Ok(resp), - Response::EchoErrorResponse(err) => Err(BarretenbergError::Backend( - err.message - )), - _ => Err(BarretenbergError::InvalidResponse( - "Expected EchoFieldsResponse".to_string() - )), - } - } - - /// Execute EchoNested - pub fn nested(&mut self, inner: EchoInner) -> Result { - let cmd = Command::EchoNested(EchoNested::new(inner)); - match self.execute(cmd)? { - Response::EchoNestedResponse(resp) => Ok(resp), - Response::EchoErrorResponse(err) => Err(BarretenbergError::Backend( - err.message - )), - _ => Err(BarretenbergError::InvalidResponse( - "Expected EchoNestedResponse".to_string() - )), - } - } - - /// Shutdown backend gracefully - pub fn shutdown(&mut self) -> Result<()> { - let cmd = Command::EchoShutdown(EchoShutdown::new()); - let _ = self.execute(cmd)?; - self.backend.destroy() - } - - /// Destroy backend without shutdown command - pub fn destroy(&mut self) -> Result<()> { - self.backend.destroy() - } -} diff --git a/ipc-codegen/examples/rust/echo/src/generated/echo_server.rs b/ipc-codegen/examples/rust/echo/src/generated/echo_server.rs deleted file mode 100644 index 27b843f8bb55..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/echo_server.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! AUTOGENERATED - DO NOT EDIT -//! Server-side dispatch for Echo IPC protocol - -use super::error::{BarretenbergError, Result}; -use super::echo_types::*; - -/// Handler trait — implement this to serve Echo commands. -pub trait Handler { - fn bytes(&mut self, cmd: EchoBytes) -> Result; - fn fields(&mut self, cmd: EchoFields) -> Result; - fn nested(&mut self, cmd: EchoNested) -> Result; -} - -/// Dispatch a single command to the handler and return the response. -pub fn dispatch(handler: &mut dyn Handler, command: Command) -> Result { - let response = match command { - Command::EchoBytes(cmd) => { - match handler.bytes(cmd) { - Ok(resp) => Response::EchoBytesResponse(resp), - Err(e) => Response::EchoErrorResponse(EchoErrorResponse { message: e.to_string() }), - } - } - Command::EchoFields(cmd) => { - match handler.fields(cmd) { - Ok(resp) => Response::EchoFieldsResponse(resp), - Err(e) => Response::EchoErrorResponse(EchoErrorResponse { message: e.to_string() }), - } - } - Command::EchoNested(cmd) => { - match handler.nested(cmd) { - Ok(resp) => Response::EchoNestedResponse(resp), - Err(e) => Response::EchoErrorResponse(EchoErrorResponse { message: e.to_string() }), - } - } - Command::EchoShutdown(_) => { - return Err(BarretenbergError::Backend("shutdown requested".to_string())); - } - }; - Ok(response) -} diff --git a/ipc-codegen/examples/rust/echo/src/generated/echo_types.rs b/ipc-codegen/examples/rust/echo/src/generated/echo_types.rs deleted file mode 100644 index 72530f1e7ab4..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/echo_types.rs +++ /dev/null @@ -1,401 +0,0 @@ -//! AUTOGENERATED - DO NOT EDIT -//! Generated types for Echo IPC protocol - -use serde::{Deserialize, Serialize}; - -/// Schema version hash for compatibility checking -pub const SCHEMA_HASH: &str = "bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc"; - -/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated, no heap. -/// Serializes as msgpack bin32 on the wire. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Fr(pub [u8; 32]); - -impl Fr { - pub fn from_bytes(bytes: [u8; 32]) -> Self { Self(bytes) } - pub fn to_bytes(&self) -> &[u8; 32] { &self.0 } - pub fn as_slice(&self) -> &[u8] { &self.0 } -} - -impl Serialize for Fr { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - serializer.serialize_bytes(&self.0) - } -} - -impl<'de> Deserialize<'de> for Fr { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - let bytes: Vec = >::deserialize(deserializer)?; - let arr: [u8; 32] = bytes.try_into() - .map_err(|v: Vec| serde::de::Error::invalid_length(v.len(), &"32 bytes"))?; - Ok(Fr(arr)) - } -} - -mod serde_bytes { - use serde::{Deserialize, Deserializer, Serializer}; - pub fn serialize(bytes: &Vec, serializer: S) -> Result - where S: Serializer { serializer.serialize_bytes(bytes) } - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where D: Deserializer<'de> { >::deserialize(deserializer) } -} - -mod serde_vec_bytes { - use serde::{Deserialize, Deserializer, Serializer, Serialize}; - use serde::ser::SerializeSeq; - use serde::de::{SeqAccess, Visitor}; - - #[derive(Serialize, Deserialize)] - struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); - - pub fn serialize(vec: &Vec>, serializer: S) -> Result - where S: Serializer { - let mut seq = serializer.serialize_seq(Some(vec.len()))?; - for bytes in vec { - seq.serialize_element(&BytesWrapper(bytes.clone()))?; - } - seq.end() - } - pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> - where D: Deserializer<'de> { - struct VecVecU8Visitor; - impl<'de> Visitor<'de> for VecVecU8Visitor { - type Value = Vec>; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a sequence of byte arrays") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let mut vec = Vec::new(); - while let Some(wrapper) = seq.next_element::()? { - vec.push(wrapper.0); - } - Ok(vec) - } - } - deserializer.deserialize_seq(VecVecU8Visitor) - } -} - -mod serde_array4_bytes { - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use serde::ser::SerializeTuple; - use serde::de::{SeqAccess, Visitor}; - - #[derive(Serialize, Deserialize)] - struct BytesWrapper(#[serde(with = "super::serde_bytes")] Vec); - - pub fn serialize(arr: &[Vec; 4], serializer: S) -> Result - where S: Serializer { - let mut tup = serializer.serialize_tuple(4)?; - for bytes in arr { - tup.serialize_element(&BytesWrapper(bytes.clone()))?; - } - tup.end() - } - pub fn deserialize<'de, D>(deserializer: D) -> Result<[Vec; 4], D::Error> - where D: Deserializer<'de> { - struct Array4Visitor; - impl<'de> Visitor<'de> for Array4Visitor { - type Value = [Vec; 4]; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("an array of 4 byte arrays") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let mut arr: [Vec; 4] = Default::default(); - for (i, item) in arr.iter_mut().enumerate() { - *item = seq.next_element::()? - .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?.0; - } - Ok(arr) - } - } - deserializer.deserialize_tuple(4, Array4Visitor) - } -} - -/// EchoInner -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoInner { - #[serde(with = "serde_vec_bytes")] - pub values: Vec>, - pub flag: Option, -} - -/// EchoBytes -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoBytes { - #[serde(rename = "__typename", skip, default)] - pub type_name: String, - #[serde(with = "serde_bytes")] - pub data: Vec, -} - -impl EchoBytes { - pub fn new(data: Vec) -> Self { - Self { - type_name: "EchoBytes".to_string(), - data, - } - } -} - -/// EchoFields -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoFields { - #[serde(rename = "__typename", skip, default)] - pub type_name: String, - pub a: u32, - pub b: u64, - pub name: String, -} - -impl EchoFields { - pub fn new(a: u32, b: u64, name: String) -> Self { - Self { - type_name: "EchoFields".to_string(), - a, - b, - name, - } - } -} - -/// EchoNested -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoNested { - #[serde(rename = "__typename", skip, default)] - pub type_name: String, - pub inner: EchoInner, -} - -impl EchoNested { - pub fn new(inner: EchoInner) -> Self { - Self { - type_name: "EchoNested".to_string(), - inner, - } - } -} - -/// EchoShutdown -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoShutdown { - #[serde(rename = "__typename", skip, default)] - pub type_name: String, - -} - -impl EchoShutdown { - pub fn new() -> Self { - Self { - type_name: "EchoShutdown".to_string(), - } - } -} - -/// EchoBytesResponse -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoBytesResponse { - #[serde(with = "serde_bytes")] - pub data: Vec, -} - -/// EchoFieldsResponse -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoFieldsResponse { - pub a: u32, - pub b: u64, - pub name: String, -} - -/// EchoNestedResponse -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoNestedResponse { - pub inner: EchoInner, -} - -/// EchoShutdownResponse -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoShutdownResponse { - -} - -/// EchoErrorResponse -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EchoErrorResponse { - pub message: String, -} - -/// Command enum - wraps all possible commands -#[derive(Debug, Clone)] -pub enum Command { - EchoBytes(EchoBytes), - EchoFields(EchoFields), - EchoNested(EchoNested), - EchoShutdown(EchoShutdown), -} - -impl Serialize for Command { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - use serde::ser::SerializeTuple; - let mut tuple = serializer.serialize_tuple(2)?; - match self { - Command::EchoBytes(data) => { - tuple.serialize_element("EchoBytes")?; - tuple.serialize_element(data)?; - } - Command::EchoFields(data) => { - tuple.serialize_element("EchoFields")?; - tuple.serialize_element(data)?; - } - Command::EchoNested(data) => { - tuple.serialize_element("EchoNested")?; - tuple.serialize_element(data)?; - } - Command::EchoShutdown(data) => { - tuple.serialize_element("EchoShutdown")?; - tuple.serialize_element(data)?; - } - } - tuple.end() - } -} - -impl<'de> Deserialize<'de> for Command { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - use serde::de::{SeqAccess, Visitor}; - struct CommandVisitor; - - impl<'de> Visitor<'de> for CommandVisitor { - type Value = Command; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a 2-element array [name, payload]") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let name: String = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - match name.as_str() { - "EchoBytes" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Command::EchoBytes(data)) - } - "EchoFields" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Command::EchoFields(data)) - } - "EchoNested" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Command::EchoNested(data)) - } - "EchoShutdown" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Command::EchoShutdown(data)) - } - _ => Err(serde::de::Error::unknown_variant(&name, &["EchoBytes", "EchoFields", "EchoNested", "EchoShutdown"])), - } - } - } - deserializer.deserialize_tuple(2, CommandVisitor) - } -} - -/// Response enum - wraps all possible responses -#[derive(Debug, Clone)] -pub enum Response { - EchoBytesResponse(EchoBytesResponse), - EchoFieldsResponse(EchoFieldsResponse), - EchoNestedResponse(EchoNestedResponse), - EchoShutdownResponse(EchoShutdownResponse), - EchoErrorResponse(EchoErrorResponse), -} - -impl Serialize for Response { - fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { - use serde::ser::SerializeTuple; - let mut tuple = serializer.serialize_tuple(2)?; - match self { - Response::EchoBytesResponse(data) => { - tuple.serialize_element("EchoBytesResponse")?; - tuple.serialize_element(data)?; - } - Response::EchoFieldsResponse(data) => { - tuple.serialize_element("EchoFieldsResponse")?; - tuple.serialize_element(data)?; - } - Response::EchoNestedResponse(data) => { - tuple.serialize_element("EchoNestedResponse")?; - tuple.serialize_element(data)?; - } - Response::EchoShutdownResponse(data) => { - tuple.serialize_element("EchoShutdownResponse")?; - tuple.serialize_element(data)?; - } - Response::EchoErrorResponse(data) => { - tuple.serialize_element("EchoErrorResponse")?; - tuple.serialize_element(data)?; - } - } - tuple.end() - } -} - -impl<'de> Deserialize<'de> for Response { - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - use serde::de::{SeqAccess, Visitor}; - struct ResponseVisitor; - - impl<'de> Visitor<'de> for ResponseVisitor { - type Value = Response; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a 2-element array [name, payload]") - } - fn visit_seq(self, mut seq: A) -> Result - where A: SeqAccess<'de> { - let name: String = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - match name.as_str() { - "EchoBytesResponse" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Response::EchoBytesResponse(data)) - } - "EchoFieldsResponse" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Response::EchoFieldsResponse(data)) - } - "EchoNestedResponse" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Response::EchoNestedResponse(data)) - } - "EchoShutdownResponse" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Response::EchoShutdownResponse(data)) - } - "EchoErrorResponse" => { - let data = seq.next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; - Ok(Response::EchoErrorResponse(data)) - } - _ => Err(serde::de::Error::unknown_variant(&name, &["EchoBytesResponse", "EchoFieldsResponse", "EchoNestedResponse", "EchoShutdownResponse", "EchoErrorResponse"])), - } - } - } - deserializer.deserialize_tuple(2, ResponseVisitor) - } -} diff --git a/ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs b/ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs deleted file mode 100644 index cfce19cbc26a..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/ipc_server.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Generic IPC server over Unix Domain Sockets. -//! Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. -//! Service-specific dispatch is injected via the dispatch function parameter. - -use std::io::{Read, Write}; -use std::os::unix::net::UnixListener; - -/// Dispatch function signature: takes raw command name + msgpack bytes, returns response name + bytes -pub type DispatchFn = Box Vec>; - -/// Run an IPC server. Accepts one connection, serves until disconnect or shutdown. -pub fn serve(socket_path: &str, handler: impl Fn(&[u8]) -> Vec) -> std::io::Result<()> { - let _ = std::fs::remove_file(socket_path); - let listener = UnixListener::bind(socket_path)?; - eprintln!("ipc-server(rust): listening on {}", socket_path); - - let (mut stream, _) = listener.accept()?; - - loop { - // Read 4-byte LE length - let mut len_buf = [0u8; 4]; - if stream.read_exact(&mut len_buf).is_err() { - break; - } - let len = u32::from_le_bytes(len_buf) as usize; - - // Read payload - let mut payload = vec![0u8; len]; - stream.read_exact(&mut payload)?; - - // Check for shutdown - let is_shutdown = payload.windows(8).any(|w| w == b"Shutdown"); - - // Dispatch - let response = handler(&payload); - - // Send length-prefixed response - let resp_len = (response.len() as u32).to_le_bytes(); - stream.write_all(&resp_len)?; - stream.write_all(&response)?; - stream.flush()?; - - if is_shutdown { - break; - } - } - - let _ = std::fs::remove_file(socket_path); - eprintln!("ipc-server(rust): shutdown"); - Ok(()) -} diff --git a/ipc-codegen/examples/ts/echo/generated/api_types.ts b/ipc-codegen/examples/ts/echo/generated/api_types.ts deleted file mode 100644 index c052de104568..000000000000 --- a/ipc-codegen/examples/ts/echo/generated/api_types.ts +++ /dev/null @@ -1,259 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT - -/** Schema version hash for compatibility checking */ -export const SCHEMA_HASH = 'bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc'; - -// Type aliases for primitive types -/** 32-byte field element (Fr/Fq). Branded Uint8Array — no arithmetic, just type safety. */ -export type Fr = Uint8Array; -export type Field2 = [Fr, Fr]; - -// Public interfaces (exported) -export interface EchoInner { - values: Uint8Array[]; - flag: boolean | null; -} - -export interface EchoBytes { - data: Uint8Array; -} - -export interface EchoFields { - a: number; - b: number; - name: string; -} - -export interface EchoNested { - inner: EchoInner; -} - -export interface EchoShutdown { - -} - -export interface EchoBytesResponse { - data: Uint8Array; -} - -export interface EchoFieldsResponse { - a: number; - b: number; - name: string; -} - -export interface EchoNestedResponse { - inner: EchoInner; -} - -export interface EchoShutdownResponse { - -} - -export interface EchoErrorResponse { - message: string; -} - -// Private Msgpack interfaces (not exported) -interface MsgpackEchoInner { - values: Uint8Array[]; - flag: boolean | null; -} - -interface MsgpackEchoBytes { - data: Uint8Array; -} - -interface MsgpackEchoFields { - a: number; - b: number; - name: string; -} - -interface MsgpackEchoNested { - inner: MsgpackEchoInner; -} - -interface MsgpackEchoShutdown { - -} - -interface MsgpackEchoBytesResponse { - data: Uint8Array; -} - -interface MsgpackEchoFieldsResponse { - a: number; - b: number; - name: string; -} - -interface MsgpackEchoNestedResponse { - inner: MsgpackEchoInner; -} - -interface MsgpackEchoShutdownResponse { - -} - -interface MsgpackEchoErrorResponse { - message: string; -} - -// Conversion functions (exported) -export function toEchoInner(o: MsgpackEchoInner): EchoInner { - if (o.values === undefined) { throw new Error("Expected values in EchoInner deserialization"); } - if (o.flag === undefined) { throw new Error("Expected flag in EchoInner deserialization"); }; - return { - values: o.values, - flag: o.flag, - }; -} - -export function toEchoBytes(o: MsgpackEchoBytes): EchoBytes { - if (o.data === undefined) { throw new Error("Expected data in EchoBytes deserialization"); }; - return { - data: o.data, - }; -} - -export function toEchoFields(o: MsgpackEchoFields): EchoFields { - if (o.a === undefined) { throw new Error("Expected a in EchoFields deserialization"); } - if (o.b === undefined) { throw new Error("Expected b in EchoFields deserialization"); } - if (o.name === undefined) { throw new Error("Expected name in EchoFields deserialization"); }; - return { - a: o.a, - b: o.b, - name: o.name, - }; -} - -export function toEchoNested(o: MsgpackEchoNested): EchoNested { - if (o.inner === undefined) { throw new Error("Expected inner in EchoNested deserialization"); }; - return { - inner: toEchoInner(o.inner), - }; -} - -export function toEchoShutdown(o: MsgpackEchoShutdown): EchoShutdown { - return {}; -} - -export function toEchoBytesResponse(o: MsgpackEchoBytesResponse): EchoBytesResponse { - if (o.data === undefined) { throw new Error("Expected data in EchoBytesResponse deserialization"); }; - return { - data: o.data, - }; -} - -export function toEchoFieldsResponse(o: MsgpackEchoFieldsResponse): EchoFieldsResponse { - if (o.a === undefined) { throw new Error("Expected a in EchoFieldsResponse deserialization"); } - if (o.b === undefined) { throw new Error("Expected b in EchoFieldsResponse deserialization"); } - if (o.name === undefined) { throw new Error("Expected name in EchoFieldsResponse deserialization"); }; - return { - a: o.a, - b: o.b, - name: o.name, - }; -} - -export function toEchoNestedResponse(o: MsgpackEchoNestedResponse): EchoNestedResponse { - if (o.inner === undefined) { throw new Error("Expected inner in EchoNestedResponse deserialization"); }; - return { - inner: toEchoInner(o.inner), - }; -} - -export function toEchoShutdownResponse(o: MsgpackEchoShutdownResponse): EchoShutdownResponse { - return {}; -} - -export function toEchoErrorResponse(o: MsgpackEchoErrorResponse): EchoErrorResponse { - if (o.message === undefined) { throw new Error("Expected message in EchoErrorResponse deserialization"); }; - return { - message: o.message, - }; -} - -export function fromEchoInner(o: EchoInner): MsgpackEchoInner { - if (o.values === undefined) { throw new Error("Expected values in EchoInner serialization"); } - if (o.flag === undefined) { throw new Error("Expected flag in EchoInner serialization"); }; - return { - values: o.values, - flag: o.flag, - }; -} - -export function fromEchoBytes(o: EchoBytes): MsgpackEchoBytes { - if (o.data === undefined) { throw new Error("Expected data in EchoBytes serialization"); }; - return { - data: o.data, - }; -} - -export function fromEchoFields(o: EchoFields): MsgpackEchoFields { - if (o.a === undefined) { throw new Error("Expected a in EchoFields serialization"); } - if (o.b === undefined) { throw new Error("Expected b in EchoFields serialization"); } - if (o.name === undefined) { throw new Error("Expected name in EchoFields serialization"); }; - return { - a: o.a, - b: o.b, - name: o.name, - }; -} - -export function fromEchoNested(o: EchoNested): MsgpackEchoNested { - if (o.inner === undefined) { throw new Error("Expected inner in EchoNested serialization"); }; - return { - inner: fromEchoInner(o.inner), - }; -} - -export function fromEchoShutdown(o: EchoShutdown): MsgpackEchoShutdown { - return {}; -} - -export function fromEchoBytesResponse(o: EchoBytesResponse): MsgpackEchoBytesResponse { - if (o.data === undefined) { throw new Error("Expected data in EchoBytesResponse serialization"); }; - return { - data: o.data, - }; -} - -export function fromEchoFieldsResponse(o: EchoFieldsResponse): MsgpackEchoFieldsResponse { - if (o.a === undefined) { throw new Error("Expected a in EchoFieldsResponse serialization"); } - if (o.b === undefined) { throw new Error("Expected b in EchoFieldsResponse serialization"); } - if (o.name === undefined) { throw new Error("Expected name in EchoFieldsResponse serialization"); }; - return { - a: o.a, - b: o.b, - name: o.name, - }; -} - -export function fromEchoNestedResponse(o: EchoNestedResponse): MsgpackEchoNestedResponse { - if (o.inner === undefined) { throw new Error("Expected inner in EchoNestedResponse serialization"); }; - return { - inner: fromEchoInner(o.inner), - }; -} - -export function fromEchoShutdownResponse(o: EchoShutdownResponse): MsgpackEchoShutdownResponse { - return {}; -} - -export function fromEchoErrorResponse(o: EchoErrorResponse): MsgpackEchoErrorResponse { - if (o.message === undefined) { throw new Error("Expected message in EchoErrorResponse serialization"); }; - return { - message: o.message, - }; -} - -// Base API interface -export interface BbApiBase { - echoBytes(command: EchoBytes): Promise; - echoFields(command: EchoFields): Promise; - echoNested(command: EchoNested): Promise; - echoShutdown(command: EchoShutdown): Promise; - destroy(): Promise; -} diff --git a/ipc-codegen/examples/ts/echo/generated/async.ts b/ipc-codegen/examples/ts/echo/generated/async.ts deleted file mode 100644 index a64717f9af5f..000000000000 --- a/ipc-codegen/examples/ts/echo/generated/async.ts +++ /dev/null @@ -1,72 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT - -import { IMsgpackBackendAsync } from '../../bb_backends/interface.js'; -import { Decoder, Encoder } from 'msgpackr'; -import { BBApiException } from '../../bbapi_exception.js'; -import { BbApiBase, EchoBytes, EchoBytesResponse, EchoFields, EchoFieldsResponse, EchoNested, EchoNestedResponse, EchoShutdown, EchoShutdownResponse, fromEchoBytes, fromEchoFields, fromEchoNested, fromEchoShutdown, toEchoBytesResponse, toEchoFieldsResponse, toEchoNestedResponse, toEchoShutdownResponse } from './api_types.js'; - -async function msgpackCall(backend: IMsgpackBackendAsync, input: any[]) { - const inputBuffer = new Encoder({ useRecords: false }).pack(input); - const encodedResult = await backend.call(inputBuffer); - return new Decoder({ useRecords: false }).unpack(encodedResult); -} - -export class AsyncApi implements BbApiBase { - constructor(protected backend: IMsgpackBackendAsync) {} - - echoBytes(command: EchoBytes): Promise { - const msgpackCommand = fromEchoBytes(command); - return msgpackCall(this.backend, [["EchoBytes", msgpackCommand]]).then(([variantName, result]: [string, any]) => { - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoBytesResponse') { - throw new BBApiException(`Expected variant name 'EchoBytesResponse' but got '${variantName}'`); - } - return toEchoBytesResponse(result); - }); - } - - echoFields(command: EchoFields): Promise { - const msgpackCommand = fromEchoFields(command); - return msgpackCall(this.backend, [["EchoFields", msgpackCommand]]).then(([variantName, result]: [string, any]) => { - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoFieldsResponse') { - throw new BBApiException(`Expected variant name 'EchoFieldsResponse' but got '${variantName}'`); - } - return toEchoFieldsResponse(result); - }); - } - - echoNested(command: EchoNested): Promise { - const msgpackCommand = fromEchoNested(command); - return msgpackCall(this.backend, [["EchoNested", msgpackCommand]]).then(([variantName, result]: [string, any]) => { - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoNestedResponse') { - throw new BBApiException(`Expected variant name 'EchoNestedResponse' but got '${variantName}'`); - } - return toEchoNestedResponse(result); - }); - } - - echoShutdown(command: EchoShutdown): Promise { - const msgpackCommand = fromEchoShutdown(command); - return msgpackCall(this.backend, [["EchoShutdown", msgpackCommand]]).then(([variantName, result]: [string, any]) => { - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoShutdownResponse') { - throw new BBApiException(`Expected variant name 'EchoShutdownResponse' but got '${variantName}'`); - } - return toEchoShutdownResponse(result); - }); - } - - destroy(): Promise { - return this.backend.destroy ? this.backend.destroy() : Promise.resolve(); - } -} diff --git a/ipc-codegen/examples/ts/echo/generated/ipc_client.ts b/ipc-codegen/examples/ts/echo/generated/ipc_client.ts deleted file mode 100644 index 1fbdf761b3b1..000000000000 --- a/ipc-codegen/examples/ts/echo/generated/ipc_client.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Generic IPC client over Unix Domain Sockets. - * Handles: socket connect, length-prefixed framing, msgpack encode/decode. - * Service-specific typed methods are in the generated wrapper. - */ -import * as net from 'node:net'; -import { Decoder, Encoder } from 'msgpackr'; - -const encoder = new Encoder({ useRecords: false }); -const decoder = new Decoder({ useRecords: false }); - -export class IpcClient { - private conn: net.Socket; - private buffer = Buffer.alloc(0); - - private constructor(conn: net.Socket) { - this.conn = conn; - } - - static async connect(socketPath: string): Promise { - const conn = net.createConnection(socketPath); - await new Promise((resolve, reject) => { - conn.on('connect', resolve); - conn.on('error', reject); - }); - return new IpcClient(conn); - } - - close() { - this.conn.destroy(); - } - - /** Send a command and receive the response. */ - async call(commandName: string, fields: any): Promise<[string, any]> { - const packed = encoder.pack([[commandName, fields]]); - const lenBuf = Buffer.alloc(4); - lenBuf.writeUInt32LE(packed.length, 0); - this.conn.write(lenBuf); - this.conn.write(packed); - - return new Promise((resolve, reject) => { - const onData = (data: Buffer) => { - this.buffer = Buffer.concat([this.buffer, data]); - if (this.buffer.length >= 4) { - const len = this.buffer.readUInt32LE(0); - if (this.buffer.length >= 4 + len) { - this.conn.removeListener('data', onData); - const payload = this.buffer.subarray(4, 4 + len); - this.buffer = this.buffer.subarray(4 + len); - resolve(decoder.unpack(payload) as [string, any]); - } - } - }; - this.conn.on('data', onData); - this.conn.on('error', reject); - }); - } -} diff --git a/ipc-codegen/examples/ts/echo/generated/ipc_server.ts b/ipc-codegen/examples/ts/echo/generated/ipc_server.ts deleted file mode 100644 index 3ef981ba2182..000000000000 --- a/ipc-codegen/examples/ts/echo/generated/ipc_server.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Generic IPC server over Unix Domain Sockets. - * Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. - * Service-specific dispatch is injected via the dispatchFn parameter. - */ -import * as net from 'node:net'; -import * as fs from 'node:fs'; -import { Decoder, Encoder } from 'msgpackr'; - -const encoder = new Encoder({ useRecords: false }); -const decoder = new Decoder({ useRecords: false }); - -export type DispatchFn = (commandName: string, payload: any) => Promise<[string, any]>; - -export function createServer(socketPath: string, dispatchFn: DispatchFn): { close: () => Promise } { - try { fs.unlinkSync(socketPath); } catch {} - - const server = net.createServer((conn) => { - let buffer = Buffer.alloc(0); - let responseChain: Promise = Promise.resolve(); - - conn.on('data', (data: Buffer) => { - buffer = Buffer.concat([buffer, data]); - - while (buffer.length >= 4) { - const len = buffer.readUInt32LE(0); - if (buffer.length < 4 + len) break; - - const payload = buffer.subarray(4, 4 + len); - buffer = buffer.subarray(4 + len); - - const request = decoder.unpack(payload) as any[]; - const [commandName, fields] = request[0] as [string, any]; - - if (commandName.endsWith('Shutdown')) { - sendResponse(conn, [commandName.replace(/^(.*)$/, '$1Response'), {}]); - setTimeout(() => { - server.close(); - try { fs.unlinkSync(socketPath); } catch {} - }, 50); - return; - } - - const prev = responseChain; - const result = dispatchFn(commandName, fields ?? {}); - responseChain = (async () => { - await prev; - try { - const [name, resp] = await result; - sendResponse(conn, [name, resp]); - } catch (err: any) { - sendResponse(conn, ['ErrorResponse', { message: err.message ?? 'Unknown error' }]); - } - })(); - void responseChain.catch(() => {}); - } - }); - - conn.on('error', () => {}); - }); - - server.listen(socketPath, () => { - console.error(`ipc-server(ts): listening on ${socketPath}`); - }); - - return { - close: () => new Promise(resolve => { - server.close(() => { - try { fs.unlinkSync(socketPath); } catch {} - resolve(); - }); - }), - }; -} - -function sendResponse(conn: net.Socket, response: [string, any]) { - const packed = encoder.pack(response); - const lenBuf = Buffer.alloc(4); - lenBuf.writeUInt32LE(packed.length, 0); - conn.write(lenBuf); - conn.write(packed); -} diff --git a/ipc-codegen/examples/ts/echo/generated/server.ts b/ipc-codegen/examples/ts/echo/generated/server.ts deleted file mode 100644 index 55f9c283140d..000000000000 --- a/ipc-codegen/examples/ts/echo/generated/server.ts +++ /dev/null @@ -1,41 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT -// Server-side dispatch for IPC protocol - -import { EchoBytes, EchoBytesResponse, EchoFields, EchoFieldsResponse, EchoNested, EchoNestedResponse, fromEchoBytesResponse, fromEchoFieldsResponse, fromEchoNestedResponse, toEchoBytes, toEchoFields, toEchoNested } from './api_types.js'; - -/** Handler interface — implement this to serve commands. */ -export interface Handler { - echoBytes(command: EchoBytes): Promise; - echoFields(command: EchoFields): Promise; - echoNested(command: EchoNested): Promise; -} - -/** - * Dispatch a [commandName, payload] pair to the handler. - * Returns [responseName, responsePayload] for serialization. - */ -export async function dispatch( - handler: Handler, - commandName: string, - payload: any, -): Promise<[string, any]> { - switch (commandName) { - case 'EchoBytes': { - const cmd = toEchoBytes(payload); - const result = await handler.echoBytes(cmd); - return ['EchoBytesResponse', fromEchoBytesResponse(result)]; - } - case 'EchoFields': { - const cmd = toEchoFields(payload); - const result = await handler.echoFields(cmd); - return ['EchoFieldsResponse', fromEchoFieldsResponse(result)]; - } - case 'EchoNested': { - const cmd = toEchoNested(payload); - const result = await handler.echoNested(cmd); - return ['EchoNestedResponse', fromEchoNestedResponse(result)]; - } - default: - throw new Error(`Unknown command: ${commandName}`); - } -} diff --git a/ipc-codegen/examples/ts/echo/generated/sync.ts b/ipc-codegen/examples/ts/echo/generated/sync.ts deleted file mode 100644 index dff2770bac54..000000000000 --- a/ipc-codegen/examples/ts/echo/generated/sync.ts +++ /dev/null @@ -1,68 +0,0 @@ -// AUTOGENERATED FILE - DO NOT EDIT - -import { IMsgpackBackendSync } from '../../bb_backends/interface.js'; -import { Decoder, Encoder } from 'msgpackr'; -import { BBApiException } from '../../bbapi_exception.js'; -import { BbApiBase, EchoBytes, EchoBytesResponse, EchoFields, EchoFieldsResponse, EchoNested, EchoNestedResponse, EchoShutdown, EchoShutdownResponse, fromEchoBytes, fromEchoFields, fromEchoNested, fromEchoShutdown, toEchoBytesResponse, toEchoFieldsResponse, toEchoNestedResponse, toEchoShutdownResponse } from './api_types.js'; - -function msgpackCall(backend: IMsgpackBackendSync, input: any[]) { - const inputBuffer = new Encoder({ useRecords: false }).pack(input); - const encodedResult = backend.call(inputBuffer); - return new Decoder({ useRecords: false }).unpack(encodedResult); -} - -export class SyncApi { - constructor(protected backend: IMsgpackBackendSync) {} - - echoBytes(command: EchoBytes): EchoBytesResponse { - const msgpackCommand = fromEchoBytes(command); - const [variantName, result] = msgpackCall(this.backend, [["EchoBytes", msgpackCommand]]); - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoBytesResponse') { - throw new BBApiException(`Expected variant name 'EchoBytesResponse' but got '${variantName}'`); - } - return toEchoBytesResponse(result); - } - - echoFields(command: EchoFields): EchoFieldsResponse { - const msgpackCommand = fromEchoFields(command); - const [variantName, result] = msgpackCall(this.backend, [["EchoFields", msgpackCommand]]); - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoFieldsResponse') { - throw new BBApiException(`Expected variant name 'EchoFieldsResponse' but got '${variantName}'`); - } - return toEchoFieldsResponse(result); - } - - echoNested(command: EchoNested): EchoNestedResponse { - const msgpackCommand = fromEchoNested(command); - const [variantName, result] = msgpackCall(this.backend, [["EchoNested", msgpackCommand]]); - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoNestedResponse') { - throw new BBApiException(`Expected variant name 'EchoNestedResponse' but got '${variantName}'`); - } - return toEchoNestedResponse(result); - } - - echoShutdown(command: EchoShutdown): EchoShutdownResponse { - const msgpackCommand = fromEchoShutdown(command); - const [variantName, result] = msgpackCall(this.backend, [["EchoShutdown", msgpackCommand]]); - if (variantName === 'EchoErrorResponse') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); - } - if (variantName !== 'EchoShutdownResponse') { - throw new BBApiException(`Expected variant name 'EchoShutdownResponse' but got '${variantName}'`); - } - return toEchoShutdownResponse(result); - } - - destroy(): void { - if (this.backend.destroy) this.backend.destroy(); - } -} diff --git a/ipc-codegen/examples/zig/echo/generated/echo_client.zig b/ipc-codegen/examples/zig/echo/generated/echo_client.zig deleted file mode 100644 index c06d06f9c33b..000000000000 --- a/ipc-codegen/examples/zig/echo/generated/echo_client.zig +++ /dev/null @@ -1,94 +0,0 @@ -//! AUTOGENERATED - DO NOT EDIT -//! Echo client — typed methods parameterized on a backend type. -//! -//! The backend must satisfy: call(self, request: []const u8) ![]u8 and destroy(self) void. -//! See backend.zig for the interface contract. -//! Implementations: UdsBackend (uds_backend.zig), FfiBackend (ffi_backend.zig). - -const std = @import("std"); -const msgpack = @import("msgpack"); -const Payload = msgpack.Payload; -const types = @import("echo_types.zig"); -const backend_mod = @import("backend.zig"); - -const alloc = std.heap.page_allocator; - -pub fn Client(comptime BackendType: type) type { - comptime backend_mod.assertBackend(BackendType); - - return struct { - const Self = @This(); - backend: *BackendType, - - pub fn init(backend: *BackendType) Self { - return .{ .backend = backend }; - } - - pub fn destroy(self: *Self) void { - self.backend.destroy(); - } - - pub fn shutdown(self: *Self) !void { - const request_bytes = try Self.encode("EchoShutdown", Payload.mapPayload(alloc)); - defer alloc.free(request_bytes); - const response_bytes = try self.backend.call(request_bytes); - alloc.free(response_bytes); - } - - pub fn bytes(self: *Self, cmd: types.EchoBytes) !types.EchoBytesResponse { - const request_bytes = try Self.encode("EchoBytes", try cmd.toPayload(alloc)); - defer alloc.free(request_bytes); - const response_bytes = try self.backend.call(request_bytes); - defer alloc.free(response_bytes); - const resp_name, const resp_payload = try Self.decode(response_bytes); - if (std.mem.eql(u8, resp_name, "EchoErrorResponse")) return error.ServerError; - return try types.EchoBytesResponse.fromPayload(resp_payload); - } - - pub fn fields(self: *Self, cmd: types.EchoFields) !types.EchoFieldsResponse { - const request_bytes = try Self.encode("EchoFields", try cmd.toPayload(alloc)); - defer alloc.free(request_bytes); - const response_bytes = try self.backend.call(request_bytes); - defer alloc.free(response_bytes); - const resp_name, const resp_payload = try Self.decode(response_bytes); - if (std.mem.eql(u8, resp_name, "EchoErrorResponse")) return error.ServerError; - return try types.EchoFieldsResponse.fromPayload(resp_payload); - } - - pub fn nested(self: *Self, cmd: types.EchoNested) !types.EchoNestedResponse { - const request_bytes = try Self.encode("EchoNested", try cmd.toPayload(alloc)); - defer alloc.free(request_bytes); - const response_bytes = try self.backend.call(request_bytes); - defer alloc.free(response_bytes); - const resp_name, const resp_payload = try Self.decode(response_bytes); - if (std.mem.eql(u8, resp_name, "EchoErrorResponse")) return error.ServerError; - return try types.EchoNestedResponse.fromPayload(resp_payload); - } - - // --- internal helpers --- - - fn encode(cmd_name: []const u8, cmd_fields: Payload) ![]u8 { - var inner = try Payload.arrPayload(2, alloc); - try inner.setArrElement(0, try Payload.strToPayload(cmd_name, alloc)); - try inner.setArrElement(1, cmd_fields); - var outer = try Payload.arrPayload(1, alloc); - try outer.setArrElement(0, inner); - - var allocating_writer = std.Io.Writer.Allocating.init(alloc); - var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); - try packer.write(outer); - return try allocating_writer.toOwnedSlice(); - } - - fn decode(response_bytes: []const u8) !struct { []const u8, Payload } { - var reader = std.Io.Reader.fixed(response_bytes); - var unpacker = msgpack.PackerIO.init(&reader, undefined); - const resp = try unpacker.read(alloc); - const resp_len = try resp.getArrLen(); - if (resp_len != 2) return error.InvalidResponse; - const name = try (try resp.getArrElement(0)).asStr(); - const payload = try resp.getArrElement(1); - return .{ name, payload }; - } - }; -} diff --git a/ipc-codegen/examples/zig/echo/generated/echo_server.zig b/ipc-codegen/examples/zig/echo/generated/echo_server.zig deleted file mode 100644 index f6da558629e4..000000000000 --- a/ipc-codegen/examples/zig/echo/generated/echo_server.zig +++ /dev/null @@ -1,72 +0,0 @@ -//! AUTOGENERATED - DO NOT EDIT -//! Echo IPC server — dispatch + stub handlers over generic IPC transport. -//! -//! Implement the handler functions below to build a working Echo service. -//! Then call: serve("socket_path") - -const std = @import("std"); -const msgpack = @import("msgpack"); -const Payload = msgpack.Payload; -const types = @import("echo_types.zig"); -const ipc_server = @import("ipc_server.zig"); - -const alloc = std.heap.page_allocator; - -/// Start the Echo server on the given socket path. -pub fn serve(socket_path: []const u8) !void { - try ipc_server.serve(socket_path, dispatch); -} - -fn dispatch(cmd_name: []const u8, cmd_fields: Payload) ipc_server.DispatchResult { - // Shutdown - if (std.mem.eql(u8, cmd_name, "EchoShutdown")) { - return .{ .resp_name = "EchoShutdownResponse", .resp_payload = Payload.mapPayload(alloc) }; - } - - // Command dispatch - if (std.mem.eql(u8, cmd_name, "EchoBytes")) { - const cmd = types.EchoBytes.fromPayload(cmd_fields) catch return makeError("deser failed"); - const resp = bytes(cmd) catch return makeError("not implemented: EchoBytes"); - return .{ .resp_name = "EchoBytesResponse", .resp_payload = resp.toPayload(alloc) }; - } - if (std.mem.eql(u8, cmd_name, "EchoFields")) { - const cmd = types.EchoFields.fromPayload(cmd_fields) catch return makeError("deser failed"); - const resp = fields(cmd) catch return makeError("not implemented: EchoFields"); - return .{ .resp_name = "EchoFieldsResponse", .resp_payload = resp.toPayload(alloc) }; - } - if (std.mem.eql(u8, cmd_name, "EchoNested")) { - const cmd = types.EchoNested.fromPayload(cmd_fields) catch return makeError("deser failed"); - const resp = nested(cmd) catch return makeError("not implemented: EchoNested"); - return .{ .resp_name = "EchoNestedResponse", .resp_payload = resp.toPayload(alloc) }; - } - - return makeError("unknown command"); -} - -fn makeError(message: []const u8) ipc_server.DispatchResult { - var err_map = Payload.mapPayload(alloc); - err_map.mapPut("message", Payload.strToPayload(message, alloc) catch return .{ .resp_name = "EchoErrorResponse", .resp_payload = Payload.mapPayload(alloc) }) catch {}; - return .{ .resp_name = "EchoErrorResponse", .resp_payload = err_map }; -} - -// --------------------------------------------------------------------------- -// Handler stubs — implement these to build your Echo service. -// --------------------------------------------------------------------------- - -/// TODO: implement EchoBytes -fn bytes(cmd: types.EchoBytes) !types.EchoBytesResponse { - _ = cmd; - return error.NotImplemented; -} - -/// TODO: implement EchoFields -fn fields(cmd: types.EchoFields) !types.EchoFieldsResponse { - _ = cmd; - return error.NotImplemented; -} - -/// TODO: implement EchoNested -fn nested(cmd: types.EchoNested) !types.EchoNestedResponse { - _ = cmd; - return error.NotImplemented; -} diff --git a/ipc-codegen/examples/zig/echo/generated/echo_types.zig b/ipc-codegen/examples/zig/echo/generated/echo_types.zig deleted file mode 100644 index 9ecd3bd833a6..000000000000 --- a/ipc-codegen/examples/zig/echo/generated/echo_types.zig +++ /dev/null @@ -1,239 +0,0 @@ -//! AUTOGENERATED - DO NOT EDIT -//! Generated from Aztec IPC msgpack schema -//! -//! Each struct has toPayload() and fromPayload() methods that convert -//! to/from zig-msgpack Payload objects for serialization. - -const std = @import("std"); -const msgpack = @import("msgpack"); -const Payload = msgpack.Payload; -const PackerIO = msgpack.PackerIO; - -/// Schema version hash for compatibility checking -pub const SCHEMA_HASH = "bb6458c4c159270c9a0e50ec8ad88d1e1c93271550542c7ab148e96adb6460cc"; - -/// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. -pub const Fr = [32]u8; - -// --------------------------------------------------------------------------- -// Type definitions -// --------------------------------------------------------------------------- - -/// EchoInner -pub const EchoInner = struct { - values: []const []const u8, - flag: ?bool, - - pub fn toPayload(self: EchoInner, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("values", blk: { - var arr = try Payload.arrPayload(self.values.len, allocator); - for (self.values, 0..) |item, i| { - try arr.setArrElement(i, try Payload.binToPayload(item, allocator)); - } - break :blk arr; - }); - try map.mapPut("flag", if (self.flag) |v| Payload{ .bool = v } else Payload{ .nil = {} }); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoInner { - return EchoInner{ - .values = blk: { - const arr_len = try (try payload.mapGet("values")).?.getArrLen(); - var result = try std.heap.page_allocator.alloc([]const u8, arr_len); - for (0..arr_len) |i| { - const elem = try (try payload.mapGet("values")).?.getArrElement(i); - result[i] = elem.bin.value(); - } - break :blk result; - }, - .flag = if ((try payload.mapGet("flag")).? == .nil) null else try (try payload.mapGet("flag")).?.asBool(), - }; - } -}; - -/// EchoBytes -pub const EchoBytes = struct { - data: []const u8, - - pub fn toPayload(self: EchoBytes, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("data", try Payload.binToPayload(self.data, allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoBytes { - return EchoBytes{ - .data = (try payload.mapGet("data")).?.bin.value(), - }; - } -}; - -/// EchoFields -pub const EchoFields = struct { - a: u32, - b: u64, - name: []const u8, - - pub fn toPayload(self: EchoFields, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("a", Payload{ .uint = @intCast(self.a) }); - try map.mapPut("b", Payload{ .uint = @intCast(self.b) }); - try map.mapPut("name", try Payload.strToPayload(self.name, allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoFields { - return EchoFields{ - .a = @intCast(try (try payload.mapGet("a")).?.asUint()), - .b = try (try payload.mapGet("b")).?.asUint(), - .name = try (try payload.mapGet("name")).?.asStr(), - }; - } -}; - -/// EchoNested -pub const EchoNested = struct { - inner: EchoInner, - - pub fn toPayload(self: EchoNested, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("inner", try self.inner.toPayload(allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoNested { - return EchoNested{ - .inner = try EchoInner.fromPayload((try payload.mapGet("inner")).?), - }; - } -}; - -/// EchoShutdown -pub const EchoShutdown = struct { - - pub fn toPayload(_: EchoShutdown, allocator: std.mem.Allocator) !Payload { - return Payload.mapPayload(allocator); - } - - pub fn fromPayload(_: Payload) !EchoShutdown { - return EchoShutdown{}; - } -}; - -/// EchoBytesResponse -pub const EchoBytesResponse = struct { - data: []const u8, - - pub fn toPayload(self: EchoBytesResponse, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("data", try Payload.binToPayload(self.data, allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoBytesResponse { - return EchoBytesResponse{ - .data = (try payload.mapGet("data")).?.bin.value(), - }; - } -}; - -/// EchoFieldsResponse -pub const EchoFieldsResponse = struct { - a: u32, - b: u64, - name: []const u8, - - pub fn toPayload(self: EchoFieldsResponse, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("a", Payload{ .uint = @intCast(self.a) }); - try map.mapPut("b", Payload{ .uint = @intCast(self.b) }); - try map.mapPut("name", try Payload.strToPayload(self.name, allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoFieldsResponse { - return EchoFieldsResponse{ - .a = @intCast(try (try payload.mapGet("a")).?.asUint()), - .b = try (try payload.mapGet("b")).?.asUint(), - .name = try (try payload.mapGet("name")).?.asStr(), - }; - } -}; - -/// EchoNestedResponse -pub const EchoNestedResponse = struct { - inner: EchoInner, - - pub fn toPayload(self: EchoNestedResponse, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("inner", try self.inner.toPayload(allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoNestedResponse { - return EchoNestedResponse{ - .inner = try EchoInner.fromPayload((try payload.mapGet("inner")).?), - }; - } -}; - -/// EchoShutdownResponse -pub const EchoShutdownResponse = struct { - - pub fn toPayload(_: EchoShutdownResponse, allocator: std.mem.Allocator) !Payload { - return Payload.mapPayload(allocator); - } - - pub fn fromPayload(_: Payload) !EchoShutdownResponse { - return EchoShutdownResponse{}; - } -}; - -/// EchoErrorResponse -pub const EchoErrorResponse = struct { - message: []const u8, - - pub fn toPayload(self: EchoErrorResponse, allocator: std.mem.Allocator) !Payload { - var map = Payload.mapPayload(allocator); - try map.mapPut("message", try Payload.strToPayload(self.message, allocator)); - return map; - } - - pub fn fromPayload(payload: Payload) !EchoErrorResponse { - return EchoErrorResponse{ - .message = try (try payload.mapGet("message")).?.asStr(), - }; - } -}; - -// --------------------------------------------------------------------------- -// Command / Response unions -// --------------------------------------------------------------------------- - -/// Tagged union of all commands -pub const Command = union(enum) { - echo_bytes: EchoBytes, - echo_fields: EchoFields, - echo_nested: EchoNested, - echo_shutdown: EchoShutdown, - - pub fn schemaName(self: Command) []const u8 { - return switch (self) { - .echo_bytes => "EchoBytes", - .echo_fields => "EchoFields", - .echo_nested => "EchoNested", - .echo_shutdown => "EchoShutdown", - }; - } -}; - -/// Tagged union of all responses -pub const Response = union(enum) { - echo_bytes_response: EchoBytesResponse, - echo_fields_response: EchoFieldsResponse, - echo_nested_response: EchoNestedResponse, - echo_shutdown_response: EchoShutdownResponse, - echo_error_response: EchoErrorResponse, -}; diff --git a/ipc-codegen/examples/zig/echo/generated/ipc_server.zig b/ipc-codegen/examples/zig/echo/generated/ipc_server.zig deleted file mode 100644 index ba58a09b5a61..000000000000 --- a/ipc-codegen/examples/zig/echo/generated/ipc_server.zig +++ /dev/null @@ -1,112 +0,0 @@ -/// Generic IPC server over Unix Domain Sockets. -/// Handles: socket setup, accept, length-prefixed framing, msgpack decode/encode. -/// Service-specific dispatch is injected via the DispatchFn parameter. -const std = @import("std"); -const posix = std.posix; -const msgpack = @import("msgpack"); -const Payload = msgpack.Payload; - -const alloc = std.heap.page_allocator; - -pub const DispatchFn = *const fn (cmd_name: []const u8, fields: Payload) DispatchResult; -pub const DispatchResult = struct { resp_name: []const u8, resp_payload: anyerror!Payload }; - -/// Run an IPC server on the given UDS path. -/// Accepts one connection, serves requests until shutdown or disconnect. -pub fn serve(socket_path: []const u8, dispatch: DispatchFn) !void { - std.fs.cwd().deleteFile(socket_path) catch {}; - - const address = try std.net.Address.initUnix(socket_path); - const server_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); - defer posix.close(server_fd); - try posix.bind(server_fd, &address.any, address.getOsSockLen()); - try posix.listen(server_fd, 1); - - std.debug.print("ipc-server: listening on {s}\n", .{socket_path}); - - const client_fd = try posix.accept(server_fd, null, null, 0); - defer posix.close(client_fd); - - while (true) { - const frame = recvFrame(client_fd) catch break; - defer alloc.free(frame); - - // Decode msgpack: [[commandName, {fields}]] - var reader = std.Io.Reader.fixed(frame); - var packer = msgpack.PackerIO.init(&reader, undefined); - const request = packer.read(alloc) catch break; - - const outer_len = request.getArrLen() catch break; - if (outer_len != 1) break; - - const inner = request.getArrElement(0) catch break; - const inner_len = inner.getArrLen() catch break; - if (inner_len != 2) break; - - const cmd_name = (inner.getArrElement(0) catch break).asStr() catch break; - const fields = inner.getArrElement(1) catch break; - - // Dispatch - const result = dispatch(cmd_name, fields); - const resp_payload = result.resp_payload catch blk: { - var err_map = Payload.mapPayload(alloc); - const msg = std.fmt.allocPrint(alloc, "error: {s}", .{cmd_name}) catch "error"; - err_map.mapPut("message", Payload.strToPayload(msg, alloc) catch break) catch break; - break :blk err_map; - }; - const is_error = if (result.resp_payload) |_| false else |_| true; - const resp_name = if (is_error) "ErrorResponse" else result.resp_name; - - const response = encodeResponse(resp_name, resp_payload) catch break; - defer alloc.free(response); - sendFrame(client_fd, response) catch break; - - // Check for shutdown - if (std.mem.indexOf(u8, cmd_name, "Shutdown") != null) break; - } - - std.fs.cwd().deleteFile(socket_path) catch {}; - std.debug.print("ipc-server: shutdown\n", .{}); -} - -fn encodeResponse(name: []const u8, payload: Payload) ![]u8 { - var resp_arr = try Payload.arrPayload(2, alloc); - try resp_arr.setArrElement(0, try Payload.strToPayload(name, alloc)); - try resp_arr.setArrElement(1, payload); - - var allocating_writer = std.Io.Writer.Allocating.init(alloc); - var packer = msgpack.PackerIO.init(undefined, &allocating_writer.writer); - try packer.write(resp_arr); - return try allocating_writer.toOwnedSlice(); -} - -fn recvFrame(fd: posix.socket_t) ![]u8 { - var hdr: [4]u8 = undefined; - var got: usize = 0; - while (got < 4) { - const n = try posix.read(fd, hdr[got..]); - if (n == 0) return error.ConnectionClosed; - got += n; - } - const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); - const data = try alloc.alloc(u8, len); - got = 0; - while (got < len) { - const n = try posix.read(fd, data[got..]); - if (n == 0) return error.ConnectionClosed; - got += n; - } - return data; -} - -fn sendFrame(fd: posix.socket_t, data: []const u8) !void { - const len: u32 = @intCast(data.len); - const header = [4]u8{ - @intCast(len & 0xFF), - @intCast((len >> 8) & 0xFF), - @intCast((len >> 16) & 0xFF), - @intCast((len >> 24) & 0xFF), - }; - _ = try posix.write(fd, &header); - _ = try posix.write(fd, data); -} From fe5147446d226d6f0c9ce3841fc98f7ed946f977 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 16:52:37 +0000 Subject: [PATCH 03/11] chore(ipc-codegen): split test matrix into per-pair test_cmds Move build-once steps (cargo build, zig build, npm install, C++ compile, golden generation) from the test runner into bootstrap.sh build. Rename run_cross_language_tests.sh -> run_cross_language_test.sh (singular) and have it accept golden or matrix . Emit one command per pair in test_cmds so each test reports PASSED / FAILED independently in the parallelize framework. Verify: - ./bootstrap.sh build # codegen + per-language compilation + goldens - ./bootstrap.sh test # 18 tests, each reported individually --- ipc-codegen/bootstrap.sh | 64 ++++++- .../scripts/run_cross_language_test.sh | 92 ++++++++++ .../scripts/run_cross_language_tests.sh | 162 ------------------ 3 files changed, 148 insertions(+), 170 deletions(-) create mode 100755 ipc-codegen/examples/scripts/run_cross_language_test.sh delete mode 100755 ipc-codegen/examples/scripts/run_cross_language_tests.sh diff --git a/ipc-codegen/bootstrap.sh b/ipc-codegen/bootstrap.sh index ceea9547356d..af7cc5b9b566 100755 --- a/ipc-codegen/bootstrap.sh +++ b/ipc-codegen/bootstrap.sh @@ -3,7 +3,7 @@ # Generates IPC bindings from committed JSON schemas under schemas/, in TS, C++, # Rust and Zig. Zero npm dependencies — runs with just Node.js (v22+). # -# The build's only consumer is its own cross-language test harness under +# The build's only direct consumer is its own cross-language test harness under # examples/. Service consumers (bb, wsdb, cdb, avm) are wired up by their # own bootstrap scripts, which invoke `ipc-codegen/bootstrap.sh build` as # a build-time prerequisite. @@ -17,19 +17,67 @@ NODE_FLAGS="--experimental-strip-types --experimental-transform-types --no-warni gen() { node $NODE_FLAGS src/generate.ts "$@"; } function build { - echo_header "ipc-codegen build (generate echo example bindings)" + echo_header "ipc-codegen build" # Service generation (bb, wsdb, cdb, avm) is invoked by each service's own - # bootstrap. The build step here only generates the echo example bindings, - # which the test harness consumes. + # bootstrap as those consumers migrate over. The build step here only + # generates the echo example bindings + compiles the per-language test + # binaries that test_cmds emits commands against. examples/echo-schema/generate.sh + + echo "Building Rust echo binaries..." + (cd examples/rust/echo && cargo build --quiet) + + echo "Building Zig echo binaries..." + (cd examples/zig/echo && zig build) + + echo "Installing TS echo deps..." + if [ ! -d examples/ts/echo/node_modules ]; then + (cd examples/ts/echo && npm install --no-package-lock --quiet) + fi + + # C++ requires msgpack-c from a bb cmake build. If it's not present, skip — + # the C++ matrix pairs will be omitted in test_cmds below. + local MSGPACK_INC + MSGPACK_INC="$(cd ../barretenberg/cpp/build/_deps/msgpack-c/src/msgpack-c/include 2>/dev/null && pwd)" || true + local BB_SERIALIZE + BB_SERIALIZE="$(cd ../barretenberg/cpp/src/barretenberg/serialize/msgpack_impl 2>/dev/null && pwd)" || true + if [ -n "${MSGPACK_INC:-}" ] && [ -d "$MSGPACK_INC" ]; then + echo "Building C++ echo binaries..." + local CXX_FLAGS="-std=c++20 -I $MSGPACK_INC -I $BB_SERIALIZE -I . -DMSGPACK_NO_BOOST -DMSGPACK_USE_STD_VARIANT_ADAPTOR" + (cd examples/cpp/echo && clang++ $CXX_FLAGS -o echo_server echo_server.cpp) + (cd examples/cpp/echo && clang++ $CXX_FLAGS -o echo_client echo_client.cpp) + else + echo "Skipping C++ echo build — msgpack-c not present at ../barretenberg/cpp/build/_deps/" + fi + + # Pre-bake golden fixtures from the Rust reference so the per-language + # golden tests are pure deserialization (no shared write-then-read). + examples/rust/echo/target/debug/generate_golden --output-dir examples/echo-schema/golden } function test_cmds { - # Single test command: the 4-language echo wire-compat matrix. - # Needs CPUS so the cargo + zig builds inside run_cross_language_tests.sh - # have headroom, and TIMEOUT for the cold-cache first run. - echo "$hash:CPUS=4:TIMEOUT=600s ipc-codegen/examples/scripts/run_cross_language_tests.sh" + # Discover which languages can participate. Rust/TS/Zig are unconditional + # (build() always builds them); C++ is conditional on bb's msgpack being present. + local matrix_langs=(rust ts zig) + if [ -x examples/cpp/echo/echo_server ]; then + matrix_langs+=(cpp) + fi + + local prefix="$hash:CPUS=1:TIMEOUT=120s" + local script="ipc-codegen/examples/scripts/run_cross_language_test.sh" + + # Golden tests (Rust + TS each verify they can deserialize the goldens + # baked by build()). + echo "$prefix $script golden rust" + echo "$prefix $script golden ts" + + # Matrix: one command per (server, client) pair. + for server in "${matrix_langs[@]}"; do + for client in "${matrix_langs[@]}"; do + echo "$prefix $script matrix $server $client" + done + done } function test { diff --git a/ipc-codegen/examples/scripts/run_cross_language_test.sh b/ipc-codegen/examples/scripts/run_cross_language_test.sh new file mode 100755 index 000000000000..493384400b60 --- /dev/null +++ b/ipc-codegen/examples/scripts/run_cross_language_test.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# +# Run a single cross-language IPC wire-compat test. +# All binaries are expected to be prebuilt by `ipc-codegen/bootstrap.sh build`. +# +# Usage: +# run_cross_language_test.sh golden # lang in {rust, ts} +# run_cross_language_test.sh matrix +# # langs in {rust, ts, cpp, zig} +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +EXAMPLES_DIR="$(dirname "$SCRIPT_DIR")" +cd "$EXAMPLES_DIR" + +# Map language -> server command / client command. Each command is run with +# `--socket ` appended. +server_cmd_for() { + case "$1" in + rust) echo "rust/echo/target/debug/echo_server" ;; + ts) echo "npx tsx ts/echo/echo_server.ts" ;; + cpp) echo "cpp/echo/echo_server" ;; + zig) echo "zig/echo/zig-out/bin/echo_server" ;; + *) echo "unknown lang: $1" >&2; exit 1 ;; + esac +} + +client_cmd_for() { + case "$1" in + rust) echo "rust/echo/target/debug/echo_client" ;; + ts) echo "npx tsx ts/echo/echo_client.ts" ;; + cpp) echo "cpp/echo/echo_client" ;; + zig) echo "zig/echo/zig-out/bin/echo_client" ;; + *) echo "unknown lang: $1" >&2; exit 1 ;; + esac +} + +run_golden() { + local lang="$1" + case "$lang" in + rust) + rust/echo/target/debug/golden_test --golden-dir echo-schema/golden + ;; + ts) + npx tsx ts/echo/golden_test.ts + ;; + *) + echo "golden tests only defined for rust and ts (got: $lang)" >&2 + exit 1 + ;; + esac +} + +run_matrix() { + local server_lang="$1" + local client_lang="$2" + local server_cmd client_cmd + server_cmd=$(server_cmd_for "$server_lang") + client_cmd=$(client_cmd_for "$client_lang") + local socket="/tmp/echo-matrix-${server_lang}-${client_lang}-$$.sock" + + # Start server in background, wait for socket, run client. + $server_cmd --socket "$socket" & + local server_pid=$! + trap "kill $server_pid 2>/dev/null || true; rm -f $socket" EXIT + + for _ in $(seq 1 20); do + [ -S "$socket" ] && break + sleep 0.1 + done + if [ ! -S "$socket" ]; then + echo "server did not create socket within 2s" >&2 + exit 1 + fi + + $client_cmd --socket "$socket" +} + +kind="${1:-}" +case "$kind" in + golden) + run_golden "${2:?golden requires }" + ;; + matrix) + run_matrix "${2:?matrix requires }" "${3:?matrix requires }" + ;; + *) + echo "Usage: $0 golden | matrix " >&2 + exit 1 + ;; +esac diff --git a/ipc-codegen/examples/scripts/run_cross_language_tests.sh b/ipc-codegen/examples/scripts/run_cross_language_tests.sh deleted file mode 100755 index 0cdea133ad4c..000000000000 --- a/ipc-codegen/examples/scripts/run_cross_language_tests.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# -# Cross-language IPC wire compatibility test matrix. -# Tests all server/client language pairs for the echo service. -# -# Usage: ./scripts/run_cross_language_tests.sh -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -EXAMPLES_DIR="$(dirname "$SCRIPT_DIR")" -cd "$EXAMPLES_DIR" - -PASS=0 -FAIL=0 -TOTAL=0 - -# Generate types from echo schema using the codegen CLI -"$EXAMPLES_DIR/echo-schema/generate.sh" - -# Build Rust binaries -echo "Building Rust echo binaries..." -(cd rust/echo && cargo build --quiet 2>&1) - -# Build C++ binaries -echo "Building C++ echo binaries..." -MSGPACK_INC="$(cd "$EXAMPLES_DIR/../../barretenberg/cpp/build/_deps/msgpack-c/src/msgpack-c/include" 2>/dev/null && pwd)" || true -BB_SERIALIZE="$(cd "$EXAMPLES_DIR/../../barretenberg/cpp/src/barretenberg/serialize/msgpack_impl" 2>/dev/null && pwd)" || true -if [ -n "$MSGPACK_INC" ] && [ -d "$MSGPACK_INC" ]; then - CXX_FLAGS="-std=c++20 -I $MSGPACK_INC -I $BB_SERIALIZE -I . -DMSGPACK_NO_BOOST -DMSGPACK_USE_STD_VARIANT_ADAPTOR" - (cd cpp/echo && clang++ $CXX_FLAGS -o echo_server echo_server.cpp 2>&1) - (cd cpp/echo && clang++ $CXX_FLAGS -o echo_client echo_client.cpp 2>&1) - CPP_AVAILABLE=true -else - echo " (skipping C++ — msgpack-c not found, run cmake first)" - CPP_AVAILABLE=false -fi - -# Install TS dependencies if needed -if [ ! -d ts/echo/node_modules ]; then - echo "Installing TS dependencies..." - (cd ts/echo && npm install --no-package-lock --quiet 2>&1) -fi - -# Server/client definitions -# Format: "lang:start_cmd:name" -SERVERS=( - "rust:rust/echo/target/debug/echo_server:Rust" - "ts:npx tsx ts/echo/echo_server.ts:TS" -) -CLIENTS=( - "rust:rust/echo/target/debug/echo_client:Rust" - "ts:npx tsx ts/echo/echo_client.ts:TS" -) - -if [ "$CPP_AVAILABLE" = true ]; then - SERVERS+=("cpp:cpp/echo/echo_server:C++") - CLIENTS+=("cpp:cpp/echo/echo_client:C++") -fi - -# Build Zig binaries -echo "Building Zig echo binaries..." -(cd zig/echo && zig build 2>&1) -SERVERS+=("zig:zig/echo/zig-out/bin/echo_server:Zig") -CLIENTS+=("zig:zig/echo/zig-out/bin/echo_client:Zig") - -run_pair() { - local server_cmd="$1" - local server_name="$2" - local client_cmd="$3" - local client_name="$4" - - TOTAL=$((TOTAL + 1)) - local socket="/tmp/echo-matrix-${server_name}-${client_name}-$$.sock" - - # Start server - $server_cmd --socket "$socket" & - local server_pid=$! - - # Wait for socket (up to 2 seconds) - for i in $(seq 1 20); do - if [ -S "$socket" ]; then break; fi - sleep 0.1 - done - - if [ ! -S "$socket" ]; then - echo " FAIL: server did not create socket" - FAIL=$((FAIL + 1)) - kill $server_pid 2>/dev/null || true - return - fi - - # Run client - if $client_cmd --socket "$socket" 2>/dev/null; then - echo " PASS: $server_name server + $client_name client" - PASS=$((PASS + 1)) - else - echo " FAIL: $server_name server + $client_name client" - FAIL=$((FAIL + 1)) - fi - - # Cleanup - wait $server_pid 2>/dev/null || true - rm -f "$socket" -} - -# --------------------------------------------------------------------------- -# Level 1: Golden file deserialization tests -# --------------------------------------------------------------------------- -echo "=== Golden File Deserialization Tests ===" -echo "" - -# Generate golden files from Rust reference -echo "Generating golden files from Rust reference..." -(cd rust/echo && cargo build --quiet --bin generate_golden 2>&1) -rust/echo/target/debug/generate_golden --output-dir echo-schema/golden 2>/dev/null - -# Rust golden test -echo " Rust:" -if (cd rust/echo && cargo build --quiet --bin golden_test 2>&1) && \ - rust/echo/target/debug/golden_test --golden-dir echo-schema/golden 2>/dev/null; then - echo " PASS" - PASS=$((PASS + 1)) -else - echo " FAIL" - FAIL=$((FAIL + 1)) -fi -TOTAL=$((TOTAL + 1)) - -# TypeScript golden test -echo " TypeScript:" -if npx tsx ts/echo/golden_test.ts 2>/dev/null; then - echo " PASS" - PASS=$((PASS + 1)) -else - echo " FAIL" - FAIL=$((FAIL + 1)) -fi -TOTAL=$((TOTAL + 1)) - -echo "" - -# --------------------------------------------------------------------------- -# Level 2+3: IPC Round-Trip Matrix -# --------------------------------------------------------------------------- -echo "=== Cross-Language Wire Compatibility Matrix ===" -echo "" - -for server_entry in "${SERVERS[@]}"; do - IFS=: read -r _ server_cmd server_name <<< "$server_entry" - for client_entry in "${CLIENTS[@]}"; do - IFS=: read -r _ client_cmd client_name <<< "$client_entry" - run_pair "$server_cmd" "$server_name" "$client_cmd" "$client_name" - done -done - -echo "" -echo "Results: $PASS/$TOTAL passed, $FAIL failed" - -if [ "$FAIL" -gt 0 ]; then - exit 1 -fi From 669bff76de97be452fe4b0822fcc6a636b0866d2 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 17:16:41 +0000 Subject: [PATCH 04/11] chore(ipc-codegen): make generated C++ client self-contained; simplify echo example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated _ipc_client.cpp and _ipc_server.hpp used to #include "barretenberg/serialize/msgpack.hpp" and msgpack_impl.hpp, forcing any consumer to be inside the barretenberg include path. The echo example worked around this by hand-rolling its own msgpack pack/unpack loop instead of using the generated EchoIpcClient. Bundle the msgpack adaptor (struct_map_impl + concepts + drop_keys, all inlined into one ~80-line header) under ipc-codegen/templates/cpp/. The C++ codegen now emits #include "msgpack_struct_map_impl.hpp" and the codegen copies the template alongside the generated client/server. With that done, rewrite examples/cpp/echo/echo_client.cpp to actually use the generated client: echo::EchoIpcClient client(socket_path); auto resp = client.fields({.a = 42, .b = 999, .name = "hello"}); instead of the previous 90 lines of manual pack/unpack. echo_server.cpp similarly drops its barretenberg struct_map_impl.hpp include — the generated server header now brings the adaptor in itself. Also fixes a generatedInclude() bug where an empty --cpp-include-dir produced "/foo.hpp" instead of "foo.hpp". --- ipc-codegen/bootstrap.sh | 10 +- ipc-codegen/examples/cpp/echo/echo_client.cpp | 126 ++--- ipc-codegen/examples/cpp/echo/echo_server.cpp | 65 +-- ipc-codegen/examples/echo-schema/generate.sh | 2 +- ipc-codegen/src/cpp_codegen.ts | 505 +++++++++++------- ipc-codegen/src/generate.ts | 370 +++++++++---- .../templates/cpp/msgpack_struct_map_impl.hpp | 96 ++++ 7 files changed, 740 insertions(+), 434 deletions(-) create mode 100644 ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp diff --git a/ipc-codegen/bootstrap.sh b/ipc-codegen/bootstrap.sh index af7cc5b9b566..d196159a5735 100755 --- a/ipc-codegen/bootstrap.sh +++ b/ipc-codegen/bootstrap.sh @@ -36,17 +36,15 @@ function build { (cd examples/ts/echo && npm install --no-package-lock --quiet) fi - # C++ requires msgpack-c from a bb cmake build. If it's not present, skip — - # the C++ matrix pairs will be omitted in test_cmds below. + # C++ needs msgpack-c headers. We pick them up from a local bb cmake build + # for now; if absent the C++ matrix pairs are skipped in test_cmds below. local MSGPACK_INC MSGPACK_INC="$(cd ../barretenberg/cpp/build/_deps/msgpack-c/src/msgpack-c/include 2>/dev/null && pwd)" || true - local BB_SERIALIZE - BB_SERIALIZE="$(cd ../barretenberg/cpp/src/barretenberg/serialize/msgpack_impl 2>/dev/null && pwd)" || true if [ -n "${MSGPACK_INC:-}" ] && [ -d "$MSGPACK_INC" ]; then echo "Building C++ echo binaries..." - local CXX_FLAGS="-std=c++20 -I $MSGPACK_INC -I $BB_SERIALIZE -I . -DMSGPACK_NO_BOOST -DMSGPACK_USE_STD_VARIANT_ADAPTOR" + local CXX_FLAGS="-std=c++20 -I $MSGPACK_INC -DMSGPACK_NO_BOOST -DMSGPACK_USE_STD_VARIANT_ADAPTOR" (cd examples/cpp/echo && clang++ $CXX_FLAGS -o echo_server echo_server.cpp) - (cd examples/cpp/echo && clang++ $CXX_FLAGS -o echo_client echo_client.cpp) + (cd examples/cpp/echo && clang++ $CXX_FLAGS -o echo_client echo_client.cpp generated/echo_ipc_client.cpp) else echo "Skipping C++ echo build — msgpack-c not present at ../barretenberg/cpp/build/_deps/" fi diff --git a/ipc-codegen/examples/cpp/echo/echo_client.cpp b/ipc-codegen/examples/cpp/echo/echo_client.cpp index 2a7ef59434c7..946c8156eb46 100644 --- a/ipc-codegen/examples/cpp/echo/echo_client.cpp +++ b/ipc-codegen/examples/cpp/echo/echo_client.cpp @@ -1,91 +1,49 @@ -/** - * Echo IPC client (C++) — uses GENERATED types + IPC client template. - * Usage: echo_client --socket /tmp/echo.sock - * - * Note: The generated EchoIpcClient (.hpp/.cpp) depends on barretenberg - * msgpack headers which are not available in this standalone test context. - * Instead we build a thin client directly on the generated types + ipc_client. - */ +// Echo IPC client (C++) — uses the generated EchoIpcClient. +// Usage: echo_client --socket /tmp/echo.sock -#include "generated/echo_types.hpp" -#include "generated/ipc_client.hpp" - -// Need msgpack for serialization + barretenberg's SERIALIZATION_FIELDS adaptor -#ifndef THROW -#define THROW throw -#endif -#ifndef RETHROW -#define RETHROW throw -#endif -#include -#include "struct_map_impl.hpp" +#include "generated/echo_ipc_client.hpp" #include #include #include -using namespace echo::wire; - -template -Resp call(ipc::IpcClient& client, Cmd&& cmd) { - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(1); pk.pack_array(2); - pk.pack(std::string(Cmd::MSGPACK_SCHEMA_NAME)); - pk.pack(std::forward(cmd)); - - auto resp = client.call(std::vector(buf.data(), buf.data() + buf.size())); - auto oh = msgpack::unpack(reinterpret_cast(resp.data()), resp.size()); - auto obj = oh.get(); - std::string resp_name(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); - if (resp_name == "EchoErrorResponse") throw std::runtime_error("server error"); - Resp result; obj.via.array.ptr[1].convert(result); - return result; -} - -int main(int argc, char** argv) { - const char* socket_path = nullptr; - for (int i = 1; i < argc - 1; i++) { - if (std::string_view(argv[i]) == "--socket") socket_path = argv[i + 1]; - } - if (!socket_path) { std::cerr << "Usage: echo_client --socket \n"; return 1; } - - ipc::IpcClient client(socket_path); - - // EchoBytes - { - EchoBytes cmd{ .data = { 0xDE, 0xAD, 0xBE, 0xEF, 0x42 } }; - auto resp = call(client, std::move(cmd)); - assert((resp.data == std::vector{ 0xDE, 0xAD, 0xBE, 0xEF, 0x42 })); - std::cerr << "echo_client(cpp): EchoBytes OK\n"; - } - - // EchoFields - { - EchoFields cmd{ .a = 42, .b = 999999, .name = "hello wire compat" }; - auto resp = call(client, std::move(cmd)); - assert(resp.a == 42 && resp.b == 999999 && resp.name == "hello wire compat"); - std::cerr << "echo_client(cpp): EchoFields OK\n"; - } - - // EchoNested - { - EchoNested cmd{ .inner = { .values = { {1, 2, 3}, {4, 5} }, .flag = true } }; - auto resp = call(client, std::move(cmd)); - assert((resp.inner.values == std::vector>{ {1, 2, 3}, {4, 5} })); - assert(resp.inner.flag == true); - std::cerr << "echo_client(cpp): EchoNested OK\n"; - } - - // Shutdown - { - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(1); pk.pack_array(2); - pk.pack(std::string("EchoShutdown")); pk.pack_map(0); - client.call(std::vector(buf.data(), buf.data() + buf.size())); - } - - std::cerr << "echo_client(cpp): all tests passed\n"; - return 0; +int main(int argc, char **argv) { + const char *socket_path = nullptr; + for (int i = 1; i < argc - 1; i++) { + if (std::string_view(argv[i]) == "--socket") + socket_path = argv[i + 1]; + } + if (!socket_path) { + std::cerr << "Usage: echo_client --socket \n"; + return 1; + } + + echo::EchoIpcClient client(socket_path); + + { + auto resp = client.bytes({.data = {0xDE, 0xAD, 0xBE, 0xEF, 0x42}}); + assert((resp.data == std::vector{0xDE, 0xAD, 0xBE, 0xEF, 0x42})); + std::cerr << "echo_client(cpp): EchoBytes OK\n"; + } + + { + auto resp = + client.fields({.a = 42, .b = 999999, .name = "hello wire compat"}); + assert(resp.a == 42 && resp.b == 999999 && + resp.name == "hello wire compat"); + std::cerr << "echo_client(cpp): EchoFields OK\n"; + } + + { + auto resp = + client.nested({.inner = {.values = {{1, 2, 3}, {4, 5}}, .flag = true}}); + assert((resp.inner.values == + std::vector>{{1, 2, 3}, {4, 5}})); + assert(resp.inner.flag == true); + std::cerr << "echo_client(cpp): EchoNested OK\n"; + } + + client.shutdown(); + std::cerr << "echo_client(cpp): all tests passed\n"; + return 0; } diff --git a/ipc-codegen/examples/cpp/echo/echo_server.cpp b/ipc-codegen/examples/cpp/echo/echo_server.cpp index db60c102d7d5..3c640cd73897 100644 --- a/ipc-codegen/examples/cpp/echo/echo_server.cpp +++ b/ipc-codegen/examples/cpp/echo/echo_server.cpp @@ -1,26 +1,7 @@ -/** - * Echo IPC server (C++) — uses GENERATED dispatch + template Ctx. - * Usage: echo_server --socket /tmp/echo.sock - */ - -// barretenberg's custom msgpack adaptor for SERIALIZATION_FIELDS — -// enables msgpack::object::convert() to work with the generated types. -// Must be included before echo_ipc_server.hpp which uses convert()/pack(). -#include "generated/echo_types.hpp" -#ifndef THROW -#define THROW throw -#endif -#ifndef RETHROW -#define RETHROW throw -#endif -#include -#include "struct_map_impl.hpp" - -// The generated server header declares template handler functions. -// We need to see those declarations before providing specializations. -// Importantly, make_echo_handler() is defined inline but only instantiated -// when serve() is called in main() — so specializations defined after -// the header but before main() are visible at instantiation time. +// Echo IPC server (C++) — provides handler specializations for the +// header-only generated dispatch. +// Usage: echo_server --socket /tmp/echo.sock + #include "generated/echo_ipc_server.hpp" #include @@ -32,30 +13,36 @@ struct EchoCtx {}; // empty context for the echo service // Template specializations — echo input fields back in response. template <> -wire::EchoBytesResponse handle_bytes(EchoCtx& /*ctx*/, wire::EchoBytes&& cmd) { - return { .data = std::move(cmd.data) }; +wire::EchoBytesResponse handle_bytes(EchoCtx & /*ctx*/, wire::EchoBytes &&cmd) { + return {.data = std::move(cmd.data)}; } template <> -wire::EchoFieldsResponse handle_fields(EchoCtx& /*ctx*/, wire::EchoFields&& cmd) { - return { .a = cmd.a, .b = cmd.b, .name = std::move(cmd.name) }; +wire::EchoFieldsResponse handle_fields(EchoCtx & /*ctx*/, + wire::EchoFields &&cmd) { + return {.a = cmd.a, .b = cmd.b, .name = std::move(cmd.name)}; } template <> -wire::EchoNestedResponse handle_nested(EchoCtx& /*ctx*/, wire::EchoNested&& cmd) { - return { .inner = std::move(cmd.inner) }; +wire::EchoNestedResponse handle_nested(EchoCtx & /*ctx*/, + wire::EchoNested &&cmd) { + return {.inner = std::move(cmd.inner)}; } } // namespace echo -int main(int argc, char** argv) { - const char* socket_path = nullptr; - for (int i = 1; i < argc - 1; i++) { - if (std::string_view(argv[i]) == "--socket") socket_path = argv[i + 1]; - } - if (!socket_path) { std::cerr << "Usage: echo_server --socket \n"; return 1; } - - echo::EchoCtx ctx; - echo::serve(socket_path, ctx); - return 0; +int main(int argc, char **argv) { + const char *socket_path = nullptr; + for (int i = 1; i < argc - 1; i++) { + if (std::string_view(argv[i]) == "--socket") + socket_path = argv[i + 1]; + } + if (!socket_path) { + std::cerr << "Usage: echo_server --socket \n"; + return 1; + } + + echo::EchoCtx ctx; + echo::serve(socket_path, ctx); + return 0; } diff --git a/ipc-codegen/examples/echo-schema/generate.sh b/ipc-codegen/examples/echo-schema/generate.sh index 31ae2bf2996c..280425ce4876 100755 --- a/ipc-codegen/examples/echo-schema/generate.sh +++ b/ipc-codegen/examples/echo-schema/generate.sh @@ -12,7 +12,7 @@ SCHEMA="$DIR/schema.json" echo "Generating echo types from $SCHEMA" -$NODE "$GEN" --schema "$SCHEMA" --lang cpp --server --client --uds --out "$EXAMPLES/cpp/echo/generated" --prefix Echo --cpp-namespace echo --cpp-include-dir echo +$NODE "$GEN" --schema "$SCHEMA" --lang cpp --server --client --uds --out "$EXAMPLES/cpp/echo/generated" --prefix Echo --cpp-namespace echo $NODE "$GEN" --schema "$SCHEMA" --lang ts --server --client --uds --out "$EXAMPLES/ts/echo/generated" --prefix Echo $NODE "$GEN" --schema "$SCHEMA" --lang rust --server --client --uds --out "$EXAMPLES/rust/echo/src/generated" --prefix Echo $NODE "$GEN" --schema "$SCHEMA" --lang zig --server --client --uds --out "$EXAMPLES/zig/echo/generated" --prefix Echo diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts index c823913df738..35b9bf6d8337 100644 --- a/ipc-codegen/src/cpp_codegen.ts +++ b/ipc-codegen/src/cpp_codegen.ts @@ -12,8 +12,14 @@ * const impl = gen.generateImpl(schema); */ -import type { CompiledSchema, Type, Struct, Field, Command } from './schema_visitor.ts'; -import { toSnakeCase } from './naming.ts'; +import type { + CompiledSchema, + Type, + Struct, + Field, + Command, +} from "./schema_visitor.ts"; +import { toSnakeCase } from "./naming.ts"; export interface CppCodegenOptions { /** C++ namespace for generated code, e.g. 'bb::cdb' */ @@ -72,20 +78,26 @@ export class CppCodegen { } /** Generate the method signature using command struct types directly */ - private generateMethodSignature(command: Command, schema: CompiledSchema, className?: string): string { + private generateMethodSignature( + command: Command, + schema: CompiledSchema, + className?: string, + ): string { const method = this.methodName(command.name); const hasFields = this.hasResponseFields(command, schema); // Wire types use top-level response names (BbFooResponse). // Command types with nested Response use Cmd::Response. const retType = hasFields - ? (this.opts.wireNamespace ? command.responseType : `${command.name}::Response`) - : 'void'; + ? this.opts.wireNamespace + ? command.responseType + : `${command.name}::Response` + : "void"; // If the command has fields, take the whole command struct by value - const params = command.fields.length > 0 ? `${command.name} cmd` : ''; + const params = command.fields.length > 0 ? `${command.name} cmd` : ""; - const prefix = className ? `${className}::` : ''; - const constSuffix = !this.isWriteCommand(command) ? ' const' : ''; + const prefix = className ? `${className}::` : ""; + const constSuffix = !this.isWriteCommand(command) ? " const" : ""; return `${retType} ${prefix}${method}(${params})${constSuffix}`; } @@ -93,11 +105,18 @@ export class CppCodegen { /** Check if a command modifies state (non-const) */ private isWriteCommand(command: Command): boolean { const name = command.name.toLowerCase(); - return name.includes('add') || name.includes('create') || - name.includes('commit') || name.includes('revert') || - name.includes('register') || name.includes('shutdown') || - name.includes('delete') || name.includes('sync') || - name.includes('rollback') || name.includes('unwind'); + return ( + name.includes("add") || + name.includes("create") || + name.includes("commit") || + name.includes("revert") || + name.includes("register") || + name.includes("shutdown") || + name.includes("delete") || + name.includes("sync") || + name.includes("rollback") || + name.includes("unwind") + ); } /** Generate the header file */ @@ -106,30 +125,32 @@ export class CppCodegen { const wireNs = this.opts.wireNamespace; const className = `${prefix}IpcClient`; - const methods = schema.commands.map(cmd => { - const sig = this.generateMethodSignature(cmd, schema); - return ` ${sig};`; - }).join('\n'); + const methods = schema.commands + .map((cmd) => { + const sig = this.generateMethodSignature(cmd, schema); + return ` ${sig};`; + }) + .join("\n"); const hashConstant = schemaHash ? `\n/** Schema version hash for compatibility checking */\nstatic constexpr const char SCHEMA_HASH[] = "${schemaHash}";\n` - : ''; + : ""; // When wireNamespace is set, include wire types and bring them into scope const wireInclude = wireNs - ? `#include "${this.generatedDir()}/${toSnakeCase(prefix)}_types.hpp"\n` - : ''; - const wireUsing = wireNs - ? `using namespace ${wireNs};\n` - : ''; + ? `#include "${this.generatedInclude(`${toSnakeCase(prefix)}_types.hpp`)}"\n` + : ""; + const wireUsing = wireNs ? `using namespace ${wireNs};\n` : ""; - const typesInclude = `${this.generatedDir()}/${toSnakeCase(prefix)}_types.hpp`; + const typesInclude = this.generatedInclude( + `${toSnakeCase(prefix)}_types.hpp`, + ); return `// AUTOGENERATED FILE - DO NOT EDIT #pragma once #include "${typesInclude}" -#include "${this.generatedDir()}/ipc_client.hpp" +#include "${this.generatedInclude("ipc_client.hpp")}" // clang-format on #include @@ -170,15 +191,16 @@ ${methods} const className = `${prefix}IpcClient`; const errorType = schema.errorTypeName || `${prefix}ErrorResponse`; - const methods = schema.commands.map(cmd => { - return this.generateMethodImpl(cmd, schema, className); - }).join('\n'); + const methods = schema.commands + .map((cmd) => { + return this.generateMethodImpl(cmd, schema, className); + }) + .join("\n"); return `// AUTOGENERATED FILE - DO NOT EDIT #include "${this.headerIncludePath()}" -#include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/serialize/msgpack_impl.hpp" +#include "msgpack_struct_map_impl.hpp" #include #include @@ -250,12 +272,19 @@ ${methods} } /** Generate a single method implementation */ - private generateMethodImpl(command: Command, schema: CompiledSchema, className: string): string { + private generateMethodImpl( + command: Command, + schema: CompiledSchema, + className: string, + ): string { const sig = this.generateMethodSignature(command, schema, className); const hasFields = this.hasResponseFields(command, schema); - const respType = this.opts.wireNamespace ? command.responseType : `${command.name}::Response`; + const respType = this.opts.wireNamespace + ? command.responseType + : `${command.name}::Response`; - const cmdExpr = command.fields.length > 0 ? 'std::move(cmd)' : `${command.name}{}`; + const cmdExpr = + command.fields.length > 0 ? "std::move(cmd)" : `${command.name}{}`; if (!hasFields) { return `${sig} @@ -272,17 +301,30 @@ ${methods} `; } - /** Get the generated/ directory include path */ + /** Get the generated/ directory include prefix. + * Returns either the explicit --cpp-include-dir value (e.g. "barretenberg/wsdb/generated") + * or empty for callers that include generated files by their bare filename. */ private generatedDir(): string { if (this.opts.generatedIncludeDir) { return this.opts.generatedIncludeDir; } - return this.opts.commandsHeader.substring(0, this.opts.commandsHeader.lastIndexOf('/')); + const lastSlash = this.opts.commandsHeader.lastIndexOf("/"); + return lastSlash >= 0 + ? this.opts.commandsHeader.substring(0, lastSlash) + : ""; + } + + /** Form an include path: `/` if dir is non-empty, else bare ``. */ + private generatedInclude(filename: string): string { + const dir = this.generatedDir(); + return dir ? `${dir}/${filename}` : filename; } /** Compute the include path for the generated client header */ private headerIncludePath(): string { - return `${this.generatedDir()}/${toSnakeCase(this.opts.prefix)}_ipc_client.hpp`; + return this.generatedInclude( + `${toSnakeCase(this.opts.prefix)}_ipc_client.hpp`, + ); } // ----------------------------------------------------------------------- @@ -294,42 +336,65 @@ ${methods} const { namespace: ns, prefix } = this.opts; // Map schema types to C++ types - const mapType = (type: import('./schema_visitor.ts').Type): string => { + const mapType = (type: import("./schema_visitor.ts").Type): string => { switch (type.kind) { - case 'primitive': + case "primitive": switch (type.primitive) { - case 'bool': return 'bool'; - case 'u8': return 'uint8_t'; - case 'u16': return 'uint16_t'; - case 'u32': return 'uint32_t'; - case 'u64': return 'uint64_t'; - case 'f64': return 'double'; - case 'string': return 'std::string'; - case 'bytes': return 'std::vector'; - case 'fr': return 'Fr'; // std::array - case 'field2': return 'std::array'; - case 'enum_u32': return 'uint32_t'; - case 'map_u32_pair': return 'std::unordered_map, uint64_t>>'; + case "bool": + return "bool"; + case "u8": + return "uint8_t"; + case "u16": + return "uint16_t"; + case "u32": + return "uint32_t"; + case "u64": + return "uint64_t"; + case "f64": + return "double"; + case "string": + return "std::string"; + case "bytes": + return "std::vector"; + case "fr": + return "Fr"; // std::array + case "field2": + return "std::array"; + case "enum_u32": + return "uint32_t"; + case "map_u32_pair": + return "std::unordered_map, uint64_t>>"; } break; - case 'vector': return `std::vector<${mapType(type.element!)}>`; - case 'array': return `std::array<${mapType(type.element!)}, ${type.size}>`; - case 'optional': return `std::optional<${mapType(type.element!)}>`; - case 'struct': return type.struct!.name; + case "vector": + return `std::vector<${mapType(type.element!)}>`; + case "array": + return `std::array<${mapType(type.element!)}, ${type.size}>`; + case "optional": + return `std::optional<${mapType(type.element!)}>`; + case "struct": + return type.struct!.name; } - return 'void'; + return "void"; }; - const allStructs = [...schema.structs.values(), ...schema.responses.values()]; - const structs = allStructs.map(s => { - const fields = s.fields.map(f => ` ${mapType(f.type)} ${f.name};`).join('\n'); - const fieldNames = s.fields.map(f => f.name).join(', '); - const schemaName = ` static constexpr const char MSGPACK_SCHEMA_NAME[] = "${s.name}";`; - const serialization = fieldNames - ? ` SERIALIZATION_FIELDS(${fieldNames})` - : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; - return `struct ${s.name} {\n${schemaName}\n${fields}\n${serialization}\n bool operator==(const ${s.name}&) const = default;\n};`; - }).join('\n\n'); + const allStructs = [ + ...schema.structs.values(), + ...schema.responses.values(), + ]; + const structs = allStructs + .map((s) => { + const fields = s.fields + .map((f) => ` ${mapType(f.type)} ${f.name};`) + .join("\n"); + const fieldNames = s.fields.map((f) => f.name).join(", "); + const schemaName = ` static constexpr const char MSGPACK_SCHEMA_NAME[] = "${s.name}";`; + const serialization = fieldNames + ? ` SERIALIZATION_FIELDS(${fieldNames})` + : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; + return `struct ${s.name} {\n${schemaName}\n${fields}\n${serialization}\n bool operator==(const ${s.name}&) const = default;\n};`; + }) + .join("\n\n"); return `// AUTOGENERATED FILE - DO NOT EDIT // Standalone types for ${prefix} service. @@ -381,11 +446,11 @@ ${methods} /// 32-byte field element (Fr/Fq). Fixed-size, stack-allocated. using Fr = std::array; -namespace ${ns}${this.opts.wireNamespace ? '::' + this.opts.wireNamespace : ''} { +namespace ${ns}${this.opts.wireNamespace ? "::" + this.opts.wireNamespace : ""} { ${structs} -} // namespace ${ns}${this.opts.wireNamespace ? '::' + this.opts.wireNamespace : ''} +} // namespace ${ns}${this.opts.wireNamespace ? "::" + this.opts.wireNamespace : ""} `; } @@ -395,27 +460,33 @@ ${structs} const errorType = schema.errorTypeName || `${prefix}ErrorResponse`; const dispatchCases = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { return ` if (cmd_name == "${c.name}") { ${c.name} cmd; cmd_payload.convert(cmd); auto resp = handle_${toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name)}(cmd); pk.pack_array(2); pk.pack(std::string("${c.responseType}")); pk.pack(resp); }`; - }).join(' else '); + }) + .join(" else "); const stubs = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { - const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { + const method = toSnakeCase( + c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name, + ); return `// TODO: implement ${c.name} inline ${c.responseType} handle_${method}(const ${c.name}& /*cmd*/) { throw std::runtime_error("not implemented: ${c.name}"); }`; - }).join('\n\n'); + }) + .join("\n\n"); - const shutdownName = schema.commands.find(c => c.name.endsWith('Shutdown'))?.name || `${prefix}Shutdown`; - const shutdownResp = shutdownName + 'Response'; + const shutdownName = + schema.commands.find((c) => c.name.endsWith("Shutdown"))?.name || + `${prefix}Shutdown`; + const shutdownResp = shutdownName + "Response"; return `// AUTOGENERATED FILE - DO NOT EDIT // ${prefix} server dispatch — standalone, no barretenberg dependencies. @@ -423,7 +494,7 @@ inline ${c.responseType} handle_${method}(const ${c.name}& /*cmd*/) { #pragma once #include "types_gen.hpp" -#include "${this.generatedDir()}/ipc_server.hpp" +#include "${this.generatedInclude("ipc_server.hpp")}" #include namespace ${ns} { @@ -480,12 +551,14 @@ ${stubs} const errorType = schema.errorTypeName || `${prefix}ErrorResponse`; const methods = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { - const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { + const method = toSnakeCase( + c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name, + ); const hasFields = c.fields.length > 0; - const param = hasFields ? `const ${c.name}& cmd` : ''; - const packCmd = hasFields ? 'cmd' : `${c.name}{}`; + const param = hasFields ? `const ${c.name}& cmd` : ""; + const packCmd = hasFields ? "cmd" : `${c.name}{}`; return ` ${c.responseType} ${method}(${param}) { msgpack::sbuffer buf; msgpack::packer pk(buf); @@ -498,14 +571,15 @@ ${stubs} ${c.responseType} result; obj.via.array.ptr[1].convert(result); return result; }`; - }).join('\n\n'); + }) + .join("\n\n"); return `// AUTOGENERATED FILE - DO NOT EDIT // ${prefix} typed IPC client — standalone, no barretenberg dependencies. #pragma once #include "types_gen.hpp" -#include "${this.generatedDir()}/ipc_client.hpp" +#include "${this.generatedInclude("ipc_client.hpp")}" #include namespace ${ns} { @@ -540,36 +614,50 @@ ${methods} * Uses native types (bb::fr, MerkleTreeId, StateReference) instead of * standalone equivalents (Fr, uint32_t, std::unordered_map<...>). */ - private mapTypeBb(type: import('./schema_visitor.ts').Type): string { + private mapTypeBb(type: import("./schema_visitor.ts").Type): string { const externals = this.opts.externals || {}; switch (type.kind) { - case 'primitive': + case "primitive": switch (type.primitive) { - case 'bool': return 'bool'; - case 'u8': return 'uint8_t'; - case 'u16': return 'uint16_t'; - case 'u32': return 'uint32_t'; - case 'u64': return 'uint64_t'; - case 'f64': return 'double'; - case 'string': return 'std::string'; - case 'bytes': return 'std::vector'; - case 'fr': return 'bb::fr'; - case 'field2': return 'std::array'; - case 'enum_u32': + case "bool": + return "bool"; + case "u8": + return "uint8_t"; + case "u16": + return "uint16_t"; + case "u32": + return "uint32_t"; + case "u64": + return "uint64_t"; + case "f64": + return "double"; + case "string": + return "std::string"; + case "bytes": + return "std::vector"; + case "fr": + return "bb::fr"; + case "field2": + return "std::array"; + case "enum_u32": // Preserve original enum name if available - return type.originalName || 'uint32_t'; - case 'map_u32_pair': + return type.originalName || "uint32_t"; + case "map_u32_pair": // StateReference is the only map type we've seen - return 'StateReference'; + return "StateReference"; } break; - case 'vector': return `std::vector<${this.mapTypeBb(type.element!)}>`; - case 'array': return `std::array<${this.mapTypeBb(type.element!)}, ${type.size}>`; - case 'optional': return `std::optional<${this.mapTypeBb(type.element!)}>`; - case 'struct': return type.struct!.name; + case "vector": + return `std::vector<${this.mapTypeBb(type.element!)}>`; + case "array": + return `std::array<${this.mapTypeBb(type.element!)}, ${type.size}>`; + case "optional": + return `std::optional<${this.mapTypeBb(type.element!)}>`; + case "struct": + return type.struct!.name; } - return 'void'; + return "void"; } /** @@ -593,8 +681,13 @@ ${methods} externalIncludes.add(inc); } - const includeLines = [...externalIncludes].sort().map(h => `#include "${h}"`).join('\n'); - const usingLines = (this.opts.usingNamespaces || []).map(ns => `using namespace ${ns};`).join('\n'); + const includeLines = [...externalIncludes] + .sort() + .map((h) => `#include "${h}"`) + .join("\n"); + const usingLines = (this.opts.usingNamespaces || []) + .map((ns) => `using namespace ${ns};`) + .join("\n"); // Generate non-external struct types (only SiblingPathAndIndex-like types // that are NOT commands/responses and NOT external) @@ -603,13 +696,15 @@ ${methods} if (name.startsWith(prefix)) continue; // Commands are generated below if (externals[name]) continue; // External — imported via #include // Generate this helper struct - const fields = struct.fields.map(f => ` ${this.mapTypeBb(f.type)} ${f.name};`).join('\n'); - const fieldNames = struct.fields.map(f => f.name).join(', '); + const fields = struct.fields + .map((f) => ` ${this.mapTypeBb(f.type)} ${f.name};`) + .join("\n"); + const fieldNames = struct.fields.map((f) => f.name).join(", "); const serialization = fieldNames ? ` SERIALIZATION_FIELDS(${fieldNames});` : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; helperStructs.push( - `struct ${name} {\n static constexpr const char MSGPACK_SCHEMA_NAME[] = "${name}";\n${fields}\n${serialization}\n bool operator==(const ${name}&) const = default;\n};` + `struct ${name} {\n static constexpr const char MSGPACK_SCHEMA_NAME[] = "${name}";\n${fields}\n${serialization}\n bool operator==(const ${name}&) const = default;\n};`, ); } for (const [name, struct] of schema.responses) { @@ -619,36 +714,43 @@ ${methods} } // Generate command structs with nested Response and execute() - const commandStructs = schema.commands.map(cmd => { - const respStruct = schema.responses.get(cmd.responseType); - - // Command fields - const cmdFields = cmd.fields.map(f => ` ${this.mapTypeBb(f.type)} ${f.name};`).join('\n'); - const cmdFieldNames = cmd.fields.map(f => f.name).join(', '); - const cmdSerialization = cmdFieldNames - ? ` SERIALIZATION_FIELDS(${cmdFieldNames});` - : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; - - // Response fields - let responseBlock: string; - if (respStruct && respStruct.fields.length > 0) { - const respFields = respStruct.fields.map(f => ` ${this.mapTypeBb(f.type)} ${f.name};`).join('\n'); - const respFieldNames = respStruct.fields.map(f => f.name).join(', '); - responseBlock = ` struct Response { + const commandStructs = schema.commands + .map((cmd) => { + const respStruct = schema.responses.get(cmd.responseType); + + // Command fields + const cmdFields = cmd.fields + .map((f) => ` ${this.mapTypeBb(f.type)} ${f.name};`) + .join("\n"); + const cmdFieldNames = cmd.fields.map((f) => f.name).join(", "); + const cmdSerialization = cmdFieldNames + ? ` SERIALIZATION_FIELDS(${cmdFieldNames});` + : ` template void msgpack(_PackFn&& pack_fn) { pack_fn(); }`; + + // Response fields + let responseBlock: string; + if (respStruct && respStruct.fields.length > 0) { + const respFields = respStruct.fields + .map((f) => ` ${this.mapTypeBb(f.type)} ${f.name};`) + .join("\n"); + const respFieldNames = respStruct.fields + .map((f) => f.name) + .join(", "); + responseBlock = ` struct Response { static constexpr const char MSGPACK_SCHEMA_NAME[] = "${cmd.responseType}"; ${respFields} SERIALIZATION_FIELDS(${respFieldNames}); bool operator==(const Response&) const = default; };`; - } else { - responseBlock = ` struct Response { + } else { + responseBlock = ` struct Response { static constexpr const char MSGPACK_SCHEMA_NAME[] = "${cmd.responseType}"; template void msgpack(_PackFn&& pack_fn) { pack_fn(); } bool operator==(const Response&) const = default; };`; - } + } - return `struct ${cmd.name} { + return `struct ${cmd.name} { static constexpr const char MSGPACK_SCHEMA_NAME[] = "${cmd.name}"; ${responseBlock} ${cmdFields} @@ -656,7 +758,8 @@ ${cmdFields} ${cmdSerialization} bool operator==(const ${cmd.name}&) const = default; };`; - }).join('\n\n'); + }) + .join("\n\n"); return `// AUTOGENERATED FILE - DO NOT EDIT #pragma once @@ -674,7 +777,7 @@ ${usingLines} // Forward declaration struct ${prefix}Request; -${helperStructs.join('\n\n')}${helperStructs.length > 0 ? '\n\n' : ''}// --------------------------------------------------------------------------- +${helperStructs.join("\n\n")}${helperStructs.length > 0 ? "\n\n" : ""}// --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- @@ -696,31 +799,40 @@ ${commandStructs} // Handler declarations — template const handlerDecls = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { - const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { + const method = toSnakeCase( + c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name, + ); return `template\nwire::${c.responseType} handle_${method}(Ctx& ctx, wire::${c.name}&& cmd);`; - }).join('\n\n'); + }) + .join("\n\n"); // Handler entries for dispatch map - const handlerEntries = schema.commands.map(cmd => { - const isShutdown = cmd.name.endsWith('Shutdown'); - const method = toSnakeCase(cmd.name.startsWith(prefix) ? cmd.name.slice(prefix.length) : cmd.name); + const handlerEntries = schema.commands + .map((cmd) => { + const isShutdown = cmd.name.endsWith("Shutdown"); + const method = toSnakeCase( + cmd.name.startsWith(prefix) + ? cmd.name.slice(prefix.length) + : cmd.name, + ); - if (isShutdown) { - return ` { "${cmd.name}", []([[maybe_unused]] Ctx& ctx, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + if (isShutdown) { + return ` { "${cmd.name}", []([[maybe_unused]] Ctx& ctx, [[maybe_unused]] const msgpack::object& payload) -> std::vector { msgpack::sbuffer buf; msgpack::packer pk(buf); pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack_map(0); THROW ::ipc::ShutdownRequested(std::vector(buf.data(), buf.data() + buf.size())); } }`; - } + } - const deserialize = cmd.fields.length > 0 - ? `wire::${cmd.name} wire_cmd; payload.convert(wire_cmd);` - : `wire::${cmd.name} wire_cmd;`; + const deserialize = + cmd.fields.length > 0 + ? `wire::${cmd.name} wire_cmd; payload.convert(wire_cmd);` + : `wire::${cmd.name} wire_cmd;`; - return ` { "${cmd.name}", [](Ctx& ctx, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + return ` { "${cmd.name}", [](Ctx& ctx, [[maybe_unused]] const msgpack::object& payload) -> std::vector { ${deserialize} auto wire_resp = handle_${method}(ctx, std::move(wire_cmd)); msgpack::sbuffer buf; @@ -728,7 +840,8 @@ ${commandStructs} pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack(wire_resp); return std::vector(buf.data(), buf.data() + buf.size()); } }`; - }).join(',\n'); + }) + .join(",\n"); return `// AUTOGENERATED FILE - DO NOT EDIT // Header-only server dispatch — template for service context. @@ -736,9 +849,12 @@ ${commandStructs} #include "${typesHeader}" #include "ipc_server.hpp" +#include "msgpack_struct_map_impl.hpp" -// msgpack headers needed for dispatch implementation. -// Includers within barretenberg must include try_catch_shim.hpp before this header. +// THROW/RETHROW mirror barretenberg's try_catch_shim macros so the same +// generated header works in both standalone builds (where exceptions are +// available) and in WASM builds (where the includer can predefine these to +// abort-with-message variants before #including this header). #ifndef THROW #define THROW throw #define RETHROW throw @@ -843,68 +959,77 @@ void serve(const char* socket_path, Ctx& ctx, std::atomic* shutdown_flag = const requestType = `${prefix}Request`; const errorTypeName = schema.errorTypeName || `${prefix}ErrorResponse`; - const serverHeaderPath = `${this.generatedDir()}/${toSnakeCase(prefix)}_ipc_server.hpp`; + const serverHeaderPath = this.generatedInclude( + `${toSnakeCase(prefix)}_ipc_server.hpp`, + ); // Generate handler lambdas for each command const wireNs = this.opts.wireNamespace; - const handlerEntries = schema.commands.map(cmd => { - const isShutdown = cmd.name.endsWith('Shutdown'); - - // When wireNamespace is set: deserialize wire type, call handle_xxx() which returns wire response - // When not set: wire types ARE domain types, call cmd.execute(request) directly - const method = toSnakeCase(cmd.name.startsWith(prefix) ? cmd.name.slice(prefix.length) : cmd.name); - let body: string; - - if (wireNs) { - if (isShutdown) { - // Shutdown: no handler call, just serialize empty response and throw - body = `msgpack::sbuffer buf; + const handlerEntries = schema.commands + .map((cmd) => { + const isShutdown = cmd.name.endsWith("Shutdown"); + + // When wireNamespace is set: deserialize wire type, call handle_xxx() which returns wire response + // When not set: wire types ARE domain types, call cmd.execute(request) directly + const method = toSnakeCase( + cmd.name.startsWith(prefix) + ? cmd.name.slice(prefix.length) + : cmd.name, + ); + let body: string; + + if (wireNs) { + if (isShutdown) { + // Shutdown: no handler call, just serialize empty response and throw + body = `msgpack::sbuffer buf; msgpack::packer pk(buf); pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack_map(0);`; - } else { - const wireType = `${wireNs}::${cmd.name}`; - const deserialize = cmd.fields.length > 0 - ? `${wireType} wire_cmd; payload.convert(wire_cmd);` - : `${wireType} wire_cmd;`; - body = `${deserialize} + } else { + const wireType = `${wireNs}::${cmd.name}`; + const deserialize = + cmd.fields.length > 0 + ? `${wireType} wire_cmd; payload.convert(wire_cmd);` + : `${wireType} wire_cmd;`; + body = `${deserialize} auto wire_resp = handle_${method}(request, std::move(wire_cmd)); msgpack::sbuffer buf; msgpack::packer pk(buf); pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack(wire_resp);`; - } - } else { - const deserialize = cmd.fields.length > 0 - ? `${cmd.name} cmd; payload.convert(cmd);` - : `${cmd.name} cmd;`; - body = `${deserialize} + } + } else { + const deserialize = + cmd.fields.length > 0 + ? `${cmd.name} cmd; payload.convert(cmd);` + : `${cmd.name} cmd;`; + body = `${deserialize} auto resp = std::move(cmd).execute(request); msgpack::sbuffer buf; msgpack::packer pk(buf); pk.pack_array(2); pk.pack(std::string("${cmd.responseType}")); pk.pack(resp);`; - } + } - if (isShutdown) { - return ` { "${cmd.name}", []([[maybe_unused]] ${requestType}& request, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + if (isShutdown) { + return ` { "${cmd.name}", []([[maybe_unused]] ${requestType}& request, [[maybe_unused]] const msgpack::object& payload) -> std::vector { ${body} throw ::ipc::ShutdownRequested(std::vector(buf.data(), buf.data() + buf.size())); } }`; - } - return ` { "${cmd.name}", [](${requestType}& request, [[maybe_unused]] const msgpack::object& payload) -> std::vector { + } + return ` { "${cmd.name}", [](${requestType}& request, [[maybe_unused]] const msgpack::object& payload) -> std::vector { ${body} return std::vector(buf.data(), buf.data() + buf.size()); } }`; - }).join(',\n'); + }) + .join(",\n"); // Include wire types header when wire/domain split is used const wireTypesInclude = wireNs - ? `#include "${this.generatedDir()}/${toSnakeCase(prefix)}_types.hpp"\n` - : ''; + ? `#include "${this.generatedInclude(`${toSnakeCase(prefix)}_types.hpp`)}"\n` + : ""; return `// AUTOGENERATED FILE - DO NOT EDIT #include "${serverHeaderPath}" -${wireTypesInclude}#include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/serialize/msgpack_impl.hpp" +${wireTypesInclude}#include "msgpack_struct_map_impl.hpp" #include #include @@ -989,15 +1114,18 @@ static std::vector make_error(const std::string& message) const ctxName = `${prefix}Context`; const stubs = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { - const method = toSnakeCase(c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name); + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { + const method = toSnakeCase( + c.name.startsWith(prefix) ? c.name.slice(prefix.length) : c.name, + ); return `template<> wire::${c.responseType} handle_${method}(${ctxName}& /*ctx*/, wire::${c.name}&& /*cmd*/) { throw std::runtime_error("not implemented: ${c.name}"); }`; - }).join('\n\n'); + }) + .join("\n\n"); return `// Handler stubs — implement your service logic here. // This file is generated ONCE. Edit freely — it will not be overwritten. @@ -1106,4 +1234,3 @@ node --experimental-strip-types "$(dirname "$SCRIPT_DIR")/codegen/src/generate.t `; } } - diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index 5af70dbc0f56..fbcdb4546938 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -22,17 +22,17 @@ * Zero npm dependencies — runs with Node.js 22+ via --experimental-strip-types. */ -import { createHash } from 'crypto'; -import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; -import { execSync } from 'child_process'; -import { dirname, join, resolve } from 'path'; -import { fileURLToPath } from 'url'; -import { SchemaVisitor, type CompiledSchema } from './schema_visitor.ts'; -import { TypeScriptCodegen } from './typescript_codegen.ts'; -import { RustCodegen } from './rust_codegen.ts'; -import { ZigCodegen } from './zig_codegen.ts'; -import { CppCodegen } from './cpp_codegen.ts'; -import { toSnakeCase } from './naming.ts'; +import { createHash } from "crypto"; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs"; +import { execSync } from "child_process"; +import { dirname, join, resolve } from "path"; +import { fileURLToPath } from "url"; +import { SchemaVisitor, type CompiledSchema } from "./schema_visitor.ts"; +import { TypeScriptCodegen } from "./typescript_codegen.ts"; +import { RustCodegen } from "./rust_codegen.ts"; +import { ZigCodegen } from "./zig_codegen.ts"; +import { CppCodegen } from "./cpp_codegen.ts"; +import { toSnakeCase } from "./naming.ts"; // @ts-ignore const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -60,29 +60,66 @@ interface Args { function parseArgs(argv: string[]): Args { const args: Args = { - schema: '', lang: '', out: '', prefix: '', - server: false, client: false, skeleton: '', - cppNamespace: '', cppWireNamespace: 'wire', cppIncludeDir: '', - uds: false, ffi: false, - curveConstants: false, stripMethodPrefix: false, + schema: "", + lang: "", + out: "", + prefix: "", + server: false, + client: false, + skeleton: "", + cppNamespace: "", + cppWireNamespace: "wire", + cppIncludeDir: "", + uds: false, + ffi: false, + curveConstants: false, + stripMethodPrefix: false, }; for (let i = 0; i < argv.length; i++) { switch (argv[i]) { - case '--schema': args.schema = argv[++i]; break; - case '--lang': args.lang = argv[++i]; break; - case '--out': args.out = argv[++i]; break; - case '--prefix': args.prefix = argv[++i]; break; - case '--server': args.server = true; break; - case '--client': args.client = true; break; - case '--skeleton': args.skeleton = argv[++i]; break; - case '--cpp-namespace': args.cppNamespace = argv[++i]; break; - case '--cpp-wire-namespace': args.cppWireNamespace = argv[++i]; break; - case '--cpp-include-dir': args.cppIncludeDir = argv[++i]; break; - case '--uds': args.uds = true; break; - case '--ffi': args.ffi = true; break; - case '--curve-constants': args.curveConstants = true; break; - case '--strip-method-prefix': args.stripMethodPrefix = true; break; + case "--schema": + args.schema = argv[++i]; + break; + case "--lang": + args.lang = argv[++i]; + break; + case "--out": + args.out = argv[++i]; + break; + case "--prefix": + args.prefix = argv[++i]; + break; + case "--server": + args.server = true; + break; + case "--client": + args.client = true; + break; + case "--skeleton": + args.skeleton = argv[++i]; + break; + case "--cpp-namespace": + args.cppNamespace = argv[++i]; + break; + case "--cpp-wire-namespace": + args.cppWireNamespace = argv[++i]; + break; + case "--cpp-include-dir": + args.cppIncludeDir = argv[++i]; + break; + case "--uds": + args.uds = true; + break; + case "--ffi": + args.ffi = true; + break; + case "--curve-constants": + args.curveConstants = true; + break; + case "--strip-method-prefix": + args.stripMethodPrefix = true; + break; default: console.error(`Unknown flag: ${argv[i]}`); process.exit(1); @@ -118,11 +155,14 @@ Optional: // --------------------------------------------------------------------------- function computeSchemaHash(schemaJson: string): string { - return createHash('sha256').update(schemaJson).digest('hex'); + return createHash("sha256").update(schemaJson).digest("hex"); } -function loadSchema(schemaPath: string): { compiled: CompiledSchema; schemaHash: string } { - const rawJson = readFileSync(schemaPath, 'utf-8').trim(); +function loadSchema(schemaPath: string): { + compiled: CompiledSchema; + schemaHash: string; +} { + const rawJson = readFileSync(schemaPath, "utf-8").trim(); const schema = JSON.parse(rawJson); const visitor = new SchemaVisitor(); const compiled = visitor.visit(schema.commands, schema.responses); @@ -132,8 +172,8 @@ function loadSchema(schemaPath: string): { compiled: CompiledSchema; schemaHash: /** Detect common prefix from command names (e.g. WsdbGetTreeInfo, WsdbCreateFork → Wsdb) */ function detectPrefix(compiled: CompiledSchema): string { - const names = compiled.commands.map(c => c.name); - if (names.length === 0) return ''; + const names = compiled.commands.map((c) => c.name); + if (names.length === 0) return ""; let prefix = names[0]; for (const name of names.slice(1)) { while (prefix && !name.startsWith(prefix)) { @@ -141,10 +181,10 @@ function detectPrefix(compiled: CompiledSchema): string { } } const words = prefix.match(/[A-Z][a-z]*/g) || []; - let result = ''; + let result = ""; for (const word of words) { const candidate = result + word; - if (names.every(n => n.startsWith(candidate))) { + if (names.every((n) => n.startsWith(candidate))) { result = candidate; } else { break; @@ -158,9 +198,9 @@ function detectPrefix(compiled: CompiledSchema): string { // --------------------------------------------------------------------------- function copyTemplate(lang: string, filename: string, outDir: string) { - const templatePath = join(__dirname, '..', 'templates', lang, filename); + const templatePath = join(__dirname, "..", "templates", lang, filename); const destPath = join(outDir, filename); - writeFileSync(destPath, readFileSync(templatePath, 'utf-8')); + writeFileSync(destPath, readFileSync(templatePath, "utf-8")); console.log(` ${destPath} (template)`); } @@ -181,7 +221,7 @@ function copyTemplateOnce(lang: string, filename: string, outDir: string) { function formatCpp(files: string[]) { if (files.length === 0) return; try { - execSync(`clang-format-20 -i ${files.join(' ')}`, { stdio: 'ignore' }); + execSync(`clang-format-20 -i ${files.join(" ")}`, { stdio: "ignore" }); } catch { // clang-format-20 may not be available } @@ -199,7 +239,9 @@ function generate(args: Args) { const { compiled, schemaHash } = loadSchema(absSchema); const prefix = args.prefix || detectPrefix(compiled); - console.log(`Schema: ${absSchema} (${compiled.commands.length} commands, prefix=${prefix})`); + console.log( + `Schema: ${absSchema} (${compiled.commands.length} commands, prefix=${prefix})`, + ); function writeFile(name: string, content: string) { const path = join(absOut, name); @@ -212,17 +254,19 @@ function generate(args: Args) { const cppFiles: string[] = []; switch (args.lang) { - case 'ts': { - const gen = new TypeScriptCodegen({ stripMethodPrefix: args.stripMethodPrefix ? prefix : undefined }); - writeFile('api_types.ts', gen.generateTypes(compiled, schemaHash)); + case "ts": { + const gen = new TypeScriptCodegen({ + stripMethodPrefix: args.stripMethodPrefix ? prefix : undefined, + }); + writeFile("api_types.ts", gen.generateTypes(compiled, schemaHash)); if (args.server) { - writeFile('server.ts', gen.generateServerApi(compiled)); - copyTemplate('ts', 'ipc_server.ts', absOut); + writeFile("server.ts", gen.generateServerApi(compiled)); + copyTemplate("ts", "ipc_server.ts", absOut); } if (args.client) { - writeFile('async.ts', gen.generateAsyncApi(compiled)); - writeFile('sync.ts', gen.generateSyncApi(compiled)); - copyTemplate('ts', 'ipc_client.ts', absOut); + writeFile("async.ts", gen.generateAsyncApi(compiled)); + writeFile("sync.ts", gen.generateSyncApi(compiled)); + copyTemplate("ts", "ipc_client.ts", absOut); } if (args.curveConstants) { generateCurveConstants(absOut); @@ -231,7 +275,11 @@ function generate(args: Args) { if (args.skeleton) { const skelDir = resolve(args.skeleton); mkdirSync(skelDir, { recursive: true }); - const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const writeSkeleton = ( + name: string, + content: string, + opts?: { executable?: boolean }, + ) => { const path = join(skelDir, name); if (existsSync(path)) { console.log(` ${path} (exists, skipped)`); @@ -239,44 +287,66 @@ function generate(args: Args) { } writeFileSync(path, content); if (opts?.executable) { - try { execSync(`chmod +x ${path}`); } catch {} + try { + execSync(`chmod +x ${path}`); + } catch {} } console.log(` ${path} (skeleton)`); }; - writeSkeleton(`${toSnakeCase(prefix)}_handlers.ts`, gen.generateHandlerStubs(compiled, prefix)); - writeSkeleton('main.ts', gen.generateMain(compiled, prefix)); - writeSkeleton('package.json', gen.generateBuildFile(prefix)); - writeSkeleton('.gitignore', gen.generateGitignore()); - writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema, prefix), { executable: true }); + writeSkeleton( + `${toSnakeCase(prefix)}_handlers.ts`, + gen.generateHandlerStubs(compiled, prefix), + ); + writeSkeleton("main.ts", gen.generateMain(compiled, prefix)); + writeSkeleton("package.json", gen.generateBuildFile(prefix)); + writeSkeleton(".gitignore", gen.generateGitignore()); + writeSkeleton( + "generate.sh", + gen.generateGenerateScript(args.schema, prefix), + { executable: true }, + ); } break; } - case 'rust': { + case "rust": { const gen = new RustCodegen({ prefix }); - writeFile(`${toSnakeCase(prefix)}_types.rs`, gen.generateTypes(compiled, schemaHash)); + writeFile( + `${toSnakeCase(prefix)}_types.rs`, + gen.generateTypes(compiled, schemaHash), + ); if (args.server) { - writeFile(`${toSnakeCase(prefix)}_server.rs`, gen.generateServer(compiled)); - copyTemplate('rust', 'ipc_server.rs', absOut); + writeFile( + `${toSnakeCase(prefix)}_server.rs`, + gen.generateServer(compiled), + ); + copyTemplate("rust", "ipc_server.rs", absOut); } if (args.client) { - writeFile(`${toSnakeCase(prefix)}_client.rs`, gen.generateApi(compiled)); + writeFile( + `${toSnakeCase(prefix)}_client.rs`, + gen.generateApi(compiled), + ); } // Backend templates (copied once, not overwritten) if (args.uds || args.ffi) { - copyTemplateOnce('rust', 'backend.rs', absOut); - copyTemplateOnce('rust', 'error.rs', absOut); + copyTemplateOnce("rust", "backend.rs", absOut); + copyTemplateOnce("rust", "error.rs", absOut); } if (args.uds) { - copyTemplateOnce('rust', 'uds_backend.rs', absOut); + copyTemplateOnce("rust", "uds_backend.rs", absOut); } if (args.ffi) { - copyTemplateOnce('rust', 'ffi_backend.rs', absOut); + copyTemplateOnce("rust", "ffi_backend.rs", absOut); } // Skeleton (one-time handler stubs + main + build files) if (args.skeleton) { const skelDir = resolve(args.skeleton); mkdirSync(skelDir, { recursive: true }); - const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const writeSkeleton = ( + name: string, + content: string, + opts?: { executable?: boolean }, + ) => { const path = join(skelDir, name); if (existsSync(path)) { console.log(` ${path} (exists, skipped)`); @@ -284,43 +354,63 @@ function generate(args: Args) { } writeFileSync(path, content); if (opts?.executable) { - try { execSync(`chmod +x ${path}`); } catch {} + try { + execSync(`chmod +x ${path}`); + } catch {} } console.log(` ${path} (skeleton)`); }; - writeSkeleton(`${toSnakeCase(prefix)}_handlers.rs`, gen.generateHandlerStubs(compiled)); - writeSkeleton('main.rs', gen.generateMain(compiled)); - writeSkeleton('Cargo.toml', gen.generateBuildFile(compiled)); - writeSkeleton('.gitignore', gen.generateGitignore()); - writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema), { executable: true }); + writeSkeleton( + `${toSnakeCase(prefix)}_handlers.rs`, + gen.generateHandlerStubs(compiled), + ); + writeSkeleton("main.rs", gen.generateMain(compiled)); + writeSkeleton("Cargo.toml", gen.generateBuildFile(compiled)); + writeSkeleton(".gitignore", gen.generateGitignore()); + writeSkeleton("generate.sh", gen.generateGenerateScript(args.schema), { + executable: true, + }); } break; } - case 'zig': { + case "zig": { const gen = new ZigCodegen({ prefix, clientName: `${prefix}Client` }); - writeFile(`${toSnakeCase(prefix)}_types.zig`, gen.generateTypes(compiled, schemaHash)); + writeFile( + `${toSnakeCase(prefix)}_types.zig`, + gen.generateTypes(compiled, schemaHash), + ); if (args.server) { - writeFile(`${toSnakeCase(prefix)}_server.zig`, gen.generateServer(compiled)); - copyTemplate('zig', 'ipc_server.zig', absOut); + writeFile( + `${toSnakeCase(prefix)}_server.zig`, + gen.generateServer(compiled), + ); + copyTemplate("zig", "ipc_server.zig", absOut); } if (args.client) { - writeFile(`${toSnakeCase(prefix)}_client.zig`, gen.generateClient(compiled)); + writeFile( + `${toSnakeCase(prefix)}_client.zig`, + gen.generateClient(compiled), + ); } // Backend templates (copied once, not overwritten) if (args.uds || args.ffi) { - copyTemplateOnce('zig', 'backend.zig', absOut); + copyTemplateOnce("zig", "backend.zig", absOut); } if (args.uds) { - copyTemplateOnce('zig', 'uds_backend.zig', absOut); + copyTemplateOnce("zig", "uds_backend.zig", absOut); } if (args.ffi) { - copyTemplateOnce('zig', 'ffi_backend.zig', absOut); + copyTemplateOnce("zig", "ffi_backend.zig", absOut); } // Skeleton (one-time handler stubs + main + build files) if (args.skeleton) { const skelDir = resolve(args.skeleton); mkdirSync(skelDir, { recursive: true }); - const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const writeSkeleton = ( + name: string, + content: string, + opts?: { executable?: boolean }, + ) => { const path = join(skelDir, name); if (existsSync(path)) { console.log(` ${path} (exists, skipped)`); @@ -328,47 +418,83 @@ function generate(args: Args) { } writeFileSync(path, content); if (opts?.executable) { - try { execSync(`chmod +x ${path}`); } catch {} + try { + execSync(`chmod +x ${path}`); + } catch {} } console.log(` ${path} (skeleton)`); }; - writeSkeleton(`${toSnakeCase(prefix)}_handlers.zig`, gen.generateHandlerStubs(compiled)); - writeSkeleton('main.zig', gen.generateMain(compiled)); - writeSkeleton('build.zig', gen.generateBuildFile(compiled)); - writeSkeleton('build.zig.zon', gen.generateBuildZon(compiled)); - writeSkeleton('.gitignore', gen.generateGitignore()); - writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema), { executable: true }); + writeSkeleton( + `${toSnakeCase(prefix)}_handlers.zig`, + gen.generateHandlerStubs(compiled), + ); + writeSkeleton("main.zig", gen.generateMain(compiled)); + writeSkeleton("build.zig", gen.generateBuildFile(compiled)); + writeSkeleton("build.zig.zon", gen.generateBuildZon(compiled)); + writeSkeleton(".gitignore", gen.generateGitignore()); + writeSkeleton("generate.sh", gen.generateGenerateScript(args.schema), { + executable: true, + }); } break; } - case 'cpp': { + case "cpp": { const ns = args.cppNamespace || prefix.toLowerCase(); const wireNs = args.cppWireNamespace; const gen = new CppCodegen({ namespace: ns, prefix, - executeHeader: '', - commandsHeader: '', + executeHeader: "", + commandsHeader: "", wireNamespace: wireNs, generatedIncludeDir: args.cppIncludeDir, }); - cppFiles.push(writeFile(`${toSnakeCase(prefix)}_types.hpp`, gen.generateStandaloneTypes(compiled))); + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_types.hpp`, + gen.generateStandaloneTypes(compiled), + ), + ); + if (args.server || args.client) { + // Bundled msgpack adaptor — keeps the generated client/server free of + // any framework-specific msgpack includes. + copyTemplate("cpp", "msgpack_struct_map_impl.hpp", absOut); + } if (args.server) { - cppFiles.push(writeFile(`${toSnakeCase(prefix)}_ipc_server.hpp`, gen.generateServerHeader(compiled))); - copyTemplate('cpp', 'ipc_server.hpp', absOut); + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_ipc_server.hpp`, + gen.generateServerHeader(compiled), + ), + ); + copyTemplate("cpp", "ipc_server.hpp", absOut); } if (args.client) { - cppFiles.push(writeFile(`${toSnakeCase(prefix)}_ipc_client.hpp`, gen.generateHeader(compiled, schemaHash))); - cppFiles.push(writeFile(`${toSnakeCase(prefix)}_ipc_client.cpp`, gen.generateImpl(compiled))); - copyTemplate('cpp', 'ipc_client.hpp', absOut); + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_ipc_client.hpp`, + gen.generateHeader(compiled, schemaHash), + ), + ); + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_ipc_client.cpp`, + gen.generateImpl(compiled), + ), + ); + copyTemplate("cpp", "ipc_client.hpp", absOut); } // Skeleton (one-time handler stubs + main + build files) if (args.skeleton) { const skelDir = resolve(args.skeleton); mkdirSync(skelDir, { recursive: true }); - const writeSkeleton = (name: string, content: string, opts?: { executable?: boolean }) => { + const writeSkeleton = ( + name: string, + content: string, + opts?: { executable?: boolean }, + ) => { const path = join(skelDir, name); if (existsSync(path)) { console.log(` ${path} (exists, skipped)`); @@ -376,50 +502,64 @@ function generate(args: Args) { } writeFileSync(path, content); if (opts?.executable) { - try { execSync(`chmod +x ${path}`); } catch {} + try { + execSync(`chmod +x ${path}`); + } catch {} } console.log(` ${path} (skeleton)`); - if (path.endsWith('.cpp') || path.endsWith('.hpp')) { + if (path.endsWith(".cpp") || path.endsWith(".hpp")) { cppFiles.push(path); } }; - writeSkeleton(`${toSnakeCase(prefix)}_handlers.cpp`, gen.generateHandlerStubs(compiled)); - writeSkeleton('main.cpp', gen.generateMain(compiled)); - writeSkeleton('CMakeLists.txt', gen.generateBuildFile(compiled)); - writeSkeleton('.gitignore', gen.generateGitignore()); - writeSkeleton('generate.sh', gen.generateGenerateScript(args.schema), { executable: true }); + writeSkeleton( + `${toSnakeCase(prefix)}_handlers.cpp`, + gen.generateHandlerStubs(compiled), + ); + writeSkeleton("main.cpp", gen.generateMain(compiled)); + writeSkeleton("CMakeLists.txt", gen.generateBuildFile(compiled)); + writeSkeleton(".gitignore", gen.generateGitignore()); + writeSkeleton("generate.sh", gen.generateGenerateScript(args.schema), { + executable: true, + }); } formatCpp(cppFiles); break; } default: - console.error(`Unknown language: ${args.lang}. Available: ts, rust, zig, cpp`); + console.error( + `Unknown language: ${args.lang}. Available: ts, rust, zig, cpp`, + ); process.exit(1); } - console.log('Done.'); + console.log("Done."); } // --------------------------------------------------------------------------- // Curve constants (special case for bb) // --------------------------------------------------------------------------- -function hexToBigInt(hex: string): bigint { return BigInt('0x' + hex); } +function hexToBigInt(hex: string): bigint { + return BigInt("0x" + hex); +} function hexToByteList(hex: string): string { const bytes: number[] = []; - for (let i = 0; i < hex.length; i += 2) bytes.push(parseInt(hex.substring(i, i + 2), 16)); - return `new Uint8Array([${bytes.join(', ')}])`; + for (let i = 0; i < hex.length; i += 2) + bytes.push(parseInt(hex.substring(i, i + 2), 16)); + return `new Uint8Array([${bytes.join(", ")}])`; } function serializeCoordinate(coord: string | string[]): string { - return Array.isArray(coord) ? `[${coord.map(c => hexToByteList(c)).join(', ')}]` : hexToByteList(coord); + return Array.isArray(coord) + ? `[${coord.map((c) => hexToByteList(c)).join(", ")}]` + : hexToByteList(coord); } function generateCurveConstants(outputDir: string) { - const constantsPath = join(__dirname, '../schemas/bb_curve_constants.json'); - const constants = JSON.parse(readFileSync(constantsPath, 'utf-8')); + const constantsPath = join(__dirname, "../schemas/bb_curve_constants.json"); + const constants = JSON.parse(readFileSync(constantsPath, "utf-8")); const content = `// AUTOGENERATED FILE - DO NOT EDIT export const BN254_FR_MODULUS = ${hexToBigInt(constants.bn254_fr_modulus)}n; export const BN254_FQ_MODULUS = ${hexToBigInt(constants.bn254_fq_modulus)}n; @@ -436,8 +576,8 @@ export const SECP256R1_FQ_MODULUS = ${hexToBigInt(constants.secp256r1_fq_modulus export const SECP256R1_G1_GENERATOR = { x: ${serializeCoordinate(constants.secp256r1_g1_generator.x)}, y: ${serializeCoordinate(constants.secp256r1_g1_generator.y)} } as const; `; mkdirSync(outputDir, { recursive: true }); - writeFileSync(join(outputDir, 'curve_constants.ts'), content); - console.log(` ${join(outputDir, 'curve_constants.ts')}`); + writeFileSync(join(outputDir, "curve_constants.ts"), content); + console.log(` ${join(outputDir, "curve_constants.ts")}`); } // --------------------------------------------------------------------------- diff --git a/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp b/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp new file mode 100644 index 000000000000..ba53ddff39ac --- /dev/null +++ b/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp @@ -0,0 +1,96 @@ +#pragma once +// +// msgpack adaptor: pack/unpack types that declare their fields via the +// SERIALIZATION_FIELDS macro into a JSON-like map. Bundled with ipc-codegen +// so generated IPC clients/servers don't pull in any framework-specific +// msgpack headers. +// +// Lifted from barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/ +// (struct_map_impl.hpp + concepts.hpp + drop_keys.hpp), trimmed to just +// what the generated dispatch needs. + +#include +#include +#include +#include + +// --- concepts ------------------------------------------------------------- + +struct IpcCodegenDoNothing { + void operator()(auto...) {} +}; + +namespace ipc_codegen::msgpack_concepts { +template +concept HasMsgPack = requires(T t, IpcCodegenDoNothing nop) { t.msgpack(nop); }; + +template +concept MsgpackConstructible = requires(T object, Args... args) { T{args...}; }; +} // namespace ipc_codegen::msgpack_concepts + +// --- drop_keys ------------------------------------------------------------ +// SERIALIZATION_FIELDS' msgpack() callback receives args interleaved as +// (key0, val0, key1, val1, …). drop_keys strips the keys so we can check +// that the type is constructible from the values. + +namespace ipc_codegen::msgpack_detail { +template +auto drop_keys_impl(Tuple &&tuple, std::index_sequence) { + return std::tie(std::get(std::forward(tuple))...); +} + +template auto drop_keys(std::tuple &&tuple) { + static_assert(sizeof...(Args) % 2 == 0, + "Tuple must contain an even number of elements"); + return drop_keys_impl(tuple, std::make_index_sequence{}); +} +} // namespace ipc_codegen::msgpack_detail + +// --- adaptors ------------------------------------------------------------- + +namespace msgpack::adaptor { + +template struct convert { + msgpack::object const &operator()(msgpack::object const &o, T &v) const { + static_assert(std::is_default_constructible_v, + "SERIALIZATION_FIELDS requires default-constructible types"); + v.msgpack([&](auto &...args) { + auto static_checker = [&](auto &...value_args) { + static_assert(ipc_codegen::msgpack_concepts::MsgpackConstructible< + T, decltype(value_args)...>, + "SERIALIZATION_FIELDS requires a constructor that can " + "take the listed field types"); + }; + if constexpr (!requires { typename T::MSGPACK_NO_STATIC_CHECK; }) { + std::apply(static_checker, + ipc_codegen::msgpack_detail::drop_keys(std::tie(args...))); + } + msgpack::type::define_map{args...}.msgpack_unpack(o); + }); + return o; + } +}; + +template struct pack { + template + packer &operator()(msgpack::packer &o, T const &v) const { + static_assert(std::is_default_constructible_v, + "SERIALIZATION_FIELDS requires default-constructible types"); + const_cast(v).msgpack([&](auto &...args) { + auto static_checker = [&](auto &...value_args) { + static_assert(ipc_codegen::msgpack_concepts::MsgpackConstructible< + T, decltype(value_args)...>, + "SERIALIZATION_FIELDS requires a constructor that can " + "take the listed field types"); + }; + if constexpr (!requires { typename T::MSGPACK_NO_STATIC_CHECK; }) { + std::apply(static_checker, + ipc_codegen::msgpack_detail::drop_keys(std::tie(args...))); + } + msgpack::type::define_map{args...}.msgpack_pack(o); + }); + return o; + } +}; + +} // namespace msgpack::adaptor From 5be3e3c0ca926bf49ef1411251768d11144165e1 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 18:27:01 +0000 Subject: [PATCH 05/11] chore(ipc-codegen): expand golden corpus into a wire-format contract Goldens now cover the msgpack encoding boundaries that codegen tweaks are most likely to silently break: - Variable-width integer encodings (fixint / uint8 / uint16 / uint32 / uint64) including u32::MAX and u64::MAX - String encodings (fixstr / str8 / str16) plus multi-byte UTF-8 - Bin encodings (bin8 / bin16) including bin-len-0 - Optional = Some(true), Some(false), and None - Empty containers (Vec::new(), vec![Vec::new()], etc.) Each language's golden test now both (a) decodes the bytes into the expected typed value AND (b) re-encodes and asserts byte-identical output, which pins down canonical msgpack output. Goldens are committed and frozen. Run ./bootstrap.sh update_goldens when intentionally changing the wire format, and review the binary diff as a breaking-change signal for external implementations. While here: - Set msgpackr Encoder to variableMapSize: true everywhere it's used (test, ipc_client.ts/ipc_server.ts templates, typescript_codegen.ts) so it emits canonical fixmap for small maps instead of always reaching for map16. - One narrow exemption: msgpackr encodes positive BigInt as int64 (`d3`) while rmp-serde uses uint64 (`cf`). Both encodings decode to the same value across every msgpack lib, so wire interop is preserved. echo_fields_uint_boundary opts out of strict roundtrip with a comment explaining why. --- ipc-codegen/bootstrap.sh | 16 +- .../golden/echo_bytes_bin16.msgpack | Bin 0 -> 277 bytes .../golden/echo_bytes_empty.msgpack | Bin 0 -> 20 bytes .../golden/echo_fields_max.msgpack | 1 + .../golden/echo_fields_str16.msgpack | Bin 0 -> 328 bytes .../golden/echo_fields_uint_boundary.msgpack | Bin 0 -> 36 bytes .../golden/echo_fields_unicode.msgpack | Bin 0 -> 54 bytes .../golden/echo_nested_flag_false.msgpack | Bin 0 -> 37 bytes .../golden/echo_nested_flag_none.msgpack | 1 + .../rust/echo/src/bin/generate_golden.rs | 154 +++++++-- .../examples/rust/echo/src/bin/golden_test.rs | 310 +++++++++++++----- ipc-codegen/examples/ts/echo/golden_test.ts | 258 ++++++++++----- ipc-codegen/src/typescript_codegen.ts | 4 +- ipc-codegen/templates/ts/ipc_client.ts | 2 +- ipc-codegen/templates/ts/ipc_server.ts | 2 +- 15 files changed, 550 insertions(+), 198 deletions(-) create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_bytes_bin16.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_bytes_empty.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_fields_max.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_fields_str16.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_fields_uint_boundary.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_fields_unicode.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_nested_flag_false.msgpack create mode 100644 ipc-codegen/examples/echo-schema/golden/echo_nested_flag_none.msgpack diff --git a/ipc-codegen/bootstrap.sh b/ipc-codegen/bootstrap.sh index d196159a5735..9adc773cfb90 100755 --- a/ipc-codegen/bootstrap.sh +++ b/ipc-codegen/bootstrap.sh @@ -49,9 +49,21 @@ function build { echo "Skipping C++ echo build — msgpack-c not present at ../barretenberg/cpp/build/_deps/" fi - # Pre-bake golden fixtures from the Rust reference so the per-language - # golden tests are pure deserialization (no shared write-then-read). + # NB: the golden msgpack fixtures under examples/echo-schema/golden/ are + # COMMITTED and FROZEN — they're the binding wire-format contract. Don't + # regenerate them here. If a deliberate wire-format change requires + # refreshing them, run `./bootstrap.sh update_goldens` and commit the diff. +} + +function update_goldens { + echo_header "ipc-codegen update_goldens" + # Rebuild the rust generate_golden binary first. + (cd examples/rust/echo && cargo build --quiet --bin generate_golden) examples/rust/echo/target/debug/generate_golden --output-dir examples/echo-schema/golden + echo "" + echo "Goldens refreshed. Review the diff carefully — these are the wire-format" + echo "contract, and any byte-level change is a breaking change for external" + echo "implementations of the schema." } function test_cmds { diff --git a/ipc-codegen/examples/echo-schema/golden/echo_bytes_bin16.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_bytes_bin16.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..a24108950f18a6f4af4eee6309321f828a56ecfc GIT binary patch literal 277 gcmbO@X{Bp&M!r*JNosN9l9a@f#G{N1t425g0Gp?>*Z=?k literal 0 HcmV?d00001 diff --git a/ipc-codegen/examples/echo-schema/golden/echo_bytes_empty.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_bytes_empty.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..08696c9d133f1b65e151fc03b76c24fb9a41d093 GIT binary patch literal 20 bcmbO@X{Bp&M!r*JNosN9l9a@f#3Kv1> MfFNyQ!?9s*VX=Z5HUIzs literal 0 HcmV?d00001 diff --git a/ipc-codegen/examples/echo-schema/golden/echo_nested_flag_false.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_nested_flag_false.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..30af0cb49794aebc9e1c8a74144365a9b231fb05 GIT binary patch literal 37 tcmbO@X_aeoM!sKaaY<@QE$ywswmWo3yurK!aek1#Ar%SlW>1OPxo5dQ!G literal 0 HcmV?d00001 diff --git a/ipc-codegen/examples/echo-schema/golden/echo_nested_flag_none.msgpack b/ipc-codegen/examples/echo-schema/golden/echo_nested_flag_none.msgpack new file mode 100644 index 000000000000..b5d2addb83d6 --- /dev/null +++ b/ipc-codegen/examples/echo-schema/golden/echo_nested_flag_none.msgpack @@ -0,0 +1 @@ +EchoNestedinnervaluesflag \ No newline at end of file diff --git a/ipc-codegen/examples/rust/echo/src/bin/generate_golden.rs b/ipc-codegen/examples/rust/echo/src/bin/generate_golden.rs index 6f53dd283fa0..bacb28c7355d 100644 --- a/ipc-codegen/examples/rust/echo/src/bin/generate_golden.rs +++ b/ipc-codegen/examples/rust/echo/src/bin/generate_golden.rs @@ -1,8 +1,17 @@ //! Generate golden msgpack files for wire compatibility testing. //! Usage: generate_golden --output-dir golden/ //! -//! Outputs one .msgpack file per test command (request format: [[name, {fields}]]) -//! and one per response. +//! The goldens are a binding wire-format contract: any new implementation +//! of the echo service (in any language) must decode these bytes into the +//! expected values, and re-encode the same inputs back to byte-identical +//! output. They cover the msgpack encoding boundaries that codegen tweaks +//! are most likely to silently break: +//! +//! - Variable-width integer encodings (fixint / uint8 / uint16 / uint32 / uint64) +//! - String encodings (fixstr / str8 / str16) plus multi-byte UTF-8 +//! - Bin encodings (bin8 / bin16) +//! - Optional = Some vs None +//! - Empty containers use echo_wire_compat::types_gen::*; use std::fs; @@ -10,55 +19,148 @@ use std::path::Path; fn main() { let args: Vec = std::env::args().collect(); - let output_dir = args.iter() + let output_dir = args + .iter() .position(|a| a == "--output-dir") .and_then(|i| args.get(i + 1)) .expect("Usage: generate_golden --output-dir "); fs::create_dir_all(output_dir).unwrap(); - // Request format: [command] — serialized as Vec (1-element array) - write_golden(output_dir, "echo_bytes_request.msgpack", &vec![ - Command::EchoBytes(EchoBytes::new(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42])) - ]); + // ---------------------------------------------------------------------- + // Original happy-path cases. + // ---------------------------------------------------------------------- + write_request( + output_dir, + "echo_bytes_request.msgpack", + Command::EchoBytes(EchoBytes::new(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42])), + ); - write_golden(output_dir, "echo_fields_request.msgpack", &vec![ - Command::EchoFields(EchoFields::new(42, 999999, "hello wire compat".to_string())) - ]); + write_request( + output_dir, + "echo_fields_request.msgpack", + Command::EchoFields(EchoFields::new(42, 999999, "hello wire compat".to_string())), + ); - write_golden(output_dir, "echo_nested_request.msgpack", &vec![ + write_request( + output_dir, + "echo_nested_request.msgpack", Command::EchoNested(EchoNested::new(EchoInner { values: vec![vec![1, 2, 3], vec![4, 5]], flag: Some(true), - })) - ]); + })), + ); - // Response format: NamedUnion (no tuple wrapper) - write_golden(output_dir, "echo_bytes_response.msgpack", - &Response::EchoBytesResponse(EchoBytesResponse { + write_response( + output_dir, + "echo_bytes_response.msgpack", + Response::EchoBytesResponse(EchoBytesResponse { data: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42], - })); + }), + ); - write_golden(output_dir, "echo_fields_response.msgpack", - &Response::EchoFieldsResponse(EchoFieldsResponse { + write_response( + output_dir, + "echo_fields_response.msgpack", + Response::EchoFieldsResponse(EchoFieldsResponse { a: 42, b: 999999, name: "hello wire compat".to_string(), - })); + }), + ); - write_golden(output_dir, "echo_nested_response.msgpack", - &Response::EchoNestedResponse(EchoNestedResponse { + write_response( + output_dir, + "echo_nested_response.msgpack", + Response::EchoNestedResponse(EchoNestedResponse { inner: EchoInner { values: vec![vec![1, 2, 3], vec![4, 5]], flag: Some(true), }, - })); + }), + ); + + // ---------------------------------------------------------------------- + // Boundary cases — these are what catch silent format regressions. + // ---------------------------------------------------------------------- + + // Empty Vec. bin8-with-len-0 vs bin16-with-len-0 vs absent — picks one. + write_request( + output_dir, + "echo_bytes_empty.msgpack", + Command::EchoBytes(EchoBytes::new(vec![])), + ); + + // 256-byte Vec. Crosses the bin8 → bin16 boundary (bin8 max is 255). + write_request( + output_dir, + "echo_bytes_bin16.msgpack", + Command::EchoBytes(EchoBytes::new(vec![0xAA; 256])), + ); + + // u32::MAX (= 2^32 - 1) and u64::MAX. Largest uint encodings; empty string + // exercises fixstr-len-0 framing. + write_request( + output_dir, + "echo_fields_max.msgpack", + Command::EchoFields(EchoFields::new(u32::MAX, u64::MAX, String::new())), + ); - eprintln!("Generated 6 golden files in {}", output_dir); + // u32 = 128 (smallest uint8) and u64 above u32::MAX (forces uint64 encoding). + write_request( + output_dir, + "echo_fields_uint_boundary.msgpack", + Command::EchoFields(EchoFields::new(128, (u32::MAX as u64) + 1, "x".to_string())), + ); + + // Multi-byte UTF-8 in name. Catches encoders that mistakenly count bytes + // by char-count, or that switch str/bin tags depending on content. + write_request( + output_dir, + "echo_fields_unicode.msgpack", + Command::EchoFields(EchoFields::new(0, 0, "héllo τέστ 🚀 mañana".to_string())), + ); + + // 300-char ASCII string. Crosses fixstr (≤31) → str8 (≤255) → str16 boundary. + write_request( + output_dir, + "echo_fields_str16.msgpack", + Command::EchoFields(EchoFields::new(0, 0, "a".repeat(300))), + ); + + // Optional = None plus empty outer Vec>. + write_request( + output_dir, + "echo_nested_flag_none.msgpack", + Command::EchoNested(EchoNested::new(EchoInner { + values: vec![], + flag: None, + })), + ); + + // Optional = Some(false) plus a Vec> containing an empty inner. + write_request( + output_dir, + "echo_nested_flag_false.msgpack", + Command::EchoNested(EchoNested::new(EchoInner { + values: vec![vec![]], + flag: Some(false), + })), + ); + + eprintln!("Generated golden files in {}", output_dir); +} + +fn write_request(dir: &str, name: &str, command: Command) { + let value = vec![command]; + let bytes = rmp_serde::to_vec_named(&value).unwrap(); + let path = Path::new(dir).join(name); + fs::write(&path, &bytes).unwrap(); + eprintln!(" {} ({} bytes)", name, bytes.len()); } -fn write_golden(dir: &str, name: &str, value: &T) { - let bytes = rmp_serde::to_vec_named(value).unwrap(); +fn write_response(dir: &str, name: &str, response: Response) { + let bytes = rmp_serde::to_vec_named(&response).unwrap(); let path = Path::new(dir).join(name); fs::write(&path, &bytes).unwrap(); eprintln!(" {} ({} bytes)", name, bytes.len()); diff --git a/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs b/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs index 3f52379ceb6a..257723ac4627 100644 --- a/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs +++ b/ipc-codegen/examples/rust/echo/src/bin/golden_test.rs @@ -1,13 +1,16 @@ -//! Golden file deserialization test (Rust). -//! Verifies Rust can deserialize the golden msgpack files. -//! Usage: golden_test --golden-dir golden/ +//! Golden file wire-format conformance test (Rust). +//! For each golden file, asserts: +//! 1. We can decode the bytes into the expected typed value. +//! 2. Re-encoding the same value produces byte-identical output. +//! The combination pins down the wire format as a binding contract. use echo_wire_compat::types_gen::*; use std::fs; fn main() { let args: Vec = std::env::args().collect(); - let golden_dir = args.iter() + let dir = args + .iter() .position(|a| a == "--golden-dir") .and_then(|i| args.get(i + 1)) .expect("Usage: golden_test --golden-dir "); @@ -15,95 +18,236 @@ fn main() { let mut pass = 0; let mut fail = 0; - // Request golden files - match check_request::(golden_dir, "echo_bytes_request.msgpack") { - Ok(cmd) => { - assert_eq!(cmd.data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]); - eprintln!(" PASS: echo_bytes_request.msgpack"); - pass += 1; - } - Err(e) => { eprintln!(" FAIL: echo_bytes_request.msgpack: {e}"); fail += 1; } - } + // Helpers close over (pass, fail) via outparams. + let bytes_eq = |a: &[u8], b: &[u8]| -> bool { a == b }; - match check_request::(golden_dir, "echo_fields_request.msgpack") { - Ok(cmd) => { - assert_eq!(cmd.a, 42); - assert_eq!(cmd.b, 999999); - assert_eq!(cmd.name, "hello wire compat"); - eprintln!(" PASS: echo_fields_request.msgpack"); - pass += 1; - } - Err(e) => { eprintln!(" FAIL: echo_fields_request.msgpack: {e}"); fail += 1; } - } + // ------ Request goldens (wire format: Vec) ------ - match check_request::(golden_dir, "echo_nested_request.msgpack") { - Ok(cmd) => { - assert_eq!(cmd.inner.values, vec![vec![1u8, 2, 3], vec![4, 5]]); - assert_eq!(cmd.inner.flag, Some(true)); - eprintln!(" PASS: echo_nested_request.msgpack"); - pass += 1; - } - Err(e) => { eprintln!(" FAIL: echo_nested_request.msgpack: {e}"); fail += 1; } + macro_rules! check_request { + ($file:expr, $variant:ident, $expect_check:expr) => {{ + let path = format!("{dir}/{}", $file); + match fs::read(&path) { + Err(e) => { eprintln!(" FAIL: {}: read: {e}", $file); fail += 1; } + Ok(bytes) => { + match rmp_serde::from_slice::>(&bytes) { + Err(e) => { eprintln!(" FAIL: {}: decode: {e}", $file); fail += 1; } + Ok(cmds) if cmds.len() != 1 => { + eprintln!(" FAIL: {}: expected 1 command, got {}", $file, cmds.len()); + fail += 1; + } + Ok(cmds) => match cmds.into_iter().next().unwrap() { + Command::$variant(v) => { + let check_fn: fn(&_) -> Result<(), String> = $expect_check; + if let Err(e) = check_fn(&v) { + eprintln!(" FAIL: {}: {e}", $file); + fail += 1; + } else { + // Roundtrip: re-encode and compare bytes. + let re = rmp_serde::to_vec_named(&vec![Command::$variant(v)]).unwrap(); + if !bytes_eq(&re, &bytes) { + eprintln!(" FAIL: {}: roundtrip byte mismatch ({} vs {} bytes)", + $file, re.len(), bytes.len()); + fail += 1; + } else { + eprintln!(" PASS: {}", $file); + pass += 1; + } + } + } + other => { + eprintln!(" FAIL: {}: wrong variant ({:?})", $file, std::mem::discriminant(&other)); + fail += 1; + } + } + } + } + } + }}; } - // Response golden files - match check_response(golden_dir, "echo_bytes_response.msgpack") { - Ok(Response::EchoBytesResponse(r)) => { - assert_eq!(r.data, vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]); - eprintln!(" PASS: echo_bytes_response.msgpack"); - pass += 1; - } - Ok(_) => { eprintln!(" FAIL: echo_bytes_response.msgpack: wrong variant"); fail += 1; } - Err(e) => { eprintln!(" FAIL: echo_bytes_response.msgpack: {e}"); fail += 1; } + // ------ Response goldens (wire format: Response, NamedUnion) ------ + + macro_rules! check_response { + ($file:expr, $variant:ident, $expect_check:expr) => {{ + let path = format!("{dir}/{}", $file); + match fs::read(&path) { + Err(e) => { + eprintln!(" FAIL: {}: read: {e}", $file); + fail += 1; + } + Ok(bytes) => match rmp_serde::from_slice::(&bytes) { + Err(e) => { + eprintln!(" FAIL: {}: decode: {e}", $file); + fail += 1; + } + Ok(Response::$variant(v)) => { + let check_fn: fn(&_) -> Result<(), String> = $expect_check; + if let Err(e) = check_fn(&v) { + eprintln!(" FAIL: {}: {e}", $file); + fail += 1; + } else { + let re = rmp_serde::to_vec_named(&Response::$variant(v)).unwrap(); + if !bytes_eq(&re, &bytes) { + eprintln!(" FAIL: {}: roundtrip byte mismatch", $file); + fail += 1; + } else { + eprintln!(" PASS: {}", $file); + pass += 1; + } + } + } + Ok(_) => { + eprintln!(" FAIL: {}: wrong variant", $file); + fail += 1; + } + }, + } + }}; } - match check_response(golden_dir, "echo_fields_response.msgpack") { - Ok(Response::EchoFieldsResponse(r)) => { - assert_eq!(r.a, 42); - assert_eq!(r.b, 999999); - assert_eq!(r.name, "hello wire compat"); - eprintln!(" PASS: echo_fields_response.msgpack"); - pass += 1; + // ============ Original happy-path cases ============ + + check_request!("echo_bytes_request.msgpack", EchoBytes, |v: &EchoBytes| { + if v.data != vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42] { + Err("data".into()) + } else { + Ok(()) } - Ok(_) => { eprintln!(" FAIL: echo_fields_response.msgpack: wrong variant"); fail += 1; } - Err(e) => { eprintln!(" FAIL: echo_fields_response.msgpack: {e}"); fail += 1; } - } + }); + check_request!( + "echo_fields_request.msgpack", + EchoFields, + |v: &EchoFields| { + if v.a != 42 || v.b != 999999 || v.name != "hello wire compat" { + Err("fields".into()) + } else { + Ok(()) + } + } + ); + check_request!( + "echo_nested_request.msgpack", + EchoNested, + |v: &EchoNested| { + if v.inner.values != vec![vec![1u8, 2, 3], vec![4, 5]] || v.inner.flag != Some(true) { + Err("nested".into()) + } else { + Ok(()) + } + } + ); - match check_response(golden_dir, "echo_nested_response.msgpack") { - Ok(Response::EchoNestedResponse(r)) => { - assert_eq!(r.inner.values, vec![vec![1u8, 2, 3], vec![4, 5]]); - assert_eq!(r.inner.flag, Some(true)); - eprintln!(" PASS: echo_nested_response.msgpack"); - pass += 1; + check_response!( + "echo_bytes_response.msgpack", + EchoBytesResponse, + |v: &EchoBytesResponse| { + if v.data != vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42] { + Err("data".into()) + } else { + Ok(()) + } } - Ok(_) => { eprintln!(" FAIL: echo_nested_response.msgpack: wrong variant"); fail += 1; } - Err(e) => { eprintln!(" FAIL: echo_nested_response.msgpack: {e}"); fail += 1; } - } + ); + check_response!( + "echo_fields_response.msgpack", + EchoFieldsResponse, + |v: &EchoFieldsResponse| { + if v.a != 42 || v.b != 999999 || v.name != "hello wire compat" { + Err("fields".into()) + } else { + Ok(()) + } + } + ); + check_response!( + "echo_nested_response.msgpack", + EchoNestedResponse, + |v: &EchoNestedResponse| { + if v.inner.values != vec![vec![1u8, 2, 3], vec![4, 5]] || v.inner.flag != Some(true) { + Err("nested".into()) + } else { + Ok(()) + } + } + ); - eprintln!("\nResults: {pass}/{} passed, {fail} failed", pass + fail); - if fail > 0 { std::process::exit(1); } -} + // ============ Boundary cases ============ -fn check_request(dir: &str, name: &str) -> Result { - let path = format!("{dir}/{name}"); - let data = fs::read(&path).map_err(|e| format!("read {path}: {e}"))?; - // Request format: [Command] — deserialized as Vec - let commands: Vec = rmp_serde::from_slice(&data) - .map_err(|e| format!("deserialize: {e}"))?; - let command = commands.into_iter().next().ok_or("empty")?; - // Extract the specific variant - // We need to serialize the inner value back to msgpack and then deserialize as T - let inner_bytes = match command { - Command::EchoBytes(v) => rmp_serde::to_vec_named(&v).unwrap(), - Command::EchoFields(v) => rmp_serde::to_vec_named(&v).unwrap(), - Command::EchoNested(v) => rmp_serde::to_vec_named(&v).unwrap(), - Command::EchoShutdown(v) => rmp_serde::to_vec_named(&v).unwrap(), - }; - rmp_serde::from_slice(&inner_bytes).map_err(|e| format!("re-deserialize: {e}")) -} + check_request!("echo_bytes_empty.msgpack", EchoBytes, |v: &EchoBytes| { + if !v.data.is_empty() { + Err(format!("expected empty, got {} bytes", v.data.len())) + } else { + Ok(()) + } + }); + check_request!("echo_bytes_bin16.msgpack", EchoBytes, |v: &EchoBytes| { + if v.data.len() != 256 || v.data.iter().any(|&b| b != 0xAA) { + Err("expected 256 x 0xAA".into()) + } else { + Ok(()) + } + }); + check_request!("echo_fields_max.msgpack", EchoFields, |v: &EchoFields| { + if v.a != u32::MAX || v.b != u64::MAX || !v.name.is_empty() { + Err("expected u32::MAX/u64::MAX/empty".into()) + } else { + Ok(()) + } + }); + check_request!( + "echo_fields_uint_boundary.msgpack", + EchoFields, + |v: &EchoFields| { + if v.a != 128 || v.b != (u32::MAX as u64) + 1 || v.name != "x" { + Err("expected 128/u32max+1/\"x\"".into()) + } else { + Ok(()) + } + } + ); + check_request!( + "echo_fields_unicode.msgpack", + EchoFields, + |v: &EchoFields| { + if v.name != "héllo τέστ 🚀 mañana" { + Err(format!("unicode mismatch: {:?}", v.name)) + } else { + Ok(()) + } + } + ); + check_request!("echo_fields_str16.msgpack", EchoFields, |v: &EchoFields| { + if v.name.len() != 300 || v.name.chars().any(|c| c != 'a') { + Err("expected 300 x 'a'".into()) + } else { + Ok(()) + } + }); + check_request!( + "echo_nested_flag_none.msgpack", + EchoNested, + |v: &EchoNested| { + if !v.inner.values.is_empty() || v.inner.flag.is_some() { + Err("expected empty values + flag=None".into()) + } else { + Ok(()) + } + } + ); + check_request!( + "echo_nested_flag_false.msgpack", + EchoNested, + |v: &EchoNested| { + if v.inner.values != vec![Vec::::new()] || v.inner.flag != Some(false) { + Err("expected [[]] + flag=Some(false)".into()) + } else { + Ok(()) + } + } + ); -fn check_response(dir: &str, name: &str) -> Result { - let path = format!("{dir}/{name}"); - let data = fs::read(&path).map_err(|e| format!("read {path}: {e}"))?; - rmp_serde::from_slice(&data).map_err(|e| format!("deserialize: {e}")) + eprintln!("\nResults: {pass}/{} passed, {fail} failed", pass + fail); + if fail > 0 { + std::process::exit(1); + } } diff --git a/ipc-codegen/examples/ts/echo/golden_test.ts b/ipc-codegen/examples/ts/echo/golden_test.ts index c48da32470b3..7bade9e64c32 100644 --- a/ipc-codegen/examples/ts/echo/golden_test.ts +++ b/ipc-codegen/examples/ts/echo/golden_test.ts @@ -1,34 +1,34 @@ /** - * Golden file wire compatibility test (TypeScript). + * Golden file wire-format conformance test (TypeScript). * - * Verifies that TypeScript can correctly deserialize msgpack data produced by - * the Rust reference implementation (the golden files). This is the critical - * cross-language compatibility check — if TS can read Rust's output, and the - * round-trip tests show Rust can read TS's output, wire compat is proven. + * For each golden file, asserts: + * 1. We can decode the bytes into the expected typed value. + * 2. Re-encoding the same value produces byte-identical output. + * The combination pins down the wire format as a binding contract. * * Usage: npx tsx golden_test.ts - * Exits 0 if all pass, 1 on failure. */ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import { Decoder } from 'msgpackr'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { Decoder, Encoder } from "msgpackr"; const decoder = new Decoder({ useRecords: false }); -const goldenDir = path.join(import.meta.dirname!, '../../echo-schema', 'golden'); +// `variableMapSize: true` makes msgpackr emit fixmap (1-byte header) for small +// maps instead of always reaching for map16. Without it the encoder produces +// a semantically-equivalent but byte-different encoding, so round-tripping +// the goldens would fail even though the wire is otherwise correct. +const encoder = new Encoder({ useRecords: false, variableMapSize: true }); +const goldenDir = path.join( + import.meta.dirname!, + "../../echo-schema", + "golden", +); let pass = 0; let fail = 0; -function assertEqual(actual: any, expected: any, label: string) { - const a = JSON.stringify(actual); - const e = JSON.stringify(expected); - if (a !== e) { - throw new Error(`${label}: expected ${e}, got ${a}`); - } -} - -function bufEqual(a: Uint8Array, b: number[]) { +function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; @@ -36,81 +36,173 @@ function bufEqual(a: Uint8Array, b: number[]) { return true; } -function checkGoldenRequest(name: string, expectedCmdName: string, validate: (fields: any) => void) { - try { - const golden = fs.readFileSync(path.join(goldenDir, name)); - const decoded = decoder.unpack(golden) as any[]; - // Request format: [[commandName, {fields}]] - assertEqual(decoded.length, 1, `${name} array length`); - const [cmdName, fields] = decoded[0]; - assertEqual(cmdName, expectedCmdName, `${name} command name`); - validate(fields); - console.log(` PASS: ${name}`); - pass++; - } catch (e: any) { - console.log(` FAIL: ${name}: ${e.message}`); - fail++; +function deepEqual(a: any, b: any): boolean { + // For our test data: bigints, strings, numbers, plain arrays of u8 (which + // msgpackr decodes as Uint8Array), and nested objects. The JSON-stringify + // trick falls down on bigint and Uint8Array; do a structural walk. + if (a === b) return true; + if (typeof a === "bigint" || typeof b === "bigint") return a === b; + if (a instanceof Uint8Array && b instanceof Uint8Array) + return bytesEqual(a, b); + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + return a.every((x, i) => deepEqual(x, b[i])); + } + if (a && b && typeof a === "object" && typeof b === "object") { + const ka = Object.keys(a).sort(); + const kb = Object.keys(b).sort(); + if (ka.length !== kb.length || !ka.every((k, i) => k === kb[i])) + return false; + return ka.every((k) => deepEqual(a[k], b[k])); } + return false; } -function checkGoldenResponse(name: string, expectedRespName: string, validate: (fields: any) => void) { +/** Read golden, decode, check expectation, and (optionally) verify re-encode + * byte-equals golden. Strict roundtrip is the binding wire-format check, but + * msgpackr has one known divergence from rmp-serde: positive bigints are + * encoded as int64 (`d3`) instead of uint64 (`cf`). Both encodings are + * accepted by every msgpack decoder we care about, so the wire is still + * interoperable — we just can't pin the bytes here. */ +function check( + file: string, + expectedDecoded: any, + opts: { strictRoundtrip?: boolean } = {}, +) { + const strictRoundtrip = opts.strictRoundtrip ?? true; try { - const golden = fs.readFileSync(path.join(goldenDir, name)); - const decoded = decoder.unpack(golden) as any[]; - // Response format: [responseName, {fields}] - assertEqual(decoded.length, 2, `${name} array length`); - const [respName, fields] = decoded; - assertEqual(respName, expectedRespName, `${name} response name`); - validate(fields); - console.log(` PASS: ${name}`); + const golden = fs.readFileSync(path.join(goldenDir, file)); + const decoded = decoder.unpack(golden); + if (!deepEqual(decoded, expectedDecoded)) { + throw new Error( + `decoded mismatch:\n got: ${stringify(decoded)}\n exp: ${stringify(expectedDecoded)}`, + ); + } + if (strictRoundtrip) { + const reencoded = encoder.encode(decoded); + if (!bytesEqual(reencoded, golden)) { + throw new Error( + `roundtrip byte mismatch (decoded OK but re-encoded ${reencoded.length} bytes vs golden ${golden.length})`, + ); + } + } + console.log(` PASS: ${file}`); pass++; } catch (e: any) { - console.log(` FAIL: ${name}: ${e.message}`); + console.log(` FAIL: ${file}: ${e.message}`); fail++; } } -console.log('Golden file deserialization tests (TypeScript):\n'); +function stringify(v: any): string { + return JSON.stringify(v, (_k, x) => { + if (typeof x === "bigint") return `${x}n`; + if (x instanceof Uint8Array) return `[${Array.from(x).join(",")}]`; + return x; + }); +} + +console.log("Golden file wire-format conformance tests (TypeScript):\n"); -// Request golden files -checkGoldenRequest('echo_bytes_request.msgpack', 'EchoBytes', (f) => { - if (!bufEqual(f.data, [0xDE, 0xAD, 0xBE, 0xEF, 0x42])) { - throw new Error('data mismatch'); - } -}); - -checkGoldenRequest('echo_fields_request.msgpack', 'EchoFields', (f) => { - assertEqual(f.a, 42, 'a'); - assertEqual(f.b, 999999, 'b'); - assertEqual(f.name, 'hello wire compat', 'name'); -}); - -checkGoldenRequest('echo_nested_request.msgpack', 'EchoNested', (f) => { - assertEqual(f.inner.flag, true, 'flag'); - assertEqual(f.inner.values.length, 2, 'values length'); - if (!bufEqual(f.inner.values[0], [1, 2, 3])) throw new Error('values[0] mismatch'); - if (!bufEqual(f.inner.values[1], [4, 5])) throw new Error('values[1] mismatch'); -}); - -// Response golden files -checkGoldenResponse('echo_bytes_response.msgpack', 'EchoBytesResponse', (f) => { - if (!bufEqual(f.data, [0xDE, 0xAD, 0xBE, 0xEF, 0x42])) { - throw new Error('data mismatch'); - } -}); - -checkGoldenResponse('echo_fields_response.msgpack', 'EchoFieldsResponse', (f) => { - assertEqual(f.a, 42, 'a'); - assertEqual(f.b, 999999, 'b'); - assertEqual(f.name, 'hello wire compat', 'name'); -}); - -checkGoldenResponse('echo_nested_response.msgpack', 'EchoNestedResponse', (f) => { - assertEqual(f.inner.flag, true, 'flag'); - assertEqual(f.inner.values.length, 2, 'values length'); - if (!bufEqual(f.inner.values[0], [1, 2, 3])) throw new Error('values[0] mismatch'); - if (!bufEqual(f.inner.values[1], [4, 5])) throw new Error('values[1] mismatch'); -}); +// Request format: [[CommandName, {fields}]] +function req(cmdName: string, fields: any) { + return [[cmdName, fields]]; +} +// Response format: [ResponseName, {fields}] +function resp(respName: string, fields: any) { + return [respName, fields]; +} + +// ============ Original happy-path cases ============ +check( + "echo_bytes_request.msgpack", + req("EchoBytes", { data: new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0x42]) }), +); +check( + "echo_fields_request.msgpack", + req("EchoFields", { a: 42, b: 999999, name: "hello wire compat" }), +); +check( + "echo_nested_request.msgpack", + req("EchoNested", { + inner: { + values: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5])], + flag: true, + }, + }), +); + +check( + "echo_bytes_response.msgpack", + resp("EchoBytesResponse", { + data: new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0x42]), + }), +); +check( + "echo_fields_response.msgpack", + resp("EchoFieldsResponse", { a: 42, b: 999999, name: "hello wire compat" }), +); +check( + "echo_nested_response.msgpack", + resp("EchoNestedResponse", { + inner: { + values: [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5])], + flag: true, + }, + }), +); + +// ============ Boundary cases ============ + +// bin8 empty + bin16 (256 bytes) — bin8/bin16 framing boundary. +check("echo_bytes_empty.msgpack", req("EchoBytes", { data: new Uint8Array() })); +check( + "echo_bytes_bin16.msgpack", + req("EchoBytes", { data: new Uint8Array(256).fill(0xaa) }), +); + +// u32::MAX + u64::MAX + empty string. Largest uint encodings; fixstr-len-0. +// msgpackr decodes u64 fields as bigint when > Number.MAX_SAFE_INTEGER (2^53-1). +// Strict roundtrip is OK here because u64::MAX requires uint64 and msgpackr +// agrees with rmp-serde at the extreme. +check( + "echo_fields_max.msgpack", + req("EchoFields", { a: 4294967295, b: 18446744073709551615n, name: "" }), +); + +// u32 = 128 (fixint → uint8 boundary), u64 above u32::MAX (forces uint64). +// strictRoundtrip: false — see check()'s comment about the bigint/uint64 quirk. +check( + "echo_fields_uint_boundary.msgpack", + req("EchoFields", { a: 128, b: 4294967296n, name: "x" }), + { strictRoundtrip: false }, +); + +// Multi-byte UTF-8 in name. +check( + "echo_fields_unicode.msgpack", + req("EchoFields", { a: 0, b: 0, name: "héllo τέστ 🚀 mañana" }), +); + +// 300-char ASCII string. Crosses fixstr (≤31) and str8 (≤255) into str16. +check( + "echo_fields_str16.msgpack", + req("EchoFields", { a: 0, b: 0, name: "a".repeat(300) }), +); + +// Optional = absent (msgpackr decodes missing-with-nil to undefined or +// strips the key entirely depending on the encoder; rmp-serde emits a nil +// value for None inside a struct, so we expect flag: null here). +check( + "echo_nested_flag_none.msgpack", + req("EchoNested", { inner: { values: [], flag: null } }), +); + +// Optional = Some(false) with values=[empty inner]. +check( + "echo_nested_flag_false.msgpack", + req("EchoNested", { inner: { values: [new Uint8Array()], flag: false } }), +); console.log(`\nResults: ${pass}/${pass + fail} passed, ${fail} failed`); if (fail > 0) process.exit(1); diff --git a/ipc-codegen/src/typescript_codegen.ts b/ipc-codegen/src/typescript_codegen.ts index 22c27c7203a8..dd9670cdbdf3 100644 --- a/ipc-codegen/src/typescript_codegen.ts +++ b/ipc-codegen/src/typescript_codegen.ts @@ -390,7 +390,7 @@ import { BBApiException } from '../../bbapi_exception.js'; ${imports} async function msgpackCall(backend: IMsgpackBackendAsync, input: any[]) { - const inputBuffer = new Encoder({ useRecords: false }).pack(input); + const inputBuffer = new Encoder({ useRecords: false, variableMapSize: true }).pack(input); const encodedResult = await backend.call(inputBuffer); return new Decoder({ useRecords: false }).unpack(encodedResult); } @@ -423,7 +423,7 @@ import { BBApiException } from '../../bbapi_exception.js'; ${imports} function msgpackCall(backend: IMsgpackBackendSync, input: any[]) { - const inputBuffer = new Encoder({ useRecords: false }).pack(input); + const inputBuffer = new Encoder({ useRecords: false, variableMapSize: true }).pack(input); const encodedResult = backend.call(inputBuffer); return new Decoder({ useRecords: false }).unpack(encodedResult); } diff --git a/ipc-codegen/templates/ts/ipc_client.ts b/ipc-codegen/templates/ts/ipc_client.ts index 1fbdf761b3b1..4d303747a578 100644 --- a/ipc-codegen/templates/ts/ipc_client.ts +++ b/ipc-codegen/templates/ts/ipc_client.ts @@ -6,7 +6,7 @@ import * as net from 'node:net'; import { Decoder, Encoder } from 'msgpackr'; -const encoder = new Encoder({ useRecords: false }); +const encoder = new Encoder({ useRecords: false, variableMapSize: true }); const decoder = new Decoder({ useRecords: false }); export class IpcClient { diff --git a/ipc-codegen/templates/ts/ipc_server.ts b/ipc-codegen/templates/ts/ipc_server.ts index 3ef981ba2182..440b27480bd1 100644 --- a/ipc-codegen/templates/ts/ipc_server.ts +++ b/ipc-codegen/templates/ts/ipc_server.ts @@ -7,7 +7,7 @@ import * as net from 'node:net'; import * as fs from 'node:fs'; import { Decoder, Encoder } from 'msgpackr'; -const encoder = new Encoder({ useRecords: false }); +const encoder = new Encoder({ useRecords: false, variableMapSize: true }); const decoder = new Decoder({ useRecords: false }); export type DispatchFn = (commandName: string, payload: any) => Promise<[string, any]>; From c9889ea636072c55dd2ec77309d985c6021d31a1 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 19:58:20 +0000 Subject: [PATCH 06/11] chore(ipc-codegen): drop committed scaffolding from generated/; rename BarretenbergError -> IpcError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanups in one pass: 1) The "scaffolding" template files (backend.rs/error.rs/uds_backend.rs and the zig equivalents) were committed inside examples/*/echo/generated/ alongside the rest of the codegen output. That was a mistake — they are copied once from ipc-codegen/templates/ on a fresh build, so they belong in templates only and the generated/ dir should be 100% build- produced. Drop the committed copies and make the gitignore a blanket **/generated/ rule. 2) The templates' error type was named BarretenbergError, which leaked the codegen package's barretenberg origin into every generated consumer (Rust, Zig). Rename to IpcError so the templates are genuinely framework-agnostic; update the rust echo example consumer code (echo_server.rs) accordingly. After this, examples/*/echo/generated/ is reproducible from schema + templates with zero committed files, which is what one would expect of a directory named "generated". --- ipc-codegen/.gitignore | 13 +--- .../examples/rust/echo/src/echo_server.rs | 4 +- .../rust/echo/src/generated/backend.rs | 44 ----------- .../examples/rust/echo/src/generated/error.rs | 35 --------- .../rust/echo/src/generated/uds_backend.rs | 78 ------------------- ipc-codegen/examples/rust/wsdb/src/lib.rs | 2 +- .../examples/zig/echo/generated/backend.zig | 27 ------- .../zig/echo/generated/uds_backend.zig | 62 --------------- ipc-codegen/src/rust_codegen.ts | 10 +-- ipc-codegen/templates/rust/error.rs | 4 +- ipc-codegen/templates/rust/ffi_backend.rs | 6 +- ipc-codegen/templates/rust/uds_backend.rs | 14 ++-- 12 files changed, 23 insertions(+), 276 deletions(-) delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/backend.rs delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/error.rs delete mode 100644 ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs delete mode 100644 ipc-codegen/examples/zig/echo/generated/backend.zig delete mode 100644 ipc-codegen/examples/zig/echo/generated/uds_backend.zig diff --git a/ipc-codegen/.gitignore b/ipc-codegen/.gitignore index 51b7386bd35c..6626e04f7af9 100644 --- a/ipc-codegen/.gitignore +++ b/ipc-codegen/.gitignore @@ -6,13 +6,6 @@ examples/zig/*/.zig-cache/ examples/zig/*/zig-out/ examples/ts/*/node_modules/ -# Generated bindings under examples — regenerated each build by bootstrap.sh, -# never committed. Scaffolding files (backend.rs, error.rs, uds_backend.rs, -# *.zig backends) ARE committed because they're one-time-copied templates the -# example customizes by hand. -examples/cpp/*/generated/ -examples/ts/*/generated/ -examples/rust/*/src/generated/echo_*.rs -examples/rust/*/src/generated/ipc_server.rs -examples/zig/*/generated/echo_*.zig -examples/zig/*/generated/ipc_server.zig +# All files under any generated/ directory are produced by bootstrap.sh build +# (either codegen output or one-time-copied templates) and are never committed. +**/generated/ diff --git a/ipc-codegen/examples/rust/echo/src/echo_server.rs b/ipc-codegen/examples/rust/echo/src/echo_server.rs index 0d63b0d54820..df6c013abab3 100644 --- a/ipc-codegen/examples/rust/echo/src/echo_server.rs +++ b/ipc-codegen/examples/rust/echo/src/echo_server.rs @@ -3,7 +3,7 @@ use echo_wire_compat::generated::echo_server::Handler; use echo_wire_compat::generated::echo_types::*; -use echo_wire_compat::generated::error::{EchoError, Result}; +use echo_wire_compat::generated::error::{IpcError, Result}; use echo_wire_compat::generated::ipc_server; use std::cell::RefCell; @@ -67,5 +67,5 @@ fn main() -> Result<()> { }; rmp_serde::to_vec_named(&response).unwrap_or_default() - }).map_err(|e| EchoError::Io(e)) + }).map_err(|e| IpcError::Io(e)) } diff --git a/ipc-codegen/examples/rust/echo/src/generated/backend.rs b/ipc-codegen/examples/rust/echo/src/generated/backend.rs deleted file mode 100644 index 75eef08387c3..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/backend.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Backend trait for msgpack communication -//! -//! This module defines a simple, pluggable interface for Barretenberg backends. -//! Users can easily implement custom backends (FFI, WASM, IPC, etc.). - -use crate::error::Result; - -/// Simple interface for msgpack backend implementations. -/// -/// Implement this trait to create a custom backend for Barretenberg. -/// The backend handles msgpack-encoded command/response communication. -/// -/// # Example -/// -/// ```ignore -/// struct MyCustomBackend { -/// // your FFI handle, connection, etc. -/// } -/// -/// impl Backend for MyCustomBackend { -/// fn call(&mut self, input: &[u8]) -> Result> { -/// // Send input to your backend -/// // Return the response -/// } -/// -/// fn destroy(&mut self) -> Result<()> { -/// // Clean up resources -/// Ok(()) -/// } -/// } -/// ``` -pub trait Backend { - /// Execute a msgpack command and return the msgpack response. - /// - /// # Arguments - /// * `input` - Msgpack-encoded command - /// - /// # Returns - /// Msgpack-encoded response - fn call(&mut self, input: &[u8]) -> Result>; - - /// Clean up resources and shutdown the backend. - fn destroy(&mut self) -> Result<()>; -} diff --git a/ipc-codegen/examples/rust/echo/src/generated/error.rs b/ipc-codegen/examples/rust/echo/src/generated/error.rs deleted file mode 100644 index f70fa5e89612..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Error types for Barretenberg operations - -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum BarretenbergError { - #[error("Serialization error: {0}")] - Serialization(String), - - #[error("Deserialization error: {0}")] - Deserialization(String), - - #[error("Backend error: {0}")] - Backend(String), - - #[error("IPC error: {0}")] - Ipc(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Invalid response: {0}")] - InvalidResponse(String), - - #[error("Connection error: {0}")] - Connection(String), - - #[error("WASM error: {0}")] - Wasm(String), -} - -pub type Result = std::result::Result; - -// Alias for codegen compatibility — generated server/client code uses EchoError -pub type EchoError = BarretenbergError; diff --git a/ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs b/ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs deleted file mode 100644 index a524391cd687..000000000000 --- a/ipc-codegen/examples/rust/echo/src/generated/uds_backend.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! UDS (Unix Domain Socket) backend for Barretenberg -//! -//! Connects to a running BB server over a Unix domain socket, -//! using the standard 4-byte LE length-prefixed msgpack protocol. -//! Same wire format as C++/TS/Zig IPC clients. - -use super::backend::Backend; -use super::error::{BarretenbergError, Result}; -use std::io::{Read, Write}; -use std::os::unix::net::UnixStream; -use std::path::Path; - -/// UDS backend — connects to a BB server over Unix domain socket. -pub struct UdsBackend { - stream: UnixStream, -} - -impl UdsBackend { - /// Connect to a BB server at the given socket path. - /// - /// # Arguments - /// * `socket_path` - Path to the Unix domain socket (e.g. "/tmp/bb.sock") - pub fn connect(socket_path: impl AsRef) -> Result { - let stream = UnixStream::connect(socket_path.as_ref()).map_err(|e| { - BarretenbergError::Ipc(format!( - "Failed to connect to {}: {}", - socket_path.as_ref().display(), - e - )) - })?; - Ok(Self { stream }) - } - - fn send_with_prefix(&mut self, data: &[u8]) -> Result<()> { - let len = data.len() as u32; - self.stream - .write_all(&len.to_le_bytes()) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to write length: {}", e)))?; - self.stream - .write_all(data) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to write data: {}", e)))?; - self.stream - .flush() - .map_err(|e| BarretenbergError::Ipc(format!("Failed to flush: {}", e)))?; - Ok(()) - } - - fn receive_with_prefix(&mut self) -> Result> { - let mut len_buf = [0u8; 4]; - self.stream - .read_exact(&mut len_buf) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to read length: {}", e)))?; - let len = u32::from_le_bytes(len_buf) as usize; - let mut data = vec![0u8; len]; - self.stream - .read_exact(&mut data) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to read data: {}", e)))?; - Ok(data) - } -} - -impl Backend for UdsBackend { - fn call(&mut self, input: &[u8]) -> Result> { - self.send_with_prefix(input)?; - self.receive_with_prefix() - } - - fn destroy(&mut self) -> Result<()> { - let _ = self.stream.shutdown(std::net::Shutdown::Both); - Ok(()) - } -} - -impl Drop for UdsBackend { - fn drop(&mut self) { - let _ = self.destroy(); - } -} diff --git a/ipc-codegen/examples/rust/wsdb/src/lib.rs b/ipc-codegen/examples/rust/wsdb/src/lib.rs index d63ef4575379..763884b5f2fb 100644 --- a/ipc-codegen/examples/rust/wsdb/src/lib.rs +++ b/ipc-codegen/examples/rust/wsdb/src/lib.rs @@ -18,4 +18,4 @@ pub mod generated { } pub use generated::backend::Backend; -pub use generated::error::{BarretenbergError, Result}; +pub use generated::error::{IpcError, Result}; diff --git a/ipc-codegen/examples/zig/echo/generated/backend.zig b/ipc-codegen/examples/zig/echo/generated/backend.zig deleted file mode 100644 index 97a65bdbfa07..000000000000 --- a/ipc-codegen/examples/zig/echo/generated/backend.zig +++ /dev/null @@ -1,27 +0,0 @@ -/// Backend abstraction — comptime interface for transport. -/// -/// A valid backend type must provide: -/// fn call(self: *T, request: []const u8) ![]u8 -/// fn destroy(self: *T) void -/// -/// Implementations: -/// UdsBackend (uds_backend.zig) — Unix Domain Socket IPC -/// FfiBackend (ffi_backend.zig) — Direct C FFI linking -/// -/// Usage with the generated client: -/// const Client = @import("wsdb_client.zig").Client; -/// const UdsBackend = @import("uds_backend.zig").UdsBackend; -/// var backend = try UdsBackend.connect("/tmp/wsdb.sock"); -/// var client = Client(UdsBackend){ .backend = &backend }; - -/// Compile-time check that a type satisfies the backend interface. -pub fn assertBackend(comptime T: type) void { - // Must have: fn call(self: *T, request: []const u8) ![]u8 - if (!@hasDecl(T, "call")) { - @compileError("Backend type " ++ @typeName(T) ++ " missing 'call' method"); - } - // Must have: fn destroy(self: *T) void - if (!@hasDecl(T, "destroy")) { - @compileError("Backend type " ++ @typeName(T) ++ " missing 'destroy' method"); - } -} diff --git a/ipc-codegen/examples/zig/echo/generated/uds_backend.zig b/ipc-codegen/examples/zig/echo/generated/uds_backend.zig deleted file mode 100644 index af701e2d05c9..000000000000 --- a/ipc-codegen/examples/zig/echo/generated/uds_backend.zig +++ /dev/null @@ -1,62 +0,0 @@ -/// UDS (Unix Domain Socket) backend for IPC communication. -/// Handles: socket connect, length-prefixed framing, raw byte send/receive. -/// Satisfies the backend interface: call(request) -> response, destroy(). -const std = @import("std"); -const posix = std.posix; - -const alloc = std.heap.page_allocator; - -pub const UdsBackend = struct { - fd: posix.socket_t, - - /// Connect to a service at the given UDS path. - pub fn connect(socket_path: []const u8) !UdsBackend { - const address = try std.net.Address.initUnix(socket_path); - const fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM, 0); - try posix.connect(fd, &address.any, address.getOsSockLen()); - return .{ .fd = fd }; - } - - /// Send a raw msgpack request and receive a raw msgpack response. - /// Framing: 4-byte LE length prefix + payload. - pub fn call(self: *UdsBackend, request: []const u8) ![]u8 { - try sendFrame(self.fd, request); - return try recvFrame(self.fd); - } - - /// Close the connection. - pub fn destroy(self: *UdsBackend) void { - posix.close(self.fd); - } - - fn sendFrame(fd: posix.socket_t, data: []const u8) !void { - const len: u32 = @intCast(data.len); - const header = [4]u8{ - @intCast(len & 0xFF), - @intCast((len >> 8) & 0xFF), - @intCast((len >> 16) & 0xFF), - @intCast((len >> 24) & 0xFF), - }; - _ = try posix.write(fd, &header); - _ = try posix.write(fd, data); - } - - fn recvFrame(fd: posix.socket_t) ![]u8 { - var hdr: [4]u8 = undefined; - var got: usize = 0; - while (got < 4) { - const n = try posix.read(fd, hdr[got..]); - if (n == 0) return error.ConnectionClosed; - got += n; - } - const len: u32 = @as(u32, hdr[0]) | (@as(u32, hdr[1]) << 8) | (@as(u32, hdr[2]) << 16) | (@as(u32, hdr[3]) << 24); - const data = try alloc.alloc(u8, len); - got = 0; - while (got < len) { - const n = try posix.read(fd, data[got..]); - if (n == 0) return error.ConnectionClosed; - got += n; - } - return data; - } -}; diff --git a/ipc-codegen/src/rust_codegen.ts b/ipc-codegen/src/rust_codegen.ts index 6cbae852964d..e3ce5acf9d3a 100644 --- a/ipc-codegen/src/rust_codegen.ts +++ b/ipc-codegen/src/rust_codegen.ts @@ -18,7 +18,7 @@ export interface RustCodegenOptions { apiStructName?: string; /** Import path for Backend trait. Defaults to 'crate::backend::Backend' */ backendImport?: string; - /** Import path for error types. Defaults to 'crate::error::{BarretenbergError, Result}' */ + /** Import path for error types. Defaults to 'crate::error::{IpcError, Result}' */ errorImport?: string; /** Import path for generated types. Defaults to 'crate::types_gen::*' */ typesImport?: string; @@ -39,7 +39,7 @@ export class RustCodegen { prefix, apiStructName: options?.apiStructName ?? `${name}Api`, backendImport: options?.backendImport ?? 'super::backend::Backend', - errorImport: options?.errorImport ?? `super::error::{BarretenbergError, Result}`, + errorImport: options?.errorImport ?? `super::error::{IpcError, Result}`, typesImport: options?.typesImport ?? `super::${toSnakeCase(prefix || 'bb')}_types::*`, typesDocComment: options?.typesDocComment ?? `Generated types for ${name} IPC protocol`, apiDocComment: options?.apiDocComment ?? `${name} IPC client API`, @@ -509,8 +509,8 @@ ${this.generateResponseEnum(schema)} return name; }).join(', '); - // Extract error type name from the error import (e.g., 'BarretenbergError' from 'crate::error::{BarretenbergError, Result}') - const errorType = this.opts.errorImport.match(/\{(\w+),/)?.[1] ?? 'BarretenbergError'; + // Extract error type name from the error import (e.g., 'IpcError' from 'crate::error::{IpcError, Result}') + const errorType = this.opts.errorImport.match(/\{(\w+),/)?.[1] ?? 'IpcError'; return ` /// Execute ${command.name} pub fn ${methodName}(&mut self, ${params}) -> Result<${respRustName}> { @@ -550,7 +550,7 @@ ${this.generateResponseEnum(schema)} } ` : ''; - const errorType = errorImport.match(/\{(\w+),/)?.[1] ?? 'BarretenbergError'; + const errorType = errorImport.match(/\{(\w+),/)?.[1] ?? 'IpcError'; return `//! AUTOGENERATED - DO NOT EDIT //! ${apiDocComment} diff --git a/ipc-codegen/templates/rust/error.rs b/ipc-codegen/templates/rust/error.rs index 726ac4a9ad2f..37691d6210be 100644 --- a/ipc-codegen/templates/rust/error.rs +++ b/ipc-codegen/templates/rust/error.rs @@ -3,7 +3,7 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum BarretenbergError { +pub enum IpcError { #[error("Serialization error: {0}")] Serialization(String), @@ -29,4 +29,4 @@ pub enum BarretenbergError { Wasm(String), } -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/ipc-codegen/templates/rust/ffi_backend.rs b/ipc-codegen/templates/rust/ffi_backend.rs index 490546001feb..a1c19269c6d8 100644 --- a/ipc-codegen/templates/rust/ffi_backend.rs +++ b/ipc-codegen/templates/rust/ffi_backend.rs @@ -24,7 +24,7 @@ //! ``` use super::backend::Backend; -use super::error::{BarretenbergError, Result}; +use super::error::{IpcError, Result}; use std::ptr; // C API exported by Barretenberg @@ -90,7 +90,7 @@ impl Backend for FfiBackend { } if output_ptr.is_null() { - return Err(BarretenbergError::Backend( + return Err(IpcError::Backend( "bbapi returned null pointer".to_string(), )); } @@ -100,7 +100,7 @@ impl Backend for FfiBackend { unsafe { libc::free(output_ptr as *mut libc::c_void); } - return Err(BarretenbergError::Backend( + return Err(IpcError::Backend( "bbapi returned empty response".to_string(), )); } diff --git a/ipc-codegen/templates/rust/uds_backend.rs b/ipc-codegen/templates/rust/uds_backend.rs index a524391cd687..5d25363ee697 100644 --- a/ipc-codegen/templates/rust/uds_backend.rs +++ b/ipc-codegen/templates/rust/uds_backend.rs @@ -5,7 +5,7 @@ //! Same wire format as C++/TS/Zig IPC clients. use super::backend::Backend; -use super::error::{BarretenbergError, Result}; +use super::error::{IpcError, Result}; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; use std::path::Path; @@ -22,7 +22,7 @@ impl UdsBackend { /// * `socket_path` - Path to the Unix domain socket (e.g. "/tmp/bb.sock") pub fn connect(socket_path: impl AsRef) -> Result { let stream = UnixStream::connect(socket_path.as_ref()).map_err(|e| { - BarretenbergError::Ipc(format!( + IpcError::Ipc(format!( "Failed to connect to {}: {}", socket_path.as_ref().display(), e @@ -35,13 +35,13 @@ impl UdsBackend { let len = data.len() as u32; self.stream .write_all(&len.to_le_bytes()) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to write length: {}", e)))?; + .map_err(|e| IpcError::Ipc(format!("Failed to write length: {}", e)))?; self.stream .write_all(data) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to write data: {}", e)))?; + .map_err(|e| IpcError::Ipc(format!("Failed to write data: {}", e)))?; self.stream .flush() - .map_err(|e| BarretenbergError::Ipc(format!("Failed to flush: {}", e)))?; + .map_err(|e| IpcError::Ipc(format!("Failed to flush: {}", e)))?; Ok(()) } @@ -49,12 +49,12 @@ impl UdsBackend { let mut len_buf = [0u8; 4]; self.stream .read_exact(&mut len_buf) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to read length: {}", e)))?; + .map_err(|e| IpcError::Ipc(format!("Failed to read length: {}", e)))?; let len = u32::from_le_bytes(len_buf) as usize; let mut data = vec![0u8; len]; self.stream .read_exact(&mut data) - .map_err(|e| BarretenbergError::Ipc(format!("Failed to read data: {}", e)))?; + .map_err(|e| IpcError::Ipc(format!("Failed to read data: {}", e)))?; Ok(data) } } From f2c5425dcad1813b548d301d7ae1fd8c490d170c Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 15 May 2026 20:04:19 +0000 Subject: [PATCH 07/11] docs(ipc-codegen): hoist README/SCHEMA_SPEC to package root; rewrite README Move ipc-codegen/src/README.md and ipc-codegen/src/SCHEMA_SPEC.md up to ipc-codegen/ root where they belong. Rewrite the README from scratch: - Quick start (bootstrap.sh build / test). - Package layout (schemas/, src/, templates/, examples/, scripts/). - Full src/generate.ts CLI reference: every flag, what it does, which language it applies to. - Worked examples for each of the four targets (TS bb client+server with curve constants; C++ wsdb under a barretenberg include path; Rust wsdb with UDS+FFI and skeleton scaffolding; Zig avm). - "Adding a new service" workflow. - Schema-of-truth + scripts/update_schemas.sh + validate_schemas.sh explanation. - Pointer to the frozen-goldens wire-format contract. Fix the SCHEMA_SPEC.md path references that still pointed at the old barretenberg/ts/src/cbind/ location. --- ipc-codegen/README.md | 173 +++++++++++++++++++++++++++ ipc-codegen/{src => }/SCHEMA_SPEC.md | 4 +- ipc-codegen/src/README.md | 86 ------------- 3 files changed, 175 insertions(+), 88 deletions(-) create mode 100644 ipc-codegen/README.md rename ipc-codegen/{src => }/SCHEMA_SPEC.md (98%) delete mode 100644 ipc-codegen/src/README.md diff --git a/ipc-codegen/README.md b/ipc-codegen/README.md new file mode 100644 index 000000000000..a0a72de223b3 --- /dev/null +++ b/ipc-codegen/README.md @@ -0,0 +1,173 @@ +# ipc-codegen + +Schema-driven IPC code generator for **C++**, **TypeScript**, **Rust**, and **Zig**. + +Given a JSON schema describing a service's commands and responses, emits matching +type definitions plus a client and/or server in the target language. All wire I/O +is msgpack-over-UDS; languages talk to each other byte-compatibly. + +## Quick start + +```sh +cd ipc-codegen +./bootstrap.sh build # generate echo example bindings, compile all 4 languages +./bootstrap.sh test # run the 18-test cross-language wire-compat matrix +``` + +## Layout + +``` +ipc-codegen/ + bootstrap.sh # build / test / update_goldens / hash + schemas/ # committed JSON schemas (one per service) + avm_schema.json + bb_schema.json + cdb_schema.json + wsdb_schema.json + bb_curve_constants.json + src/ # generator (TypeScript, runs under Node 22+) + generate.ts # CLI entry point + schema_visitor.ts # JSON schema -> CompiledSchema IR + cpp_codegen.ts # IR -> C++ output + typescript_codegen.ts # IR -> TypeScript output + rust_codegen.ts # IR -> Rust output + zig_codegen.ts # IR -> Zig output + naming.ts # snake_case / PascalCase helpers + templates/ # static templates copied alongside generated code + cpp/{ipc_client,ipc_server,msgpack_struct_map_impl}.hpp + ts/{ipc_client,ipc_server}.ts + rust/{backend,error,ipc_client,ipc_server,uds_backend,ffi_backend}.rs + zig/{backend,uds_backend,ffi_backend,ipc_client,ipc_server,ffi_client}.zig + scripts/ + update_schemas.sh # refresh schemas/ from current C++ binaries + validate_schemas.sh # CI guard: schemas/ matches the binaries + examples/ # 4-language echo service (test harness, see below) + SCHEMA_SPEC.md # wire protocol and schema-format reference +``` + +## CLI: `src/generate.ts` + +Invoked once per (schema, language) pair. Run directly with `node --experimental-strip-types`, or via `bootstrap.sh`. + +``` +node --experimental-strip-types --experimental-transform-types --no-warnings \ + src/generate.ts --schema --lang --out [flags] +``` + +### Required flags + +| Flag | Purpose | +|---|---| +| `--schema ` | Path to the JSON schema. | +| `--lang ` | Target language. | +| `--out ` | Output directory. Files are (re)written every run; static templates are copied alongside (and re-copied only if missing for one-time scaffolding files). | + +### Role flags + +| Flag | Purpose | +|---|---| +| `--server` | Emit server dispatch (matches request name to handler, deserializes, calls handler, serializes response). | +| `--client` | Emit a typed client class/struct with one method per command. | +| `--uds` | Include the Unix-domain-socket backend (Rust/Zig). | +| `--ffi` | Include the FFI backend (Rust/Zig). | + +### Naming flags + +| Flag | Purpose | +|---|---| +| `--prefix ` | Type prefix applied to generated type names (`CircuitProve`, etc.). Auto-detected from the schema if omitted. | +| `--strip-method-prefix` | TS only. Drops the prefix from client *method* names: `bbCircuitProve()` → `circuitProve()`. Types keep the prefix. | + +### C++-specific flags + +| Flag | Purpose | +|---|---| +| `--cpp-namespace ` | C++ namespace, e.g. `bb::wsdb`. Default: lowercased prefix. | +| `--cpp-wire-namespace ` | Inner namespace for wire types, default `wire`. | +| `--cpp-include-dir ` | Include-path prefix for cross-includes between generated files, e.g. `barretenberg/wsdb/generated`. Leave unset when generated files are in the same directory as their consumer. | + +### Other + +| Flag | Purpose | +|---|---| +| `--curve-constants` | TS only. Also emit `curve_constants.ts` with bn254/grumpkin/secp moduli & generators (currently only used by bb). | +| `--skeleton ` | One-shot scaffolding: writes a `_handlers.{ts,rs,zig,cpp}` stub, `main`, and a build file into `` if they don't already exist. Skipped on subsequent runs. | + +## Worked examples + +Each invocation produces both the per-command type definitions and the role(s) you request. + +### TypeScript client + server, with curve constants (bb) + +```sh +src/generate.ts \ + --schema schemas/bb_schema.json \ + --lang ts \ + --out ../barretenberg/ts/src/cbind/generated \ + --server --client \ + --prefix Bb --strip-method-prefix --curve-constants +``` + +Produces `api_types.ts`, `async.ts`, `sync.ts`, `server.ts`, `ipc_client.ts` (template), `ipc_server.ts` (template), `curve_constants.ts`. + +### C++ server (no client), under a barretenberg sub-include path (wsdb) + +```sh +src/generate.ts \ + --schema schemas/wsdb_schema.json \ + --lang cpp \ + --out ../barretenberg/cpp/src/barretenberg/wsdb/generated \ + --server --client \ + --cpp-namespace bb::wsdb --prefix Wsdb \ + --cpp-include-dir barretenberg/wsdb/generated +``` + +Produces `wsdb_types.hpp`, `wsdb_ipc_client.{hpp,cpp}`, `wsdb_ipc_server.hpp`, plus the `ipc_client.hpp` / `ipc_server.hpp` / `msgpack_struct_map_impl.hpp` templates. Cross-includes use the supplied `--cpp-include-dir` prefix (`#include "barretenberg/wsdb/generated/wsdb_types.hpp"`). + +### Rust UDS + FFI client (wsdb) + +```sh +src/generate.ts \ + --schema schemas/wsdb_schema.json \ + --lang rust \ + --out src/generated \ + --server --client --uds --ffi \ + --prefix Wsdb \ + --skeleton src +``` + +Produces `wsdb_types.rs`, `wsdb_client.rs`, `wsdb_server.rs`, `ipc_server.rs` (template), plus the backend templates (`backend.rs`, `error.rs`, `uds_backend.rs`, `ffi_backend.rs`). The skeleton flag also writes a one-time `wsdb_handlers.rs`, `main.rs`, and `Cargo.toml` into `src/` so a new service crate is buildable on first run. + +### Zig client + server (avm) + +```sh +src/generate.ts \ + --schema schemas/avm_schema.json \ + --lang zig \ + --out src/generated \ + --server --client --uds --ffi \ + --prefix Avm +``` + +Produces `avm_types.zig`, `avm_client.zig`, `avm_server.zig`, plus backend templates. + +## Adding a new service + +1. **Define the C++ command structs** in your service's `.hpp`, each with `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS(...)`. Group them into a single `Command` and `Response` `NamedUnion`. +2. **Build the service binary** and run `./scripts/update_schemas.sh` — this calls ` msgpack schema` and writes the JSON to `schemas/_schema.json`. Commit the schema. +3. **Wire your consumer's `bootstrap.sh build` to invoke `src/generate.ts`** with the flags above. Generated files go under a `generated/` directory which is gitignored by convention. +4. **Run `./bootstrap.sh test`** in `ipc-codegen/` to confirm the codegen and cross-language wire compat tests still pass. + +## Schemas are the source of truth + +The JSON files under `schemas/` are checked in. They're what `generate.ts` consumes. They're produced from the C++ binaries via `./scripts/update_schemas.sh` whenever the underlying commands change. `validate_schemas.sh` is the CI guard that diffs the committed JSON against the current binaries — a stale schema is a CI failure, not a runtime surprise. + +Each generated file embeds a `SCHEMA_HASH` so callers can detect at connection time that their bindings predate the server. + +## Wire-format contract + +`examples/echo-schema/golden/*.msgpack` is a frozen set of byte-level fixtures covering every relevant msgpack encoding boundary (variable-width ints, fixstr/str8/str16, bin8/bin16, optional `Some`/`None`, empty containers, multi-byte UTF-8). The per-language golden tests (`examples/{rust,ts}/echo/...`) both decode the fixtures and re-encode round-trip — pinning down canonical msgpack output across implementations. + +If you intentionally change the wire format, run `./bootstrap.sh update_goldens` and review the diff. Any byte-level change is a breaking change for external implementations of the schema. + +See `SCHEMA_SPEC.md` for the wire protocol details. diff --git a/ipc-codegen/src/SCHEMA_SPEC.md b/ipc-codegen/SCHEMA_SPEC.md similarity index 98% rename from ipc-codegen/src/SCHEMA_SPEC.md rename to ipc-codegen/SCHEMA_SPEC.md index d8969daa53a2..d47b9725602b 100644 --- a/ipc-codegen/src/SCHEMA_SPEC.md +++ b/ipc-codegen/SCHEMA_SPEC.md @@ -238,5 +238,5 @@ To add a new command to a service: - Schema export: `barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp` - Schema naming: `barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_name.hpp` - NamedUnion: `barretenberg/cpp/src/barretenberg/common/named_union.hpp` -- Schema visitor (IR compiler): `barretenberg/ts/src/cbind/schema_visitor.ts` -- Service codegen orchestrator: `barretenberg/ts/src/cbind/service_codegen.ts` +- Schema visitor (IR compiler): `ipc-codegen/src/schema_visitor.ts` +- CLI entry point: `ipc-codegen/src/generate.ts` diff --git a/ipc-codegen/src/README.md b/ipc-codegen/src/README.md deleted file mode 100644 index 892a9ea1d6e8..000000000000 --- a/ipc-codegen/src/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Multi-Language IPC Code Generation - -Generates type-safe client and server bindings from Aztec IPC service schemas -in four languages: **C++**, **TypeScript**, **Rust**, and **Zig**. - -## Architecture - -``` -C++ Service Binaries (aztec-wsdb, aztec-cdb, aztec-avm, bb) - │ - │ `./binary msgpack schema` → JSON to stdout - ▼ -Raw Schema JSON - │ - │ SchemaVisitor (schema_visitor.ts) - ▼ -CompiledSchema IR (language-neutral) - │ - ├──► TypeScriptCodegen → types, async client, server dispatch - ├──► CppCodegen → IPC client class, server handler - ├──► RustCodegen → types, API struct, Handler trait - └──► ZigCodegen → types, client struct, handler vtable -``` - -## Files - -| File | Purpose | -|------|---------| -| `generate.ts` | Unified entry point — runs all services and languages | -| `service_codegen.ts` | Service configs, language target wiring, `generateForService()` | -| `schema_visitor.ts` | Compiles raw JSON schema to `CompiledSchema` IR | -| `typescript_codegen.ts` | TypeScript types, async/sync client, server dispatch | -| `cpp_codegen.ts` | C++ IPC client class, server handler function | -| `rust_codegen.ts` | Rust types/enums, API struct, Handler trait | -| `zig_codegen.ts` | Zig structs, client, handler vtable | -| `naming.ts` | Shared naming utilities (snake_case, PascalCase) | -| `SCHEMA_SPEC.md` | Wire protocol and schema format specification | - -## Services - -| Service | Binary | Languages | Client | Server | -|---------|--------|-----------|--------|--------| -| bb | `bb` | TS, Rust | yes | no | -| wsdb | `aztec-wsdb` | TS, C++, Rust, Zig | yes | yes | -| cdb | `aztec-cdb` | TS, C++, Rust, Zig | yes | yes | -| avm | `aztec-avm` | TS, Rust, Zig | yes | no | - -## Usage - -```bash -# Generate all services, all languages -yarn generate - -# Generate a single service -yarn generate:wsdb - -# Generate specific services via unified entry point -npx tsx src/cbind/generate.ts wsdb cdb -``` - -## Adding a New Command - -1. Define the command struct in C++ with `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS` -2. Add a nested `Response` struct -3. Add both to the service's `Command` and `CommandResponse` NamedUnion types -4. Run `yarn generate` -5. All language bindings regenerate automatically - -## Adding a New Language - -1. Create `_codegen.ts` implementing `generateTypes()`, `generateClient()`, `generateServer()` -2. Add a target helper function in `service_codegen.ts` -3. Wire it into the relevant service configs -4. See `SCHEMA_SPEC.md` for the wire protocol contract - -## Output Locations - -- **TypeScript**: `src/aztec-{wsdb,cdb,avm}/generated/` -- **C++**: `cpp/src/barretenberg/{wsdb,cdb}/*_generated.{hpp,cpp}` -- **Rust**: `rust/aztec-ipc/src/{wsdb,cdb,avm}/` -- **Zig**: `zig/aztec-ipc/src/{wsdb,cdb,avm}/` - -## Schema Versioning - -Each generated file includes a `SCHEMA_HASH` constant (SHA-256 of the raw schema JSON). -Clients can check this at connection time to detect incompatible schema changes. From c308cf7c537d6e0cd237f4b62bc4bff9f6c88eb6 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Mon, 18 May 2026 16:42:54 +0000 Subject: [PATCH 08/11] fix(ipc-codegen): drop wsdb skeleton examples; pin zig-msgpack to a tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues in one cleanup: 1) examples/{rust,zig}/wsdb/ were skeleton scaffolds where every handler returned "not implemented". They weren't part of the cross-language test matrix and they duplicated what the README's "Worked examples" section already documents. The echo example fully exercises codegen + roundtrip across all four languages and is what reviewers should look at. Delete the scaffolds. 2) examples/zig/echo/build.zig.zon pinned the zig-msgpack tarball URL to heads/main, which drifts as the upstream branch moves. CI fetched a newer snapshot than the committed hash and zig-fetch refused with a hash-mismatch error, breaking the whole `make ipc-codegen` target (and every dependent test job). Switch the URL to `tags/0.0.17` (the latest tagged release, which is also what CI was implicitly fetching from main) and use that release's canonical hash. The tagged URL produces a stable hash; this won't drift again. Verify: cd ipc-codegen && ./bootstrap.sh test → 18/18 pass. --- ipc-codegen/examples/rust/wsdb/Cargo.toml | 11 --- ipc-codegen/examples/rust/wsdb/README.md | 45 ---------- ipc-codegen/examples/rust/wsdb/generate.sh | 19 ----- ipc-codegen/examples/rust/wsdb/src/lib.rs | 21 ----- ipc-codegen/examples/zig/echo/build.zig.zon | 9 +- ipc-codegen/examples/zig/wsdb/.gitignore | 3 - ipc-codegen/examples/zig/wsdb/README.md | 93 --------------------- ipc-codegen/examples/zig/wsdb/build.zig | 31 ------- ipc-codegen/examples/zig/wsdb/build.zig.zon | 17 ---- ipc-codegen/examples/zig/wsdb/generate.sh | 16 ---- ipc-codegen/examples/zig/wsdb/src/main.zig | 24 ------ 11 files changed, 7 insertions(+), 282 deletions(-) delete mode 100644 ipc-codegen/examples/rust/wsdb/Cargo.toml delete mode 100644 ipc-codegen/examples/rust/wsdb/README.md delete mode 100755 ipc-codegen/examples/rust/wsdb/generate.sh delete mode 100644 ipc-codegen/examples/rust/wsdb/src/lib.rs delete mode 100644 ipc-codegen/examples/zig/wsdb/.gitignore delete mode 100644 ipc-codegen/examples/zig/wsdb/README.md delete mode 100644 ipc-codegen/examples/zig/wsdb/build.zig delete mode 100644 ipc-codegen/examples/zig/wsdb/build.zig.zon delete mode 100755 ipc-codegen/examples/zig/wsdb/generate.sh delete mode 100644 ipc-codegen/examples/zig/wsdb/src/main.zig diff --git a/ipc-codegen/examples/rust/wsdb/Cargo.toml b/ipc-codegen/examples/rust/wsdb/Cargo.toml deleted file mode 100644 index 89a915403bba..000000000000 --- a/ipc-codegen/examples/rust/wsdb/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "wsdb-service-example" -version = "0.1.0" -edition = "2021" -description = "Example WSDB service implementation in Rust using codegen" - -[dependencies] -rmp = "0.8" -rmp-serde = "1" -serde = { version = "1", features = ["derive"] } -thiserror = "2" diff --git a/ipc-codegen/examples/rust/wsdb/README.md b/ipc-codegen/examples/rust/wsdb/README.md deleted file mode 100644 index fdb68399e07f..000000000000 --- a/ipc-codegen/examples/rust/wsdb/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Rust WSDB Service Example - -Demonstrates building a WSDB service in Rust using the codegen tool. - -## Setup - -```bash -# Generate all bindings (types, client, server, backends, handler stubs) -./generate.sh - -# Build -cargo build -``` - -## Structure - -``` -src/ - generated/ # Always-regenerated by codegen (don't hand-edit) - wsdb_types.rs # Wire types with serialization - wsdb_client.rs # Typed client wrapper - wsdb_server.rs # Server dispatch - backend.rs # Backend trait (template, editable) - error.rs # Error types (template, editable) - uds_backend.rs # UDS transport (template, editable) - ffi_backend.rs # FFI transport (template, editable) - wsdb_handlers.rs # Handler stubs (skeleton, implement these!) - main.rs # Entry point (skeleton, edit as needed) - lib.rs # Module declarations -``` - -## Implementing handlers - -Edit `src/wsdb_handlers.rs` — each `handle_*` function receives a wire command -and returns a wire response. Add your business logic (database, state management, etc.) -to the `WsdbContext` struct. - -## Regenerating after schema changes - -```bash -./generate.sh -``` - -This regenerates types/client/server in `generated/` but does NOT overwrite -your handler implementations or backend templates. diff --git a/ipc-codegen/examples/rust/wsdb/generate.sh b/ipc-codegen/examples/rust/wsdb/generate.sh deleted file mode 100755 index a08975550bcb..000000000000 --- a/ipc-codegen/examples/rust/wsdb/generate.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Generate WSDB Rust bindings from the committed schema. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CODEGEN="$SCRIPT_DIR/../../.." -SCHEMA="$CODEGEN/schemas/wsdb_schema.json" -NODE_FLAGS="--experimental-strip-types --experimental-transform-types --no-warnings" - -echo "Generating Rust WSDB bindings..." -node $NODE_FLAGS "$CODEGEN/src/generate.ts" \ - --schema "$SCHEMA" \ - --lang rust \ - --out "$SCRIPT_DIR/src/generated" \ - --server --client --uds --ffi \ - --prefix Wsdb \ - --skeleton "$SCRIPT_DIR/src" - -echo "Done. Generated files in src/generated/, skeleton in src/" diff --git a/ipc-codegen/examples/rust/wsdb/src/lib.rs b/ipc-codegen/examples/rust/wsdb/src/lib.rs deleted file mode 100644 index 763884b5f2fb..000000000000 --- a/ipc-codegen/examples/rust/wsdb/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -// WSDB service example — generated + hand-written code. -// -// To regenerate: ./generate.sh -// Then implement the handlers in wsdb_handlers.rs - -pub mod generated { - pub mod wsdb_types; - pub mod wsdb_client; - pub mod wsdb_server; - pub mod backend; - pub mod error; - - #[cfg(feature = "uds")] - pub mod uds_backend; - - #[cfg(feature = "ffi")] - pub mod ffi_backend; -} - -pub use generated::backend::Backend; -pub use generated::error::{IpcError, Result}; diff --git a/ipc-codegen/examples/zig/echo/build.zig.zon b/ipc-codegen/examples/zig/echo/build.zig.zon index 0a16193fa043..eca36acc8919 100644 --- a/ipc-codegen/examples/zig/echo/build.zig.zon +++ b/ipc-codegen/examples/zig/echo/build.zig.zon @@ -4,9 +4,14 @@ .fingerprint = 0x15539c02bc3573e2, .minimum_zig_version = "0.14.0", .dependencies = .{ + // Pinned to the 0.0.14 tag (not heads/main) so the manifest hash is + // stable. heads/main drifts whenever zig-msgpack's main branch moves, + // which breaks `zig fetch` with a hash-mismatch error on CI runners + // that pull a different snapshot than the one this hash was captured + // against. .zig_msgpack = .{ - .url = "https://github.com/zigcc/zig-msgpack/archive/refs/heads/main.tar.gz", - .hash = "zig_msgpack-0.0.14-evvueIkqBQA7F_-8hpmPYvAjmg9MOWJLm6p8sb0HnGem", + .url = "https://github.com/zigcc/zig-msgpack/archive/refs/tags/0.0.17.tar.gz", + .hash = "zig_msgpack-0.0.14-evvueL5SBQACmim6j6klQ9wWIIG_UxGlPvVYdiNy0KT8", }, }, .paths = .{ diff --git a/ipc-codegen/examples/zig/wsdb/.gitignore b/ipc-codegen/examples/zig/wsdb/.gitignore deleted file mode 100644 index 647d2f5a5919..000000000000 --- a/ipc-codegen/examples/zig/wsdb/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -src/generated/ -.zig-cache/ -zig-out/ diff --git a/ipc-codegen/examples/zig/wsdb/README.md b/ipc-codegen/examples/zig/wsdb/README.md deleted file mode 100644 index af39477f79ed..000000000000 --- a/ipc-codegen/examples/zig/wsdb/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Zig WSDB Server Example - -A skeleton WSDB (World State Database) implementation in Zig, using -generated types and serialization from the Aztec IPC codegen tool. - -All command handlers return "not implemented" errors. Replace them with -your world-state logic to build a working WSDB. - -## Prerequisites - -- [Zig](https://ziglang.org/) 0.15+ -- [Node.js](https://nodejs.org/) 22+ (for the codegen step) - -## Build & Run - -```bash -# 1. Generate types from the committed WSDB schema -./generate.sh - -# 2. Build -zig build - -# 3. Run -./zig-out/bin/zig-wsdb --socket /tmp/wsdb.sock -``` - -## How It Works - -``` -generate.sh calls: - codegen/src/generate.ts --schema wsdb_schema.json --lang zig --prefix Wsdb --out src/generated/ - - produces: src/generated/types.zig (WSDB command/response structs) - src/generated/server.zig (server dispatch vtable) - -src/main.zig - UDS server → recv frame → decode msgpack → dispatch by command name - → deserialize fields into generated struct via fromPayload() - → call handler (your code) - → serialize response via toPayload() - → encode as NamedUnion [responseName, {fields}] - → send frame -``` - -## Implementing a Command - -Edit `handleCommand()` in `src/main.zig`: - -```zig -fn handleCommand(comptime CmdType: type, comptime RespType: type, cmd: CmdType) !RespType { - if (CmdType == types.WsdbGetTreeInfo) { - return types.WsdbGetTreeInfoResponse{ - .tree_id = cmd.tree_id, - .root = std.mem.zeroes(types.Fr), // 32-byte field element - .size = 0, - .depth = 32, - }; - } - return error.NotImplemented; -} -``` - -## Connecting a Client - -Any WSDB client that speaks the IPC protocol can connect — C++, TypeScript, Rust, or Zig. -The wire format is: 4-byte LE length prefix + msgpack payload. - -```bash -# Example: connect the existing C++ WSDB client -aztec-wsdb-client --socket /tmp/wsdb.sock -``` - -## Project Structure - -``` -├── generate.sh # Invokes codegen CLI to produce types -├── build.zig # Zig build file -├── build.zig.zon # Zig package manifest (depends on zig-msgpack) -├── src/ -│ ├── main.zig # Server: UDS, framing, dispatch, handlers -│ └── generated/ # AUTOGENERATED (gitignored) -│ ├── types.zig # All WSDB command/response structs -│ └── server.zig # Server dispatch vtable -└── README.md -``` - -## Schema Updates - -If the WSDB schema changes: -1. Someone updates `ipc-codegen/schemas/wsdb_schema.json` -2. Run `./generate.sh` to regenerate types -3. Fix any compilation errors from changed/removed commands -4. `zig build` diff --git a/ipc-codegen/examples/zig/wsdb/build.zig b/ipc-codegen/examples/zig/wsdb/build.zig deleted file mode 100644 index e287aaa276e1..000000000000 --- a/ipc-codegen/examples/zig/wsdb/build.zig +++ /dev/null @@ -1,31 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const msgpack_dep = b.dependency("zig_msgpack", .{ - .target = target, - .optimize = optimize, - }); - const msgpack_mod = msgpack_dep.module("msgpack"); - - const exe = b.addExecutable(.{ - .name = "zig-wsdb", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }), - }); - exe.root_module.addImport("msgpack", msgpack_mod); - b.installArtifact(exe); - - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); - } - const run_step = b.step("run", "Run the WSDB server"); - run_step.dependOn(&run_cmd.step); -} diff --git a/ipc-codegen/examples/zig/wsdb/build.zig.zon b/ipc-codegen/examples/zig/wsdb/build.zig.zon deleted file mode 100644 index 1e4c2b8243ab..000000000000 --- a/ipc-codegen/examples/zig/wsdb/build.zig.zon +++ /dev/null @@ -1,17 +0,0 @@ -.{ - .name = .zig_wsdb, - .version = "0.1.0", - .fingerprint = 0xeb5cdc04a6a8770f, - .minimum_zig_version = "0.14.0", - .dependencies = .{ - .zig_msgpack = .{ - .url = "https://github.com/zigcc/zig-msgpack/archive/refs/heads/main.tar.gz", - .hash = "zig_msgpack-0.0.14-evvueIkqBQA7F_-8hpmPYvAjmg9MOWJLm6p8sb0HnGem", - }, - }, - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - }, -} diff --git a/ipc-codegen/examples/zig/wsdb/generate.sh b/ipc-codegen/examples/zig/wsdb/generate.sh deleted file mode 100755 index 9c9fe638c304..000000000000 --- a/ipc-codegen/examples/zig/wsdb/generate.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# Generate WSDB Zig bindings from the committed schema. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CODEGEN="$(cd "$SCRIPT_DIR/../../.." && pwd)" - -mkdir -p "$SCRIPT_DIR/src/generated" - -node --experimental-strip-types --experimental-transform-types --no-warnings \ - "$CODEGEN/src/generate.ts" \ - --schema "$CODEGEN/schemas/wsdb_schema.json" \ - --lang zig \ - --out "$SCRIPT_DIR/src/generated" \ - --server --client --uds --ffi \ - --prefix Wsdb diff --git a/ipc-codegen/examples/zig/wsdb/src/main.zig b/ipc-codegen/examples/zig/wsdb/src/main.zig deleted file mode 100644 index 2e65067c85f4..000000000000 --- a/ipc-codegen/examples/zig/wsdb/src/main.zig +++ /dev/null @@ -1,24 +0,0 @@ -/// Zig WSDB Server — uses generated server dispatch + generic IPC transport. -/// -/// All command handlers return "not implemented" errors. -/// Edit src/generated/server.zig to implement your world-state logic. -/// -/// Usage: zig-wsdb --socket /tmp/wsdb.sock -const server = @import("generated/server_gen.zig"); - -pub fn main() !void { - var args = @import("std").process.args(); - _ = args.next(); - var socket_path: ?[]const u8 = null; - while (args.next()) |arg| { - if (@import("std").mem.eql(u8, arg, "--socket")) { - socket_path = args.next(); - } - } - const path = socket_path orelse { - @import("std").debug.print("Usage: zig-wsdb --socket \n", .{}); - @import("std").process.exit(1); - }; - - try server.serve(path); -} From 8ec85152654a59c9cadb7b2135648b1f1e3fe082 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Mon, 18 May 2026 17:13:56 +0000 Subject: [PATCH 09/11] fix(ipc-codegen): make ts-server matrix tests reliable under docker_isolate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous flow had `npx tsx ts/echo/echo_server.ts` for TS-side test binaries and waited only 2 seconds for the server's UDS socket to appear. That worked locally but routinely failed under CI's docker_isolate with CPUS=1, where `npx tsx` cold start took longer than 2s — every ts-as-server matrix pair came back as "server did not create socket within 2s". Two narrow fixes: - Add tsx as a real dependency in examples/ts/echo/package.json so it lands in node_modules/.bin after `npm install` (no more npx-resolve hop). - Replace `npx tsx ...` in run_cross_language_test.sh with `ts/echo/node_modules/.bin/tsx ...` and raise the wait-for-socket cap from 2s to 30s. Native binaries (rust/c++/zig) still bind in <100ms, so the longer ceiling only stretches worst-case TS startup. Verified: 18/18 still pass locally, now in 0s per test rather than 1-2s. --- .../examples/scripts/run_cross_language_test.sh | 13 ++++++++----- ipc-codegen/examples/ts/echo/package.json | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ipc-codegen/examples/scripts/run_cross_language_test.sh b/ipc-codegen/examples/scripts/run_cross_language_test.sh index 493384400b60..7ab75449befa 100755 --- a/ipc-codegen/examples/scripts/run_cross_language_test.sh +++ b/ipc-codegen/examples/scripts/run_cross_language_test.sh @@ -19,7 +19,7 @@ cd "$EXAMPLES_DIR" server_cmd_for() { case "$1" in rust) echo "rust/echo/target/debug/echo_server" ;; - ts) echo "npx tsx ts/echo/echo_server.ts" ;; + ts) echo "ts/echo/node_modules/.bin/tsx ts/echo/echo_server.ts" ;; cpp) echo "cpp/echo/echo_server" ;; zig) echo "zig/echo/zig-out/bin/echo_server" ;; *) echo "unknown lang: $1" >&2; exit 1 ;; @@ -29,7 +29,7 @@ server_cmd_for() { client_cmd_for() { case "$1" in rust) echo "rust/echo/target/debug/echo_client" ;; - ts) echo "npx tsx ts/echo/echo_client.ts" ;; + ts) echo "ts/echo/node_modules/.bin/tsx ts/echo/echo_client.ts" ;; cpp) echo "cpp/echo/echo_client" ;; zig) echo "zig/echo/zig-out/bin/echo_client" ;; *) echo "unknown lang: $1" >&2; exit 1 ;; @@ -43,7 +43,7 @@ run_golden() { rust/echo/target/debug/golden_test --golden-dir echo-schema/golden ;; ts) - npx tsx ts/echo/golden_test.ts + ts/echo/node_modules/.bin/tsx ts/echo/golden_test.ts ;; *) echo "golden tests only defined for rust and ts (got: $lang)" >&2 @@ -65,12 +65,15 @@ run_matrix() { local server_pid=$! trap "kill $server_pid 2>/dev/null || true; rm -f $socket" EXIT - for _ in $(seq 1 20); do + # Generous timeout: under docker_isolate with CPUS=1, `npx tsx` cold start + # alone routinely takes several seconds. Native binaries (rust/c++/zig) bind + # the socket in <100ms either way, so this only stretches the worst-case wait. + for _ in $(seq 1 300); do [ -S "$socket" ] && break sleep 0.1 done if [ ! -S "$socket" ]; then - echo "server did not create socket within 2s" >&2 + echo "server did not create socket within 30s" >&2 exit 1 fi diff --git a/ipc-codegen/examples/ts/echo/package.json b/ipc-codegen/examples/ts/echo/package.json index 3f4aaee24171..b8b506bd9c24 100644 --- a/ipc-codegen/examples/ts/echo/package.json +++ b/ipc-codegen/examples/ts/echo/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "dependencies": { - "msgpackr": "^1.10.0" + "msgpackr": "^1.10.0", + "tsx": "^4.19.0" } } From 951cf72892fa0cb6e4d04f980ca17afc48752423 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 19 May 2026 18:03:34 +0000 Subject: [PATCH 10/11] refactor(ipc-codegen): drop committed service schemas + bb-aware tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make ipc-codegen a self-contained codegen package with zero knowledge of its consumers: - Delete schemas/{bb,wsdb,cdb,avm}_schema.json + bb_curve_constants.json. Each consumer commits its own schema next to the C++ server that defines the wire format, and passes the path on the command line. - Delete scripts/{update,validate}_schemas.sh. Schema refresh + drift guarding is each consumer's responsibility now; the snapshot tooling belongs near the producing binary, not in the generator. - README + SCHEMA_SPEC: rewrite consumer-facing sections with neutral paths; drop bb/wsdb-specific layout and "yarn generate" instructions. - .rebuild_patterns: drop schemas/ pattern; pick up examples/ (echo schema + goldens live there). - generated TS error message: "Unknown error from barretenberg" -> "Unknown error from server". - Bundled cpp msgpack adaptor + generator banners: drop barretenberg attribution / "no barretenberg deps" wording from emitted output. Also fold in the clang-format-templates-at-destination fix (originally on PR2's branch) so PR1 ships a complete ipc-codegen and PR2 stays focused on consumer migrations. The echo example matrix (18 cross-language tests) still passes — the package's own self-test is unaffected. --- ipc-codegen/.rebuild_patterns | 2 +- ipc-codegen/README.md | 63 ++-- ipc-codegen/SCHEMA_SPEC.md | 7 +- ipc-codegen/schemas/avm_schema.json | 2 - ipc-codegen/schemas/bb_curve_constants.json | 36 -- ipc-codegen/schemas/bb_schema.json | 2 - ipc-codegen/schemas/cdb_schema.json | 2 - ipc-codegen/schemas/wsdb_schema.json | 2 - ipc-codegen/scripts/update_schemas.sh | 54 --- ipc-codegen/scripts/validate_schemas.sh | 56 ---- ipc-codegen/src/cpp_codegen.ts | 9 +- ipc-codegen/src/generate.ts | 10 +- ipc-codegen/src/typescript_codegen.ts | 313 +++++++++++------- .../templates/cpp/msgpack_struct_map_impl.hpp | 4 +- 14 files changed, 233 insertions(+), 329 deletions(-) delete mode 100644 ipc-codegen/schemas/avm_schema.json delete mode 100644 ipc-codegen/schemas/bb_curve_constants.json delete mode 100644 ipc-codegen/schemas/bb_schema.json delete mode 100644 ipc-codegen/schemas/cdb_schema.json delete mode 100644 ipc-codegen/schemas/wsdb_schema.json delete mode 100755 ipc-codegen/scripts/update_schemas.sh delete mode 100755 ipc-codegen/scripts/validate_schemas.sh diff --git a/ipc-codegen/.rebuild_patterns b/ipc-codegen/.rebuild_patterns index 010254c52d4a..15d048b8fba4 100644 --- a/ipc-codegen/.rebuild_patterns +++ b/ipc-codegen/.rebuild_patterns @@ -1,5 +1,5 @@ ^ipc-codegen/src/.*\.ts$ -^ipc-codegen/schemas/.*\.(json|msgpack)$ ^ipc-codegen/templates/ +^ipc-codegen/examples/ ^ipc-codegen/package\.json$ ^ipc-codegen/bootstrap\.sh$ diff --git a/ipc-codegen/README.md b/ipc-codegen/README.md index a0a72de223b3..b6a494c6ccd6 100644 --- a/ipc-codegen/README.md +++ b/ipc-codegen/README.md @@ -19,12 +19,6 @@ cd ipc-codegen ``` ipc-codegen/ bootstrap.sh # build / test / update_goldens / hash - schemas/ # committed JSON schemas (one per service) - avm_schema.json - bb_schema.json - cdb_schema.json - wsdb_schema.json - bb_curve_constants.json src/ # generator (TypeScript, runs under Node 22+) generate.ts # CLI entry point schema_visitor.ts # JSON schema -> CompiledSchema IR @@ -38,13 +32,12 @@ ipc-codegen/ ts/{ipc_client,ipc_server}.ts rust/{backend,error,ipc_client,ipc_server,uds_backend,ffi_backend}.rs zig/{backend,uds_backend,ffi_backend,ipc_client,ipc_server,ffi_client}.zig - scripts/ - update_schemas.sh # refresh schemas/ from current C++ binaries - validate_schemas.sh # CI guard: schemas/ matches the binaries examples/ # 4-language echo service (test harness, see below) SCHEMA_SPEC.md # wire protocol and schema-format reference ``` +The package contains no service schemas of its own. Each consumer owns and commits its schema next to the C++ server that defines the wire format, and invokes `generate.ts` with that local path. + ## CLI: `src/generate.ts` Invoked once per (schema, language) pair. Run directly with `node --experimental-strip-types`, or via `bootstrap.sh`. @@ -84,7 +77,7 @@ node --experimental-strip-types --experimental-transform-types --no-warnings \ |---|---| | `--cpp-namespace ` | C++ namespace, e.g. `bb::wsdb`. Default: lowercased prefix. | | `--cpp-wire-namespace ` | Inner namespace for wire types, default `wire`. | -| `--cpp-include-dir ` | Include-path prefix for cross-includes between generated files, e.g. `barretenberg/wsdb/generated`. Leave unset when generated files are in the same directory as their consumer. | +| `--cpp-include-dir ` | Include-path prefix for cross-includes between generated files, e.g. `myservice/generated`. Leave unset when generated files are in the same directory as their consumer. | ### Other @@ -95,72 +88,72 @@ node --experimental-strip-types --experimental-transform-types --no-warnings \ ## Worked examples -Each invocation produces both the per-command type definitions and the role(s) you request. +Paths below are illustrative — consumers commit their own schema next to the C++ server that owns the wire format and supply absolute or relative paths on the command line. -### TypeScript client + server, with curve constants (bb) +### TypeScript client + server, with curve constants ```sh src/generate.ts \ - --schema schemas/bb_schema.json \ + --schema /path/to/myservice_schema.json \ --lang ts \ - --out ../barretenberg/ts/src/cbind/generated \ + --out /path/to/output/generated \ --server --client \ - --prefix Bb --strip-method-prefix --curve-constants + --prefix MyService --strip-method-prefix --curve-constants ``` Produces `api_types.ts`, `async.ts`, `sync.ts`, `server.ts`, `ipc_client.ts` (template), `ipc_server.ts` (template), `curve_constants.ts`. -### C++ server (no client), under a barretenberg sub-include path (wsdb) +### C++ server + client, under a project sub-include path ```sh src/generate.ts \ - --schema schemas/wsdb_schema.json \ + --schema /path/to/myservice_schema.json \ --lang cpp \ - --out ../barretenberg/cpp/src/barretenberg/wsdb/generated \ + --out /path/to/myservice/generated \ --server --client \ - --cpp-namespace bb::wsdb --prefix Wsdb \ - --cpp-include-dir barretenberg/wsdb/generated + --cpp-namespace my::ns --prefix MyService \ + --cpp-include-dir myservice/generated ``` -Produces `wsdb_types.hpp`, `wsdb_ipc_client.{hpp,cpp}`, `wsdb_ipc_server.hpp`, plus the `ipc_client.hpp` / `ipc_server.hpp` / `msgpack_struct_map_impl.hpp` templates. Cross-includes use the supplied `--cpp-include-dir` prefix (`#include "barretenberg/wsdb/generated/wsdb_types.hpp"`). +Produces `myservice_types.hpp`, `myservice_ipc_client.{hpp,cpp}`, `myservice_ipc_server.hpp`, plus the `ipc_client.hpp` / `ipc_server.hpp` / `msgpack_struct_map_impl.hpp` templates. Cross-includes use the supplied `--cpp-include-dir` prefix (`#include "myservice/generated/myservice_types.hpp"`). -### Rust UDS + FFI client (wsdb) +### Rust UDS + FFI client ```sh src/generate.ts \ - --schema schemas/wsdb_schema.json \ + --schema /path/to/myservice_schema.json \ --lang rust \ - --out src/generated \ + --out /path/to/crate/src/generated \ --server --client --uds --ffi \ - --prefix Wsdb \ - --skeleton src + --prefix MyService \ + --skeleton /path/to/crate/src ``` -Produces `wsdb_types.rs`, `wsdb_client.rs`, `wsdb_server.rs`, `ipc_server.rs` (template), plus the backend templates (`backend.rs`, `error.rs`, `uds_backend.rs`, `ffi_backend.rs`). The skeleton flag also writes a one-time `wsdb_handlers.rs`, `main.rs`, and `Cargo.toml` into `src/` so a new service crate is buildable on first run. +Produces `myservice_types.rs`, `myservice_client.rs`, `myservice_server.rs`, `ipc_server.rs` (template), plus the backend templates (`backend.rs`, `error.rs`, `uds_backend.rs`, `ffi_backend.rs`). The skeleton flag also writes a one-time `myservice_handlers.rs`, `main.rs`, and `Cargo.toml` into the skeleton dir so a new service crate is buildable on first run. -### Zig client + server (avm) +### Zig client + server ```sh src/generate.ts \ - --schema schemas/avm_schema.json \ + --schema /path/to/myservice_schema.json \ --lang zig \ - --out src/generated \ + --out /path/to/output/generated \ --server --client --uds --ffi \ - --prefix Avm + --prefix MyService ``` -Produces `avm_types.zig`, `avm_client.zig`, `avm_server.zig`, plus backend templates. +Produces `myservice_types.zig`, `myservice_client.zig`, `myservice_server.zig`, plus backend templates. ## Adding a new service 1. **Define the C++ command structs** in your service's `.hpp`, each with `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS(...)`. Group them into a single `Command` and `Response` `NamedUnion`. -2. **Build the service binary** and run `./scripts/update_schemas.sh` — this calls ` msgpack schema` and writes the JSON to `schemas/_schema.json`. Commit the schema. -3. **Wire your consumer's `bootstrap.sh build` to invoke `src/generate.ts`** with the flags above. Generated files go under a `generated/` directory which is gitignored by convention. +2. **Snapshot the schema.** Build the service binary and run ` msgpack schema` to dump the JSON. Commit it next to the C++ source that defines it (e.g. alongside the `Command` / `Response` headers). This is the wire-format source of truth. +3. **Wire your consumer's build to invoke `src/generate.ts`** with the flags above, passing the absolute path to the committed schema and the desired output directory. Generated files go under a `generated/` directory which is gitignored by convention. 4. **Run `./bootstrap.sh test`** in `ipc-codegen/` to confirm the codegen and cross-language wire compat tests still pass. ## Schemas are the source of truth -The JSON files under `schemas/` are checked in. They're what `generate.ts` consumes. They're produced from the C++ binaries via `./scripts/update_schemas.sh` whenever the underlying commands change. `validate_schemas.sh` is the CI guard that diffs the committed JSON against the current binaries — a stale schema is a CI failure, not a runtime surprise. +The JSON schema is the wire contract between client and server. Consumers commit it next to the C++ server that defines the underlying `SERIALIZATION_FIELDS`, so the file lives close to what it describes and tracks with that code. Whenever a server-side command changes, refresh the JSON snapshot by running ` msgpack schema` against the rebuilt binary and committing the diff. Diverged schemas are a CI failure (each consumer is responsible for guarding its own snapshot). Each generated file embeds a `SCHEMA_HASH` so callers can detect at connection time that their bindings predate the server. diff --git a/ipc-codegen/SCHEMA_SPEC.md b/ipc-codegen/SCHEMA_SPEC.md index d47b9725602b..f2a0d1f9f26c 100644 --- a/ipc-codegen/SCHEMA_SPEC.md +++ b/ipc-codegen/SCHEMA_SPEC.md @@ -230,13 +230,12 @@ To add a new command to a service: 1. Define the command struct in C++ with `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS` 2. Add a nested `Response` struct with its own `MSGPACK_SCHEMA_NAME` and `SERIALIZATION_FIELDS` 3. Add both to the service's `Command` and `CommandResponse` NamedUnion types -4. Run `yarn generate` to regenerate all language bindings +4. Re-snapshot the schema JSON and re-run ipc-codegen for every target language 5. Verify generated code compiles in all target languages ## Source Files -- Schema export: `barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_impl.hpp` -- Schema naming: `barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/schema_name.hpp` -- NamedUnion: `barretenberg/cpp/src/barretenberg/common/named_union.hpp` - Schema visitor (IR compiler): `ipc-codegen/src/schema_visitor.ts` - CLI entry point: `ipc-codegen/src/generate.ts` + +The schema JSON is produced by the consumer's own C++ msgpack reflection (typically a ` msgpack schema` subcommand that walks `SERIALIZATION_FIELDS` and `NamedUnion`s and prints the IR). ipc-codegen treats the resulting JSON as the source of truth and never reaches back into the producer. diff --git a/ipc-codegen/schemas/avm_schema.json b/ipc-codegen/schemas/avm_schema.json deleted file mode 100644 index b1e8e050b4d5..000000000000 --- a/ipc-codegen/schemas/avm_schema.json +++ /dev/null @@ -1,2 +0,0 @@ -{"__typename":"AvmApi","commands":["named_union",[["AvmSimulate",{"__typename":"AvmSimulate","inputs":["vector",["unsigned char"]]}],["AvmSimulateWithHints",{"__typename":"AvmSimulateWithHints","inputs":["vector",["unsigned char"]]}],["AvmShutdown",{"__typename":"AvmShutdown"}]]],"responses":["named_union",[["AvmErrorResponse",{"__typename":"AvmErrorResponse","message":"string"}],["AvmSimulateResponse",{"__typename":"AvmSimulateResponse","result":["vector",["unsigned char"]]}],["AvmSimulateWithHintsResponse",{"__typename":"AvmSimulateWithHintsResponse","result":["vector",["unsigned char"]]}],["AvmShutdownResponse",{"__typename":"AvmShutdownResponse"}]]]} - diff --git a/ipc-codegen/schemas/bb_curve_constants.json b/ipc-codegen/schemas/bb_curve_constants.json deleted file mode 100644 index 20dab049c505..000000000000 --- a/ipc-codegen/schemas/bb_curve_constants.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "bn254_fr_modulus": "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", - "bn254_fq_modulus": "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", - "bn254_g1_generator": { - "x": "0000000000000000000000000000000000000000000000000000000000000001", - "y": "0000000000000000000000000000000000000000000000000000000000000002" - }, - "bn254_g2_generator": { - "x": [ - "1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed", - "198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2" - ], - "y": [ - "12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa", - "090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b" - ] - }, - "grumpkin_fr_modulus": "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", - "grumpkin_fq_modulus": "30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", - "grumpkin_g1_generator": { - "x": "0000000000000000000000000000000000000000000000000000000000000001", - "y": "0000000000000002cf135e7506a45d632d270d45f1181294833fc48d823f272c" - }, - "secp256k1_fr_modulus": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", - "secp256k1_fq_modulus": "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", - "secp256k1_g1_generator": { - "x": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "y": "483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" - }, - "secp256r1_fr_modulus": "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", - "secp256r1_fq_modulus": "ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", - "secp256r1_g1_generator": { - "x": "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", - "y": "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5" - } -} \ No newline at end of file diff --git a/ipc-codegen/schemas/bb_schema.json b/ipc-codegen/schemas/bb_schema.json deleted file mode 100644 index a8defb4d4052..000000000000 --- a/ipc-codegen/schemas/bb_schema.json +++ /dev/null @@ -1,2 +0,0 @@ -{"__typename":"Api","commands":["named_union",[["BbCircuitProve",{"__typename":"BbCircuitProve","circuit":{"__typename":"CircuitInput","name":"string","bytecode":["vector",["unsigned char"]],"verification_key":["vector",["unsigned char"]]},"witness":["vector",["unsigned char"]],"settings":{"__typename":"ProofSystemSettings","ipa_accumulation":"bool","oracle_hash_type":"string","disable_zk":"bool","optimized_solidity_verifier":"bool"}}],["BbCircuitComputeVk",{"__typename":"BbCircuitComputeVk","circuit":{"__typename":"CircuitInputNoVK","name":"string","bytecode":["vector",["unsigned char"]]},"settings":"ProofSystemSettings"}],["BbCircuitStats",{"__typename":"BbCircuitStats","circuit":"CircuitInput","include_gates_per_opcode":"bool","settings":"ProofSystemSettings"}],["BbCircuitVerify",{"__typename":"BbCircuitVerify","verification_key":["vector",["unsigned char"]],"public_inputs":["vector",[["vector",["unsigned char"]]]],"proof":["vector",[["vector",["unsigned char"]]]],"settings":"ProofSystemSettings"}],["BbChonkComputeVk",{"__typename":"BbChonkComputeVk","circuit":"CircuitInputNoVK"}],["BbChonkStart",{"__typename":"BbChonkStart","num_circuits":"unsigned int"}],["BbChonkLoad",{"__typename":"BbChonkLoad","circuit":"CircuitInput"}],["BbChonkAccumulate",{"__typename":"BbChonkAccumulate","witness":["vector",["unsigned char"]]}],["BbChonkProve",{"__typename":"BbChonkProve"}],["BbChonkVerify",{"__typename":"BbChonkVerify","proof":{"__typename":"ChonkProof","hiding_oink_proof":["vector",[["array",["unsigned char",32]]]],"merge_proof":["vector",[["array",["unsigned char",32]]]],"eccvm_proof":["vector",[["array",["unsigned char",32]]]],"ipa_proof":["vector",[["array",["unsigned char",32]]]],"joint_proof":["vector",[["array",["unsigned char",32]]]]},"vk":["vector",["unsigned char"]]}],["BbChonkBatchVerify",{"__typename":"BbChonkBatchVerify","proofs":["vector",["ChonkProof"]],"vks":["vector",[["vector",["unsigned char"]]]]}],["BbVkAsFields",{"__typename":"BbVkAsFields","verification_key":["vector",["unsigned char"]]}],["BbMegaVkAsFields",{"__typename":"BbMegaVkAsFields","verification_key":["vector",["unsigned char"]]}],["BbCircuitWriteSolidityVerifier",{"__typename":"BbCircuitWriteSolidityVerifier","verification_key":["vector",["unsigned char"]],"settings":"ProofSystemSettings"}],["BbChonkCheckPrecomputedVk",{"__typename":"BbChonkCheckPrecomputedVk","circuit":"CircuitInput"}],["BbChonkStats",{"__typename":"BbChonkStats","circuit":"CircuitInputNoVK","include_gates_per_opcode":"bool"}],["BbChonkCompressProof",{"__typename":"BbChonkCompressProof","proof":"ChonkProof"}],["BbChonkDecompressProof",{"__typename":"BbChonkDecompressProof","compressed_proof":["vector",["unsigned char"]]}],["BbPoseidon2Hash",{"__typename":"BbPoseidon2Hash","inputs":["vector",[["array",["unsigned char",32]]]]}],["BbPoseidon2Permutation",{"__typename":"BbPoseidon2Permutation","inputs":["array",[["array",["unsigned char",32]],4]]}],["BbPedersenCommit",{"__typename":"BbPedersenCommit","inputs":["vector",[["array",["unsigned char",32]]]],"hash_index":"unsigned int"}],["BbPedersenHash",{"__typename":"BbPedersenHash","inputs":["vector",[["array",["unsigned char",32]]]],"hash_index":"unsigned int"}],["BbPedersenHashBuffer",{"__typename":"BbPedersenHashBuffer","input":["vector",["unsigned char"]],"hash_index":"unsigned int"}],["BbBlake2s",{"__typename":"BbBlake2s","data":["vector",["unsigned char"]]}],["BbBlake2sToField",{"__typename":"BbBlake2sToField","data":["vector",["unsigned char"]]}],["BbAesEncrypt",{"__typename":"BbAesEncrypt","plaintext":["vector",["unsigned char"]],"iv":["vector",["unsigned char"]],"key":["vector",["unsigned char"]],"length":"unsigned int"}],["BbAesDecrypt",{"__typename":"BbAesDecrypt","ciphertext":["vector",["unsigned char"]],"iv":["vector",["unsigned char"]],"key":["vector",["unsigned char"]],"length":"unsigned int"}],["BbGrumpkinMul",{"__typename":"BbGrumpkinMul","point":{"__typename":"GrumpkinPoint","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"scalar":["array",["unsigned char",32]]}],["BbGrumpkinAdd",{"__typename":"BbGrumpkinAdd","point_a":"GrumpkinPoint","point_b":"GrumpkinPoint"}],["BbGrumpkinBatchMul",{"__typename":"BbGrumpkinBatchMul","points":["vector",["GrumpkinPoint"]],"scalar":["array",["unsigned char",32]]}],["BbGrumpkinGetRandomFr",{"__typename":"BbGrumpkinGetRandomFr","points_buf":["vector",["unsigned char"]]}],["BbGrumpkinReduce512",{"__typename":"BbGrumpkinReduce512","input":["vector",["unsigned char"]]}],["BbSecp256k1Mul",{"__typename":"BbSecp256k1Mul","point":{"__typename":"Secp256k1Point","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"scalar":["array",["unsigned char",32]]}],["BbSecp256k1GetRandomFr",{"__typename":"BbSecp256k1GetRandomFr","points_buf":["vector",["unsigned char"]]}],["BbSecp256k1Reduce512",{"__typename":"BbSecp256k1Reduce512","input":["vector",["unsigned char"]]}],["BbBn254FrSqrt",{"__typename":"BbBn254FrSqrt","input":["array",["unsigned char",32]]}],["BbBn254FqSqrt",{"__typename":"BbBn254FqSqrt","input":["array",["unsigned char",32]]}],["BbBn254G1Mul",{"__typename":"BbBn254G1Mul","point":{"__typename":"Bn254G1Point","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"scalar":["array",["unsigned char",32]]}],["BbBn254G2Mul",{"__typename":"BbBn254G2Mul","point":{"__typename":"Bn254G2Point","x":["array",[["array",["unsigned char",32]],2]],"y":["array",[["array",["unsigned char",32]],2]]},"scalar":["array",["unsigned char",32]]}],["BbBn254G1IsOnCurve",{"__typename":"BbBn254G1IsOnCurve","point":"Bn254G1Point"}],["BbBn254G1FromCompressed",{"__typename":"BbBn254G1FromCompressed","compressed":["array",["unsigned char",32]]}],["BbSchnorrComputePublicKey",{"__typename":"BbSchnorrComputePublicKey","private_key":["array",["unsigned char",32]]}],["BbSchnorrConstructSignature",{"__typename":"BbSchnorrConstructSignature","message":["vector",["unsigned char"]],"private_key":["array",["unsigned char",32]]}],["BbSchnorrVerifySignature",{"__typename":"BbSchnorrVerifySignature","message":["vector",["unsigned char"]],"public_key":"GrumpkinPoint","s":["array",["unsigned char",32]],"e":["array",["unsigned char",32]]}],["BbEcdsaSecp256k1ComputePublicKey",{"__typename":"BbEcdsaSecp256k1ComputePublicKey","private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256r1ComputePublicKey",{"__typename":"BbEcdsaSecp256r1ComputePublicKey","private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256k1ConstructSignature",{"__typename":"BbEcdsaSecp256k1ConstructSignature","message":["vector",["unsigned char"]],"private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256r1ConstructSignature",{"__typename":"BbEcdsaSecp256r1ConstructSignature","message":["vector",["unsigned char"]],"private_key":["array",["unsigned char",32]]}],["BbEcdsaSecp256k1RecoverPublicKey",{"__typename":"BbEcdsaSecp256k1RecoverPublicKey","message":["vector",["unsigned char"]],"r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256r1RecoverPublicKey",{"__typename":"BbEcdsaSecp256r1RecoverPublicKey","message":["vector",["unsigned char"]],"r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256k1VerifySignature",{"__typename":"BbEcdsaSecp256k1VerifySignature","message":["vector",["unsigned char"]],"public_key":"Secp256k1Point","r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256r1VerifySignature",{"__typename":"BbEcdsaSecp256r1VerifySignature","message":["vector",["unsigned char"]],"public_key":{"__typename":"Secp256r1Point","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbSrsInitSrs",{"__typename":"BbSrsInitSrs","points_buf":["vector",["unsigned char"]],"num_points":"unsigned int","g2_point":["vector",["unsigned char"]]}],["BbChonkBatchVerifierStart",{"__typename":"BbChonkBatchVerifierStart","vks":["vector",[["vector",["unsigned char"]]]],"num_cores":"unsigned int","batch_size":"unsigned int","fifo_path":"string"}],["BbChonkBatchVerifierQueue",{"__typename":"BbChonkBatchVerifierQueue","request_id":"unsigned long","vk_index":"unsigned int","proof_fields":["vector",[["array",["unsigned char",32]]]]}],["BbChonkBatchVerifierStop",{"__typename":"BbChonkBatchVerifierStop"}],["BbSrsInitGrumpkinSrs",{"__typename":"BbSrsInitGrumpkinSrs","points_buf":["vector",["unsigned char"]],"num_points":"unsigned int"}],["BbShutdown",{"__typename":"BbShutdown"}]]],"responses":["named_union",[["BbErrorResponse",{"__typename":"BbErrorResponse","message":"string"}],["BbCircuitProveResponse",{"__typename":"BbCircuitProveResponse","public_inputs":["vector",[["vector",["unsigned char"]]]],"proof":["vector",[["vector",["unsigned char"]]]],"vk":{"__typename":"BbCircuitComputeVkResponse","bytes":["vector",["unsigned char"]],"fields":["vector",[["vector",["unsigned char"]]]],"hash":["vector",["unsigned char"]]}}],["BbCircuitComputeVkResponse","BbCircuitComputeVkResponse"],["BbCircuitInfoResponse",{"__typename":"BbCircuitInfoResponse","num_gates":"unsigned int","num_gates_dyadic":"unsigned int","num_acir_opcodes":"unsigned int","gates_per_opcode":["vector",["unsigned int"]]}],["BbCircuitVerifyResponse",{"__typename":"BbCircuitVerifyResponse","verified":"bool"}],["BbChonkComputeVkResponse",{"__typename":"BbChonkComputeVkResponse","bytes":["vector",["unsigned char"]],"fields":["vector",[["array",["unsigned char",32]]]]}],["BbChonkStartResponse",{"__typename":"BbChonkStartResponse"}],["BbChonkLoadResponse",{"__typename":"BbChonkLoadResponse"}],["BbChonkAccumulateResponse",{"__typename":"BbChonkAccumulateResponse"}],["BbChonkProveResponse",{"__typename":"BbChonkProveResponse","proof":"ChonkProof"}],["BbChonkVerifyResponse",{"__typename":"BbChonkVerifyResponse","valid":"bool"}],["BbChonkBatchVerifyResponse",{"__typename":"BbChonkBatchVerifyResponse","valid":"bool"}],["BbVkAsFieldsResponse",{"__typename":"BbVkAsFieldsResponse","fields":["vector",[["array",["unsigned char",32]]]]}],["BbMegaVkAsFieldsResponse",{"__typename":"BbMegaVkAsFieldsResponse","fields":["vector",[["array",["unsigned char",32]]]]}],["BbCircuitWriteSolidityVerifierResponse",{"__typename":"BbCircuitWriteSolidityVerifierResponse","solidity_code":"string"}],["BbChonkCheckPrecomputedVkResponse",{"__typename":"BbChonkCheckPrecomputedVkResponse","valid":"bool","actual_vk":["vector",["unsigned char"]]}],["BbChonkStatsResponse",{"__typename":"BbChonkStatsResponse","acir_opcodes":"unsigned int","circuit_size":"unsigned int","gates_per_opcode":["vector",["unsigned int"]]}],["BbChonkCompressProofResponse",{"__typename":"BbChonkCompressProofResponse","compressed_proof":["vector",["unsigned char"]]}],["BbChonkDecompressProofResponse",{"__typename":"BbChonkDecompressProofResponse","proof":"ChonkProof"}],["BbPoseidon2HashResponse",{"__typename":"BbPoseidon2HashResponse","hash":["array",["unsigned char",32]]}],["BbPoseidon2PermutationResponse",{"__typename":"BbPoseidon2PermutationResponse","outputs":["array",[["array",["unsigned char",32]],4]]}],["BbPedersenCommitResponse",{"__typename":"BbPedersenCommitResponse","point":"GrumpkinPoint"}],["BbPedersenHashResponse",{"__typename":"BbPedersenHashResponse","hash":["array",["unsigned char",32]]}],["BbPedersenHashBufferResponse",{"__typename":"BbPedersenHashBufferResponse","hash":["array",["unsigned char",32]]}],["BbBlake2sResponse",{"__typename":"BbBlake2sResponse","hash":["array",["unsigned char",32]]}],["BbBlake2sToFieldResponse",{"__typename":"BbBlake2sToFieldResponse","field":["array",["unsigned char",32]]}],["BbAesEncryptResponse",{"__typename":"BbAesEncryptResponse","ciphertext":["vector",["unsigned char"]]}],["BbAesDecryptResponse",{"__typename":"BbAesDecryptResponse","plaintext":["vector",["unsigned char"]]}],["BbGrumpkinMulResponse",{"__typename":"BbGrumpkinMulResponse","point":"GrumpkinPoint"}],["BbGrumpkinAddResponse",{"__typename":"BbGrumpkinAddResponse","point":"GrumpkinPoint"}],["BbGrumpkinBatchMulResponse",{"__typename":"BbGrumpkinBatchMulResponse","points":["vector",["GrumpkinPoint"]]}],["BbGrumpkinGetRandomFrResponse",{"__typename":"BbGrumpkinGetRandomFrResponse","value":["array",["unsigned char",32]]}],["BbGrumpkinReduce512Response",{"__typename":"BbGrumpkinReduce512Response","value":["array",["unsigned char",32]]}],["BbSecp256k1MulResponse",{"__typename":"BbSecp256k1MulResponse","point":"Secp256k1Point"}],["BbSecp256k1GetRandomFrResponse",{"__typename":"BbSecp256k1GetRandomFrResponse","value":["array",["unsigned char",32]]}],["BbSecp256k1Reduce512Response",{"__typename":"BbSecp256k1Reduce512Response","value":["array",["unsigned char",32]]}],["BbBn254FrSqrtResponse",{"__typename":"BbBn254FrSqrtResponse","is_square_root":"bool","value":["array",["unsigned char",32]]}],["BbBn254FqSqrtResponse",{"__typename":"BbBn254FqSqrtResponse","is_square_root":"bool","value":["array",["unsigned char",32]]}],["BbBn254G1MulResponse",{"__typename":"BbBn254G1MulResponse","point":"Bn254G1Point"}],["BbBn254G2MulResponse",{"__typename":"BbBn254G2MulResponse","point":"Bn254G2Point"}],["BbBn254G1IsOnCurveResponse",{"__typename":"BbBn254G1IsOnCurveResponse","is_on_curve":"bool"}],["BbBn254G1FromCompressedResponse",{"__typename":"BbBn254G1FromCompressedResponse","point":"Bn254G1Point"}],["BbSchnorrComputePublicKeyResponse",{"__typename":"BbSchnorrComputePublicKeyResponse","public_key":"GrumpkinPoint"}],["BbSchnorrConstructSignatureResponse",{"__typename":"BbSchnorrConstructSignatureResponse","s":["array",["unsigned char",32]],"e":["array",["unsigned char",32]]}],["BbSchnorrVerifySignatureResponse",{"__typename":"BbSchnorrVerifySignatureResponse","verified":"bool"}],["BbEcdsaSecp256k1ComputePublicKeyResponse",{"__typename":"BbEcdsaSecp256k1ComputePublicKeyResponse","public_key":"Secp256k1Point"}],["BbEcdsaSecp256r1ComputePublicKeyResponse",{"__typename":"BbEcdsaSecp256r1ComputePublicKeyResponse","public_key":"Secp256r1Point"}],["BbEcdsaSecp256k1ConstructSignatureResponse",{"__typename":"BbEcdsaSecp256k1ConstructSignatureResponse","r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256r1ConstructSignatureResponse",{"__typename":"BbEcdsaSecp256r1ConstructSignatureResponse","r":["array",["unsigned char",32]],"s":["array",["unsigned char",32]],"v":"unsigned char"}],["BbEcdsaSecp256k1RecoverPublicKeyResponse",{"__typename":"BbEcdsaSecp256k1RecoverPublicKeyResponse","public_key":"Secp256k1Point"}],["BbEcdsaSecp256r1RecoverPublicKeyResponse",{"__typename":"BbEcdsaSecp256r1RecoverPublicKeyResponse","public_key":"Secp256r1Point"}],["BbEcdsaSecp256k1VerifySignatureResponse",{"__typename":"BbEcdsaSecp256k1VerifySignatureResponse","verified":"bool"}],["BbEcdsaSecp256r1VerifySignatureResponse",{"__typename":"BbEcdsaSecp256r1VerifySignatureResponse","verified":"bool"}],["BbSrsInitSrsResponse",{"__typename":"BbSrsInitSrsResponse","points_buf":["vector",["unsigned char"]]}],["BbChonkBatchVerifierStartResponse",{"__typename":"BbChonkBatchVerifierStartResponse"}],["BbChonkBatchVerifierQueueResponse",{"__typename":"BbChonkBatchVerifierQueueResponse"}],["BbChonkBatchVerifierStopResponse",{"__typename":"BbChonkBatchVerifierStopResponse"}],["BbSrsInitGrumpkinSrsResponse",{"__typename":"BbSrsInitGrumpkinSrsResponse","points_buf":["vector",["unsigned char"]]}],["BbShutdownResponse",{"__typename":"BbShutdownResponse"}]]]} - diff --git a/ipc-codegen/schemas/cdb_schema.json b/ipc-codegen/schemas/cdb_schema.json deleted file mode 100644 index 3e54b0e2035f..000000000000 --- a/ipc-codegen/schemas/cdb_schema.json +++ /dev/null @@ -1,2 +0,0 @@ -{"__typename":"CdbApi","commands":["named_union",[["CdbGetContractInstance",{"__typename":"CdbGetContractInstance","address":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbGetContractClass",{"__typename":"CdbGetContractClass","classId":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbGetBytecodeCommitment",{"__typename":"CdbGetBytecodeCommitment","classId":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbGetDebugFunctionName",{"__typename":"CdbGetDebugFunctionName","address":["array",["unsigned char",32]],"selector":["array",["unsigned char",32]],"forkId":"unsigned long"}],["CdbAddContracts",{"__typename":"CdbAddContracts","contractDeploymentData":{"__typename":"ContractDeploymentData","contractClassLogs":["vector",[{"__typename":"ContractClassLog","contractAddress":["array",["unsigned char",32]],"fields":{"__typename":"ContractClassLogFields","fields":["vector",[["array",["unsigned char",32]]]]},"emittedLength":"unsigned int"}]],"privateLogs":["vector",[{"__typename":"PrivateLog","fields":["vector",[["array",["unsigned char",32]]]],"emittedLength":"unsigned int"}]]},"forkId":"unsigned long"}],["CdbCreateCheckpoint",{"__typename":"CdbCreateCheckpoint","forkId":"unsigned long"}],["CdbCommitCheckpoint",{"__typename":"CdbCommitCheckpoint","forkId":"unsigned long"}],["CdbRevertCheckpoint",{"__typename":"CdbRevertCheckpoint","forkId":"unsigned long"}],["CdbAddContractClass",{"__typename":"CdbAddContractClass","contractClass":{"__typename":"ContractClass","id":["array",["unsigned char",32]],"artifactHash":["array",["unsigned char",32]],"privateFunctionsRoot":["array",["unsigned char",32]],"packedBytecode":["vector",["unsigned char"]]},"bytecodeCommitment":["array",["unsigned char",32]]}],["CdbAddContractInstance",{"__typename":"CdbAddContractInstance","address":["array",["unsigned char",32]],"instance":{"__typename":"ContractInstance","salt":["array",["unsigned char",32]],"deployer":["array",["unsigned char",32]],"currentContractClassId":["array",["unsigned char",32]],"originalContractClassId":["array",["unsigned char",32]],"initializationHash":["array",["unsigned char",32]],"publicKeys":{"__typename":"PublicKeys","masterNullifierPublicKey":{"__typename":"GrumpkinPoint","x":["array",["unsigned char",32]],"y":["array",["unsigned char",32]]},"masterIncomingViewingPublicKey":"GrumpkinPoint","masterOutgoingViewingPublicKey":"GrumpkinPoint","masterTaggingPublicKey":"GrumpkinPoint"}}}],["CdbRegisterFunctionSignatures",{"__typename":"CdbRegisterFunctionSignatures","signatures":["vector",["string"]]}],["CdbGetContractClassIds",{"__typename":"CdbGetContractClassIds"}],["CdbShutdown",{"__typename":"CdbShutdown"}]]],"responses":["named_union",[["CdbErrorResponse",{"__typename":"CdbErrorResponse","message":"string"}],["CdbGetContractInstanceResponse",{"__typename":"CdbGetContractInstanceResponse","instance":["optional",["ContractInstance"]]}],["CdbGetContractClassResponse",{"__typename":"CdbGetContractClassResponse","contractClass":["optional",["ContractClass"]]}],["CdbGetBytecodeCommitmentResponse",{"__typename":"CdbGetBytecodeCommitmentResponse","commitment":["optional",[["array",["unsigned char",32]]]]}],["CdbGetDebugFunctionNameResponse",{"__typename":"CdbGetDebugFunctionNameResponse","name":["optional",["string"]]}],["CdbAddContractsResponse",{"__typename":"CdbAddContractsResponse"}],["CdbCreateCheckpointResponse",{"__typename":"CdbCreateCheckpointResponse"}],["CdbCommitCheckpointResponse",{"__typename":"CdbCommitCheckpointResponse"}],["CdbRevertCheckpointResponse",{"__typename":"CdbRevertCheckpointResponse"}],["CdbAddContractClassResponse",{"__typename":"CdbAddContractClassResponse"}],["CdbAddContractInstanceResponse",{"__typename":"CdbAddContractInstanceResponse"}],["CdbRegisterFunctionSignaturesResponse",{"__typename":"CdbRegisterFunctionSignaturesResponse"}],["CdbGetContractClassIdsResponse",{"__typename":"CdbGetContractClassIdsResponse","classIds":["vector",[["array",["unsigned char",32]]]]}],["CdbShutdownResponse",{"__typename":"CdbShutdownResponse"}]]]} - diff --git a/ipc-codegen/schemas/wsdb_schema.json b/ipc-codegen/schemas/wsdb_schema.json deleted file mode 100644 index 8959153dca13..000000000000 --- a/ipc-codegen/schemas/wsdb_schema.json +++ /dev/null @@ -1,2 +0,0 @@ -{"__typename":"WsdbApi","commands":["named_union",[["WsdbGetTreeInfo",{"__typename":"WsdbGetTreeInfo","treeId":"MerkleTreeId","revision":{"__typename":"WorldStateRevision","forkId":"unsigned long","blockNumber":"unsigned int","includeUncommitted":"bool"}}],["WsdbGetStateReference",{"__typename":"WsdbGetStateReference","revision":"WorldStateRevision"}],["WsdbGetInitialStateReference",{"__typename":"WsdbGetInitialStateReference"}],["WsdbGetLeafValue",{"__typename":"WsdbGetLeafValue","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndex":"unsigned long"}],["WsdbGetLeafPreimage",{"__typename":"WsdbGetLeafPreimage","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndex":"unsigned long"}],["WsdbGetSiblingPath",{"__typename":"WsdbGetSiblingPath","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndex":"unsigned long"}],["WsdbGetBlockNumbersForLeafIndices",{"__typename":"WsdbGetBlockNumbersForLeafIndices","treeId":"MerkleTreeId","revision":"WorldStateRevision","leafIndices":["vector",["unsigned long"]]}],["WsdbFindLeafIndices",{"__typename":"WsdbFindLeafIndices","treeId":"MerkleTreeId","revision":"WorldStateRevision","leaves":["vector",[["vector",["unsigned char"]]]],"startIndex":"unsigned long"}],["WsdbFindLowLeaf",{"__typename":"WsdbFindLowLeaf","treeId":"MerkleTreeId","revision":"WorldStateRevision","key":["array",["unsigned char",32]]}],["WsdbFindSiblingPaths",{"__typename":"WsdbFindSiblingPaths","treeId":"MerkleTreeId","revision":"WorldStateRevision","leaves":["vector",[["vector",["unsigned char"]]]]}],["WsdbAppendLeaves",{"__typename":"WsdbAppendLeaves","treeId":"MerkleTreeId","leaves":["vector",[["vector",["unsigned char"]]]],"forkId":"unsigned long"}],["WsdbBatchInsert",{"__typename":"WsdbBatchInsert","treeId":"MerkleTreeId","leaves":["vector",[["vector",["unsigned char"]]]],"subtreeDepth":"unsigned int","forkId":"unsigned long"}],["WsdbSequentialInsert",{"__typename":"WsdbSequentialInsert","treeId":"MerkleTreeId","leaves":["vector",[["vector",["unsigned char"]]]],"forkId":"unsigned long"}],["WsdbUpdateArchive",{"__typename":"WsdbUpdateArchive","blockStateRef":"unordered_map","blockHeaderHash":["array",["unsigned char",32]],"forkId":"unsigned long"}],["WsdbCommit",{"__typename":"WsdbCommit"}],["WsdbRollback",{"__typename":"WsdbRollback"}],["WsdbSyncBlock",{"__typename":"WsdbSyncBlock","blockNumber":"unsigned int","blockStateRef":"unordered_map","blockHeaderHash":["array",["unsigned char",32]],"paddedNoteHashes":["vector",[["array",["unsigned char",32]]]],"paddedL1ToL2Messages":["vector",[["array",["unsigned char",32]]]],"paddedNullifiers":["vector",[{"__typename":"NullifierLeafValue","nullifier":["array",["unsigned char",32]]}]],"publicDataWrites":["vector",[{"__typename":"PublicDataLeafValue","slot":["array",["unsigned char",32]],"value":["array",["unsigned char",32]]}]]}],["WsdbCreateFork",{"__typename":"WsdbCreateFork","latest":"bool","blockNumber":"unsigned int"}],["WsdbDeleteFork",{"__typename":"WsdbDeleteFork","forkId":"unsigned long"}],["WsdbFinalizeBlocks",{"__typename":"WsdbFinalizeBlocks","toBlockNumber":"unsigned int"}],["WsdbUnwindBlocks",{"__typename":"WsdbUnwindBlocks","toBlockNumber":"unsigned int"}],["WsdbRemoveHistoricalBlocks",{"__typename":"WsdbRemoveHistoricalBlocks","toBlockNumber":"unsigned int"}],["WsdbGetStatus",{"__typename":"WsdbGetStatus"}],["WsdbCreateCheckpoint",{"__typename":"WsdbCreateCheckpoint","forkId":"unsigned long"}],["WsdbCommitCheckpoint",{"__typename":"WsdbCommitCheckpoint","forkId":"unsigned long"}],["WsdbRevertCheckpoint",{"__typename":"WsdbRevertCheckpoint","forkId":"unsigned long"}],["WsdbCommitAllCheckpoints",{"__typename":"WsdbCommitAllCheckpoints","forkId":"unsigned long"}],["WsdbRevertAllCheckpoints",{"__typename":"WsdbRevertAllCheckpoints","forkId":"unsigned long"}],["WsdbCopyStores",{"__typename":"WsdbCopyStores","dstPath":"string","compact":["optional",["bool"]]}],["WsdbShutdown",{"__typename":"WsdbShutdown"}]]],"responses":["named_union",[["WsdbErrorResponse",{"__typename":"WsdbErrorResponse","message":"string"}],["WsdbGetTreeInfoResponse",{"__typename":"WsdbGetTreeInfoResponse","treeId":"MerkleTreeId","root":["array",["unsigned char",32]],"size":"unsigned long","depth":"unsigned int"}],["WsdbGetStateReferenceResponse",{"__typename":"WsdbGetStateReferenceResponse","state":"unordered_map"}],["WsdbGetInitialStateReferenceResponse",{"__typename":"WsdbGetInitialStateReferenceResponse","state":"unordered_map"}],["WsdbGetLeafValueResponse",{"__typename":"WsdbGetLeafValueResponse","value":["optional",[["vector",["unsigned char"]]]]}],["WsdbGetLeafPreimageResponse",{"__typename":"WsdbGetLeafPreimageResponse","preimage":["optional",[["vector",["unsigned char"]]]]}],["WsdbGetSiblingPathResponse",{"__typename":"WsdbGetSiblingPathResponse","path":["vector",[["array",["unsigned char",32]]]]}],["WsdbGetBlockNumbersForLeafIndicesResponse",{"__typename":"WsdbGetBlockNumbersForLeafIndicesResponse","blockNumbers":["vector",[["optional",["unsigned int"]]]]}],["WsdbFindLeafIndicesResponse",{"__typename":"WsdbFindLeafIndicesResponse","indices":["vector",[["optional",["unsigned long"]]]]}],["WsdbFindLowLeafResponse",{"__typename":"WsdbFindLowLeafResponse","alreadyPresent":"bool","index":"unsigned long"}],["WsdbFindSiblingPathsResponse",{"__typename":"WsdbFindSiblingPathsResponse","paths":["vector",[["optional",[{"__typename":"SiblingPathAndIndex","index":"unsigned long","path":["vector",[["array",["unsigned char",32]]]]}]]]]}],["WsdbAppendLeavesResponse",{"__typename":"WsdbAppendLeavesResponse"}],["WsdbBatchInsertResponse",{"__typename":"WsdbBatchInsertResponse","result":["vector",["unsigned char"]]}],["WsdbSequentialInsertResponse",{"__typename":"WsdbSequentialInsertResponse","result":["vector",["unsigned char"]]}],["WsdbUpdateArchiveResponse",{"__typename":"WsdbUpdateArchiveResponse"}],["WsdbCommitResponse",{"__typename":"WsdbCommitResponse","status":{"__typename":"WorldStateStatusFull","summary":{"__typename":"WorldStateStatusSummary","unfinalizedBlockNumber":"unsigned long","finalizedBlockNumber":"unsigned long","oldestHistoricalBlock":"unsigned long","treesAreSynched":"bool"},"dbStats":{"__typename":"WorldStateDBStats","noteHashTreeStats":{"__typename":"TreeDBStats","mapSize":"unsigned long","physicalFileSize":"unsigned long","blocksDBStats":{"__typename":"DBStats","name":"string","numDataItems":"unsigned long","totalUsedSize":"unsigned long"},"nodesDBStats":"DBStats","leafPreimagesDBStats":"DBStats","leafIndicesDBStats":"DBStats","blockIndicesDBStats":"DBStats"},"messageTreeStats":"TreeDBStats","archiveTreeStats":"TreeDBStats","publicDataTreeStats":"TreeDBStats","nullifierTreeStats":"TreeDBStats"},"meta":{"__typename":"WorldStateMeta","noteHashTreeMeta":{"__typename":"TreeMeta","name":"string","depth":"unsigned int","size":"unsigned long","committedSize":"unsigned long","root":["array",["unsigned char",32]],"initialSize":"unsigned long","initialRoot":["array",["unsigned char",32]],"oldestHistoricBlock":"unsigned int","unfinalizedBlockHeight":"unsigned int","finalizedBlockHeight":"unsigned int"},"messageTreeMeta":"TreeMeta","archiveTreeMeta":"TreeMeta","publicDataTreeMeta":"TreeMeta","nullifierTreeMeta":"TreeMeta"}}}],["WsdbRollbackResponse",{"__typename":"WsdbRollbackResponse"}],["WsdbSyncBlockResponse",{"__typename":"WsdbSyncBlockResponse","status":"WorldStateStatusFull"}],["WsdbCreateForkResponse",{"__typename":"WsdbCreateForkResponse","forkId":"unsigned long"}],["WsdbDeleteForkResponse",{"__typename":"WsdbDeleteForkResponse"}],["WsdbFinalizeBlocksResponse",{"__typename":"WsdbFinalizeBlocksResponse","status":"WorldStateStatusSummary"}],["WsdbUnwindBlocksResponse",{"__typename":"WsdbUnwindBlocksResponse","status":"WorldStateStatusFull"}],["WsdbRemoveHistoricalBlocksResponse",{"__typename":"WsdbRemoveHistoricalBlocksResponse","status":"WorldStateStatusFull"}],["WsdbGetStatusResponse",{"__typename":"WsdbGetStatusResponse","status":"WorldStateStatusSummary"}],["WsdbCreateCheckpointResponse",{"__typename":"WsdbCreateCheckpointResponse"}],["WsdbCommitCheckpointResponse",{"__typename":"WsdbCommitCheckpointResponse"}],["WsdbRevertCheckpointResponse",{"__typename":"WsdbRevertCheckpointResponse"}],["WsdbCommitAllCheckpointsResponse",{"__typename":"WsdbCommitAllCheckpointsResponse"}],["WsdbRevertAllCheckpointsResponse",{"__typename":"WsdbRevertAllCheckpointsResponse"}],["WsdbCopyStoresResponse",{"__typename":"WsdbCopyStoresResponse"}],["WsdbShutdownResponse",{"__typename":"WsdbShutdownResponse"}]]]} - diff --git a/ipc-codegen/scripts/update_schemas.sh b/ipc-codegen/scripts/update_schemas.sh deleted file mode 100755 index 5e230356ce6e..000000000000 --- a/ipc-codegen/scripts/update_schemas.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# -# Update committed schema JSON files from C++ binaries. -# Run this after changing C++ command structs. -# -# Usage: ./scripts/update_schemas.sh -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CODEGEN_DIR="$(dirname "$SCRIPT_DIR")" -BB_BIN="${CODEGEN_DIR}/../cpp/build/bin" - -echo "Updating schemas from C++ binaries..." - -for service in bb:bb wsdb:aztec-wsdb cdb:aztec-cdb avm:aztec-avm; do - IFS=: read -r name binary <<< "$service" - bin_path="${BB_BIN}/${binary}" - - if [ ! -x "$bin_path" ]; then - echo " [skip] ${name}: binary not found at ${bin_path}" - echo " Build C++ first: cd barretenberg/cpp && cmake --preset default && cd build && ninja ${binary}" - continue - fi - - "$bin_path" msgpack schema 2>/dev/null > "${CODEGEN_DIR}/schemas/${name}_schema.json" - echo " [updated] ${name}_schema.json" -done - -# Curve constants: export as msgpack then convert to JSON -bb_path="${BB_BIN}/bb" -if [ -x "$bb_path" ]; then - tmpfile=$(mktemp) - "$bb_path" msgpack curve_constants 2>/dev/null > "$tmpfile" - # Convert msgpack to JSON (msgpackr from barretenberg/ts node_modules) - NODE_PATH="${CODEGEN_DIR}/../ts/node_modules" node -e " - const {unpack} = require('msgpackr'); - const fs = require('fs'); - const buf = fs.readFileSync('$tmpfile'); - const c = unpack(buf); - const toHex = (a) => Buffer.from(a).toString('hex'); - const cvt = (p) => Array.isArray(p.x) ? {x:p.x.map(toHex),y:p.y.map(toHex)} : {x:toHex(p.x),y:toHex(p.y)}; - const out = {}; - for (const [k,v] of Object.entries(c)) { - out[k] = k.endsWith('_modulus') ? toHex(v) : cvt(v); - } - fs.writeFileSync('${CODEGEN_DIR}/schemas/bb_curve_constants.json', JSON.stringify(out, null, 2)); - " - rm -f "$tmpfile" - echo " [updated] bb_curve_constants.json" -fi - -echo "" -echo "Done. Run 'npx tsx src/generate.ts' to regenerate bindings, then commit." diff --git a/ipc-codegen/scripts/validate_schemas.sh b/ipc-codegen/scripts/validate_schemas.sh deleted file mode 100755 index f9c49a5395a1..000000000000 --- a/ipc-codegen/scripts/validate_schemas.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# -# Validate committed schema JSON files match C++ binary output. -# Run in CI after C++ build to catch schema drift. -# -# Usage: ./scripts/validate_schemas.sh -# Exit 0: schemas match. Exit 1: schemas out of date. -# -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -CODEGEN_DIR="$(dirname "$SCRIPT_DIR")" -BB_BIN="${CODEGEN_DIR}/../cpp/build/bin" - -FAIL=0 - -for service in bb:bb wsdb:aztec-wsdb cdb:aztec-cdb avm:aztec-avm; do - IFS=: read -r name binary <<< "$service" - bin_path="${BB_BIN}/${binary}" - schema_path="${CODEGEN_DIR}/schemas/${name}_schema.json" - - if [ ! -x "$bin_path" ]; then - echo " [skip] ${name}: binary not found at ${bin_path}" - continue - fi - - if [ ! -f "$schema_path" ]; then - echo " [FAIL] ${name}: committed schema not found at ${schema_path}" - FAIL=1 - continue - fi - - # Export current schema from binary - current=$("$bin_path" msgpack schema 2>/dev/null) - - # Compare with committed - committed=$(cat "$schema_path") - - if [ "$current" = "$committed" ]; then - echo " [ok] ${name}_schema.json matches binary" - else - echo " [FAIL] ${name}_schema.json is out of date!" - echo " Run: cd ipc-codegen && ./scripts/update_schemas.sh" - FAIL=1 - fi -done - -if [ "$FAIL" -gt 0 ]; then - echo "" - echo "Schema validation failed. Committed schemas are out of sync with C++ code." - echo "Fix: cd ipc-codegen && ./scripts/update_schemas.sh && git add schemas/" - exit 1 -fi - -echo "" -echo "All schemas are up to date." diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts index 35b9bf6d8337..4f21192969da 100644 --- a/ipc-codegen/src/cpp_codegen.ts +++ b/ipc-codegen/src/cpp_codegen.ts @@ -302,7 +302,7 @@ ${methods} } /** Get the generated/ directory include prefix. - * Returns either the explicit --cpp-include-dir value (e.g. "barretenberg/wsdb/generated") + * Returns either the explicit --cpp-include-dir value (e.g. "myservice/generated") * or empty for callers that include generated files by their bare filename. */ private generatedDir(): string { if (this.opts.generatedIncludeDir) { @@ -411,7 +411,8 @@ ${methods} // Self-contained serialization macro. // Defines a msgpack() method that enumerates field name/value pairs. // Works with msgpack packers (serialization) and schema reflectors. -// If barretenberg's SERIALIZATION_FIELDS is already available, use it instead. +// Skipped if the consumer already defines SERIALIZATION_FIELDS (which then +// wins, so wire and domain types share the same enumeration semantics). // --------------------------------------------------------------------------- #ifndef SERIALIZATION_FIELDS #define _SF_E1(x) #x, x @@ -489,7 +490,7 @@ inline ${c.responseType} handle_${method}(const ${c.name}& /*cmd*/) { const shutdownResp = shutdownName + "Response"; return `// AUTOGENERATED FILE - DO NOT EDIT -// ${prefix} server dispatch — standalone, no barretenberg dependencies. +// ${prefix} server dispatch — only depends on msgpack-c. // Implement the handle_* functions to build your ${prefix} service. #pragma once @@ -575,7 +576,7 @@ ${stubs} .join("\n\n"); return `// AUTOGENERATED FILE - DO NOT EDIT -// ${prefix} typed IPC client — standalone, no barretenberg dependencies. +// ${prefix} typed IPC client — only depends on msgpack-c. #pragma once #include "types_gen.hpp" diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index fbcdb4546938..125f785c361b 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -139,9 +139,9 @@ Optional: --client Generate client --skeleton Generate handler stubs + main (one-time) --prefix Type prefix (auto-detected if omitted) - --cpp-namespace C++ namespace (e.g. bb::wsdb) + --cpp-namespace C++ namespace (e.g. my::ns) --cpp-wire-namespace Wire types sub-namespace (default: wire) - --cpp-include-dir Include path for generated dir (e.g. barretenberg/wsdb/generated) + --cpp-include-dir Include path for generated dir (e.g. myservice/generated) --curve-constants Generate TS curve constants --strip-method-prefix Strip prefix from TS method names (e.g. BbCircuitProve -> circuitProve)`); process.exit(1); @@ -456,10 +456,14 @@ function generate(args: Args) { gen.generateStandaloneTypes(compiled), ), ); + // Bundled cpp templates are copied verbatim. Push them through clang-format + // alongside the generated files so they pass downstream format-check + // (each consumer's local .clang-format gets picked up via the file path). if (args.server || args.client) { // Bundled msgpack adaptor — keeps the generated client/server free of // any framework-specific msgpack includes. copyTemplate("cpp", "msgpack_struct_map_impl.hpp", absOut); + cppFiles.push(join(absOut, "msgpack_struct_map_impl.hpp")); } if (args.server) { cppFiles.push( @@ -469,6 +473,7 @@ function generate(args: Args) { ), ); copyTemplate("cpp", "ipc_server.hpp", absOut); + cppFiles.push(join(absOut, "ipc_server.hpp")); } if (args.client) { cppFiles.push( @@ -484,6 +489,7 @@ function generate(args: Args) { ), ); copyTemplate("cpp", "ipc_client.hpp", absOut); + cppFiles.push(join(absOut, "ipc_client.hpp")); } // Skeleton (one-time handler stubs + main + build files) diff --git a/ipc-codegen/src/typescript_codegen.ts b/ipc-codegen/src/typescript_codegen.ts index dd9670cdbdf3..8fe9ddd29b1b 100644 --- a/ipc-codegen/src/typescript_codegen.ts +++ b/ipc-codegen/src/typescript_codegen.ts @@ -8,12 +8,18 @@ * - No complex abstraction */ -import type { CompiledSchema, Type, Struct, Field, Command } from './schema_visitor.ts'; -import { toPascalCase, toSnakeCase } from './naming.ts'; +import type { + CompiledSchema, + Type, + Struct, + Field, + Command, +} from "./schema_visitor.ts"; +import { toPascalCase, toSnakeCase } from "./naming.ts"; function toCamelCase(name: string): string { // If no underscores, assume already camelCase (e.g. forkId, classId) - if (!name.includes('_')) { + if (!name.includes("_")) { return name.charAt(0).toLowerCase() + name.slice(1); } const pascal = toPascalCase(name); @@ -21,9 +27,9 @@ function toCamelCase(name: string): string { } export class TypeScriptCodegen { - private errorTypeName: string = 'ErrorResponse'; + private errorTypeName: string = "ErrorResponse"; /** Prefix to strip from command names when generating method names (e.g. "Bb" -> BbCircuitProve becomes circuitProve) */ - private methodPrefix: string = ''; + private methodPrefix: string = ""; constructor(options?: { stripMethodPrefix?: string }) { if (options?.stripMethodPrefix) { @@ -43,94 +49,126 @@ export class TypeScriptCodegen { // Type mapping: Schema type -> TypeScript type private mapType(type: Type): string { switch (type.kind) { - case 'primitive': + case "primitive": switch (type.primitive) { - case 'bool': return 'boolean'; - case 'u8': return 'number'; - case 'u16': return 'number'; - case 'u32': return 'number'; - case 'u64': return 'number'; - case 'f64': return 'number'; - case 'string': return 'string'; - case 'bytes': return 'Uint8Array'; - case 'fr': return 'Fr'; // 32-byte field element - case 'field2': return '[Fr, Fr]'; // Extension field (Fq2) - case 'enum_u32': return 'number'; // C++ enum as integer - case 'map_u32_pair': return 'Record'; // map> + case "bool": + return "boolean"; + case "u8": + return "number"; + case "u16": + return "number"; + case "u32": + return "number"; + case "u64": + return "number"; + case "f64": + return "number"; + case "string": + return "string"; + case "bytes": + return "Uint8Array"; + case "fr": + return "Fr"; // 32-byte field element + case "field2": + return "[Fr, Fr]"; // Extension field (Fq2) + case "enum_u32": + return "number"; // C++ enum as integer + case "map_u32_pair": + return "Record"; // map> } break; - case 'vector': { + case "vector": { const inner = this.mapType(type.element!); // Wrap union types in parens to avoid precedence issues: (Foo | undefined)[] - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + return type.element!.kind === "optional" + ? `(${inner})[]` + : `${inner}[]`; } - case 'array': { + case "array": { const inner = this.mapType(type.element!); - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + return type.element!.kind === "optional" + ? `(${inner})[]` + : `${inner}[]`; } - case 'optional': + case "optional": return `${this.mapType(type.element!)} | null`; - case 'struct': + case "struct": return toPascalCase(type.struct!.name); } - return 'unknown'; + return "unknown"; } // Type mapping for msgpack interfaces (uses Msgpack* prefix for structs) private mapMsgpackType(type: Type): string { switch (type.kind) { - case 'primitive': + case "primitive": switch (type.primitive) { - case 'bool': return 'boolean'; - case 'u8': return 'number'; - case 'u16': return 'number'; - case 'u32': return 'number'; - case 'u64': return 'number'; - case 'f64': return 'number'; - case 'string': return 'string'; - case 'bytes': return 'Uint8Array'; - case 'fr': return 'Uint8Array'; // Fr on the wire is still 32 bytes - case 'field2': return '[Uint8Array, Uint8Array]'; - case 'enum_u32': return 'number'; - case 'map_u32_pair': return 'Record'; + case "bool": + return "boolean"; + case "u8": + return "number"; + case "u16": + return "number"; + case "u32": + return "number"; + case "u64": + return "number"; + case "f64": + return "number"; + case "string": + return "string"; + case "bytes": + return "Uint8Array"; + case "fr": + return "Uint8Array"; // Fr on the wire is still 32 bytes + case "field2": + return "[Uint8Array, Uint8Array]"; + case "enum_u32": + return "number"; + case "map_u32_pair": + return "Record"; } break; - case 'vector': { + case "vector": { const inner = this.mapMsgpackType(type.element!); - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + return type.element!.kind === "optional" + ? `(${inner})[]` + : `${inner}[]`; } - case 'array': { + case "array": { const inner = this.mapMsgpackType(type.element!); - return type.element!.kind === 'optional' ? `(${inner})[]` : `${inner}[]`; + return type.element!.kind === "optional" + ? `(${inner})[]` + : `${inner}[]`; } - case 'optional': + case "optional": return `${this.mapMsgpackType(type.element!)} | null`; - case 'struct': + case "struct": return `Msgpack${toPascalCase(type.struct!.name)}`; } - return 'unknown'; + return "unknown"; } // Check if type needs conversion (has nested structs) private needsConversion(type: Type): boolean { switch (type.kind) { - case 'primitive': + case "primitive": return false; - case 'vector': - case 'array': - case 'optional': + case "vector": + case "array": + case "optional": return this.needsConversion(type.element!); - case 'struct': + case "struct": return true; } return false; @@ -152,7 +190,7 @@ export class TypeScriptCodegen { // Generate public interface private generateInterface(struct: Struct): string { const tsName = toPascalCase(struct.name); - const fields = struct.fields.map(f => this.generateField(f)).join('\n'); + const fields = struct.fields.map((f) => this.generateField(f)).join("\n"); return `export interface ${tsName} { ${fields} @@ -162,7 +200,9 @@ ${fields} // Generate msgpack interface (internal) private generateMsgpackInterface(struct: Struct): string { const tsName = toPascalCase(struct.name); - const fields = struct.fields.map(f => this.generateMsgpackField(f)).join('\n'); + const fields = struct.fields + .map((f) => this.generateMsgpackField(f)) + .join("\n"); return `interface Msgpack${tsName} { ${fields} @@ -180,16 +220,19 @@ ${fields} } const checks = struct.fields - .map(f => ` if (o.${f.name} === undefined) { throw new Error("Expected ${f.name} in ${tsName} deserialization"); }`) - .join('\n'); + .map( + (f) => + ` if (o.${f.name} === undefined) { throw new Error("Expected ${f.name} in ${tsName} deserialization"); }`, + ) + .join("\n"); const conversions = struct.fields - .map(f => { + .map((f) => { const tsFieldName = toCamelCase(f.name); const converter = this.generateToConverter(f.type, `o.${f.name}`); return ` ${tsFieldName}: ${converter},`; }) - .join('\n'); + .join("\n"); return `function to${tsName}(o: Msgpack${tsName}): ${tsName} { ${checks}; @@ -210,19 +253,22 @@ ${conversions} } const checks = struct.fields - .map(f => { + .map((f) => { const tsFieldName = toCamelCase(f.name); return ` if (o.${tsFieldName} === undefined) { throw new Error("Expected ${tsFieldName} in ${tsName} serialization"); }`; }) - .join('\n'); + .join("\n"); const conversions = struct.fields - .map(f => { + .map((f) => { const tsFieldName = toCamelCase(f.name); - const converter = this.generateFromConverter(f.type, `o.${tsFieldName}`); + const converter = this.generateFromConverter( + f.type, + `o.${tsFieldName}`, + ); return ` ${f.name}: ${converter},`; }) - .join('\n'); + .join("\n"); return `function from${tsName}(o: ${tsName}): Msgpack${tsName} { ${checks}; @@ -239,18 +285,18 @@ ${conversions} } switch (type.kind) { - case 'vector': - case 'array': + case "vector": + case "array": if (this.needsConversion(type.element!)) { - return `${value}.map((v: any) => ${this.generateToConverter(type.element!, 'v')})`; + return `${value}.map((v: any) => ${this.generateToConverter(type.element!, "v")})`; } return value; - case 'optional': + case "optional": if (this.needsConversion(type.element!)) { return `${value} != null ? ${this.generateToConverter(type.element!, value)} : null`; } return value; - case 'struct': + case "struct": return `to${toPascalCase(type.struct!.name)}(${value})`; } return value; @@ -263,18 +309,18 @@ ${conversions} } switch (type.kind) { - case 'vector': - case 'array': + case "vector": + case "array": if (this.needsConversion(type.element!)) { - return `${value}.map((v: any) => ${this.generateFromConverter(type.element!, 'v')})`; + return `${value}.map((v: any) => ${this.generateFromConverter(type.element!, "v")})`; } return value; - case 'optional': + case "optional": if (this.needsConversion(type.element!)) { return `${value} != null ? ${this.generateFromConverter(type.element!, value)} : null`; } return value; - case 'struct': + case "struct": return `from${toPascalCase(type.struct!.name)}(${value})`; } return value; @@ -282,33 +328,41 @@ ${conversions} // Generate types file (api_types.ts) generateTypes(schema: CompiledSchema, schemaHash?: string): string { - const allStructs = [...schema.structs.values(), ...schema.responses.values()]; + const allStructs = [ + ...schema.structs.values(), + ...schema.responses.values(), + ]; // Public interfaces const publicInterfaces = allStructs - .map(s => this.generateInterface(s)) - .join('\n\n'); + .map((s) => this.generateInterface(s)) + .join("\n\n"); // Msgpack interfaces const msgpackInterfaces = allStructs - .map(s => this.generateMsgpackInterface(s)) - .join('\n\n'); + .map((s) => this.generateMsgpackInterface(s)) + .join("\n\n"); // Conversion functions const toFunctions = allStructs - .map(s => 'export ' + this.generateToFunction(s)) - .join('\n\n'); + .map((s) => "export " + this.generateToFunction(s)) + .join("\n\n"); const fromFunctions = allStructs - .map(s => 'export ' + this.generateFromFunction(s)) - .join('\n\n'); + .map((s) => "export " + this.generateFromFunction(s)) + .join("\n\n"); // BbApiBase interface const apiMethods = schema.commands - .map(c => ` ${this.toMethodName(c.name)}(command: ${toPascalCase(c.name)}): Promise<${toPascalCase(c.responseType)}>;`) - .join('\n'); + .map( + (c) => + ` ${this.toMethodName(c.name)}(command: ${toPascalCase(c.name)}): Promise<${toPascalCase(c.responseType)}>;`, + ) + .join("\n"); - const hashLine = schemaHash ? `\n/** Schema version hash for compatibility checking */\nexport const SCHEMA_HASH = '${schemaHash}';\n` : ''; + const hashLine = schemaHash + ? `\n/** Schema version hash for compatibility checking */\nexport const SCHEMA_HASH = '${schemaHash}';\n` + : ""; return `// AUTOGENERATED FILE - DO NOT EDIT ${hashLine} @@ -346,7 +400,7 @@ ${apiMethods} const msgpackCommand = from${cmdType}(command); return msgpackCall(this.backend, [["${command.name}", msgpackCommand]]).then(([variantName, result]: [string, any]) => { if (variantName === '${this.errorTypeName}') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); + throw new BBApiException(result.message || 'Unknown error from server'); } if (variantName !== '${command.responseType}') { throw new BBApiException(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); @@ -365,7 +419,7 @@ ${apiMethods} const msgpackCommand = from${cmdType}(command); const [variantName, result] = msgpackCall(this.backend, [["${command.name}", msgpackCommand]]); if (variantName === '${this.errorTypeName}') { - throw new BBApiException(result.message || 'Unknown error from barretenberg'); + throw new BBApiException(result.message || 'Unknown error from server'); } if (variantName !== '${command.responseType}') { throw new BBApiException(\`Expected variant name '${command.responseType}' but got '\${variantName}'\`); @@ -376,11 +430,11 @@ ${apiMethods} // Generate async API file generateAsyncApi(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + this.errorTypeName = schema.errorTypeName || "ErrorResponse"; const imports = this.generateApiImports(schema); const methods = schema.commands - .map(c => this.generateAsyncApiMethod(c)) - .join('\n\n'); + .map((c) => this.generateAsyncApiMethod(c)) + .join("\n\n"); return `// AUTOGENERATED FILE - DO NOT EDIT @@ -409,11 +463,11 @@ ${methods} // Generate sync API file generateSyncApi(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + this.errorTypeName = schema.errorTypeName || "ErrorResponse"; const imports = this.generateApiImports(schema); const methods = schema.commands - .map(c => this.generateSyncApiMethod(c)) - .join('\n\n'); + .map((c) => this.generateSyncApiMethod(c)) + .join("\n\n"); return `// AUTOGENERATED FILE - DO NOT EDIT @@ -455,10 +509,10 @@ ${methods} } // Add BbApiBase - types.add('BbApiBase'); + types.add("BbApiBase"); const sortedTypes = Array.from(types).sort(); - return `import { ${sortedTypes.join(', ')} } from './api_types.js';`; + return `import { ${sortedTypes.join(", ")} } from './api_types.js';`; } // ----------------------------------------------------------------------- @@ -467,24 +521,24 @@ ${methods} /** Generate a server handler interface and dispatch function */ generateServerApi(schema: CompiledSchema): string { - this.errorTypeName = schema.errorTypeName || 'ErrorResponse'; + this.errorTypeName = schema.errorTypeName || "ErrorResponse"; const errorType = toPascalCase(this.errorTypeName); // Generate handler interface const handlerMethods = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { const methodName = this.toMethodName(c.name); const cmdType = toPascalCase(c.name); const respType = toPascalCase(c.responseType); return ` ${methodName}(command: ${cmdType}): Promise<${respType}>;`; }) - .join('\n'); + .join("\n"); // Generate dispatch switch cases const dispatchCases = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { const methodName = this.toMethodName(c.name); const cmdType = toPascalCase(c.name); const respType = toPascalCase(c.responseType); @@ -494,12 +548,12 @@ ${methods} return ['${c.responseType}', from${respType}(result)]; }`; }) - .join('\n'); + .join("\n"); // Collect imports const importTypes = new Set(); for (const cmd of schema.commands) { - if (cmd.name.endsWith('Shutdown')) continue; + if (cmd.name.endsWith("Shutdown")) continue; const cmdType = toPascalCase(cmd.name); const respType = toPascalCase(cmd.responseType); importTypes.add(cmdType); @@ -512,7 +566,7 @@ ${methods} return `// AUTOGENERATED FILE - DO NOT EDIT // Server-side dispatch for IPC protocol -import { ${sortedImports.join(', ')} } from './api_types.js'; +import { ${sortedImports.join(", ")} } from './api_types.js'; /** Handler interface — implement this to serve commands. */ export interface Handler { @@ -548,28 +602,29 @@ ${dispatchCases} // Collect import types const importTypes = new Set(); for (const cmd of schema.commands) { - if (cmd.name.endsWith('Shutdown')) continue; + if (cmd.name.endsWith("Shutdown")) continue; importTypes.add(toPascalCase(cmd.name)); importTypes.add(toPascalCase(cmd.responseType)); } - importTypes.add('Handler'); + importTypes.add("Handler"); const sortedImports = Array.from(importTypes).sort(); const stubs = schema.commands - .filter(c => !c.name.endsWith('Shutdown')) - .map(c => { + .filter((c) => !c.name.endsWith("Shutdown")) + .map((c) => { const methodName = this.toMethodName(c.name); const cmdType = toPascalCase(c.name); const respType = toPascalCase(c.responseType); return ` async ${methodName}(command: ${cmdType}): Promise<${respType}> { throw new Error('not implemented: ${c.name}'); }`; - }).join('\n\n'); + }) + .join("\n\n"); return `// Handler stubs — implement your service logic here. // This file is generated ONCE. Edit freely — it will not be overwritten. -import { ${sortedImports.join(', ')} } from './generated/${serverModule}.js'; +import { ${sortedImports.join(", ")} } from './generated/${serverModule}.js'; /** Shared context for your service — add database connections, state, etc. */ export interface ${prefix}Context { @@ -612,24 +667,30 @@ serve(socketPath, (commandName: string, payload: any) => dispatch(handler, comma /** Generate package.json for a standalone service */ generateBuildFile(prefix: string): string { - const pkgName = toSnakeCase(prefix).replace(/_/g, '-'); - - return JSON.stringify({ - name: `${pkgName}-service`, - version: '0.1.0', - type: 'module', - scripts: { - build: 'tsc', - start: 'node --experimental-strip-types main.ts', - generate: 'bash generate.sh', - }, - dependencies: { - msgpackr: '^1.10.0', - }, - devDependencies: { - typescript: '^5.4.0', - }, - }, null, 2) + '\n'; + const pkgName = toSnakeCase(prefix).replace(/_/g, "-"); + + return ( + JSON.stringify( + { + name: `${pkgName}-service`, + version: "0.1.0", + type: "module", + scripts: { + build: "tsc", + start: "node --experimental-strip-types main.ts", + generate: "bash generate.sh", + }, + dependencies: { + msgpackr: "^1.10.0", + }, + devDependencies: { + typescript: "^5.4.0", + }, + }, + null, + 2, + ) + "\n" + ); } /** Generate .gitignore for the skeleton project */ diff --git a/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp b/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp index ba53ddff39ac..c770b835248c 100644 --- a/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp +++ b/ipc-codegen/templates/cpp/msgpack_struct_map_impl.hpp @@ -5,9 +5,7 @@ // so generated IPC clients/servers don't pull in any framework-specific // msgpack headers. // -// Lifted from barretenberg/cpp/src/barretenberg/serialize/msgpack_impl/ -// (struct_map_impl.hpp + concepts.hpp + drop_keys.hpp), trimmed to just -// what the generated dispatch needs. +// Self-contained struct-map msgpack adaptor — only depends on msgpack-c. #include #include From 5a693f547b1f0bcf19ee32bd5dad01d8affa1522 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 19 May 2026 18:13:32 +0000 Subject: [PATCH 11/11] refactor(ipc-codegen): make --curve-constants take an explicit JSON path Previously the curve constants JSON was looked up relative to the generator's source directory (../schemas/bb_curve_constants.json), which baked a path to a committed-in-ipc-codegen file into the tool. Move ownership of the file to the consumer (alongside its other committed schema), and pass the path on the command line: generate.ts ... --curve-constants /path/to/curve_constants.json The flag now requires a path argument instead of being a bare boolean. No callers within this PR use it; the bb.js consumer that does will update in PR2. --- ipc-codegen/src/generate.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index 125f785c361b..4d85eb2308ce 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -17,7 +17,7 @@ * --skeleton Generate handler stubs + main (one-time, not regenerated) * --cpp-namespace C++ namespace (e.g. bb::wsdb) * --cpp-wire-namespace Wire types sub-namespace (default: wire) - * --curve-constants Generate TS curve constants (bb-only special case) + * --curve-constants Generate TS curve constants from JSON at * * Zero npm dependencies — runs with Node.js 22+ via --experimental-strip-types. */ @@ -54,7 +54,7 @@ interface Args { cppIncludeDir: string; uds: boolean; ffi: boolean; - curveConstants: boolean; + curveConstants: string; stripMethodPrefix: boolean; } @@ -72,7 +72,7 @@ function parseArgs(argv: string[]): Args { cppIncludeDir: "", uds: false, ffi: false, - curveConstants: false, + curveConstants: "", stripMethodPrefix: false, }; @@ -115,7 +115,7 @@ function parseArgs(argv: string[]): Args { args.ffi = true; break; case "--curve-constants": - args.curveConstants = true; + args.curveConstants = argv[++i]; break; case "--strip-method-prefix": args.stripMethodPrefix = true; @@ -142,7 +142,7 @@ Optional: --cpp-namespace C++ namespace (e.g. my::ns) --cpp-wire-namespace Wire types sub-namespace (default: wire) --cpp-include-dir Include path for generated dir (e.g. myservice/generated) - --curve-constants Generate TS curve constants + --curve-constants Generate TS curve constants from JSON at --strip-method-prefix Strip prefix from TS method names (e.g. BbCircuitProve -> circuitProve)`); process.exit(1); } @@ -269,7 +269,7 @@ function generate(args: Args) { copyTemplate("ts", "ipc_client.ts", absOut); } if (args.curveConstants) { - generateCurveConstants(absOut); + generateCurveConstants(absOut, resolve(args.curveConstants)); } // Skeleton (one-time handler stubs + main + build files) if (args.skeleton) { @@ -563,8 +563,7 @@ function serializeCoordinate(coord: string | string[]): string { : hexToByteList(coord); } -function generateCurveConstants(outputDir: string) { - const constantsPath = join(__dirname, "../schemas/bb_curve_constants.json"); +function generateCurveConstants(outputDir: string, constantsPath: string) { const constants = JSON.parse(readFileSync(constantsPath, "utf-8")); const content = `// AUTOGENERATED FILE - DO NOT EDIT export const BN254_FR_MODULUS = ${hexToBigInt(constants.bn254_fr_modulus)}n;