From c27daaf41c60322fa55f83d530225f6c3abedc8b Mon Sep 17 00:00:00 2001 From: Aurelien Brabant Date: Sun, 9 Aug 2026 06:35:04 +0200 Subject: [PATCH 1/3] feat: add and migrate to figura beve generator --- cmake/Figura.cmake | 6 +- cmake/Glaze.cmake | 2 +- figura/manager-extension.fig | 4 +- figura/manager.fig | 4 +- figura/tsapi.fig | 2 +- src/file-indexer/CMakeLists.txt | 1 + src/lib/figura/src/codegen/beve-ts.hpp | 293 +++++++++++++++++ src/lib/figura/src/codegen/codegen.hpp | 54 ++++ src/lib/figura/src/codegen/glaze-qt.hpp | 137 ++++---- src/lib/figura/src/codegen/glaze.hpp | 139 ++++---- src/lib/figura/src/codegen/typescript.hpp | 70 +++- src/lib/figura/src/compile.hpp | 4 + src/lib/figura/src/parser.hpp | 2 + src/server/CMakeLists.txt | 6 +- .../extension/extension-command-runtime.hpp | 3 +- .../extension/manager/extension-manager.cpp | 5 +- .../src/extension/services/ui-service.hpp | 10 +- src/snippet/CMakeLists.txt | 1 + src/typescript/CMakeLists.txt | 8 +- src/typescript/api/src/api/proto/api.ts | 301 +++++++++++++++++- src/typescript/api/src/proto/ipc.ts | 20 +- src/typescript/api/tsconfig.json | 1 + src/typescript/cmake/Api.cmake | 2 +- src/typescript/extension-manager/src/index.ts | 22 +- .../src/loaders/load-view-command.tsx | 2 +- src/typescript/extension-manager/src/types.ts | 5 + .../extension-manager/src/worker.tsx | 9 +- .../extension-manager/tsconfig.json | 2 +- 28 files changed, 939 insertions(+), 176 deletions(-) create mode 100644 src/lib/figura/src/codegen/beve-ts.hpp diff --git a/cmake/Figura.cmake b/cmake/Figura.cmake index 10c6a0c4c8..9270214c92 100644 --- a/cmake/Figura.cmake +++ b/cmake/Figura.cmake @@ -9,7 +9,7 @@ function(figura_compile) cmake_parse_arguments( ARG "CLIENT;SERVER" - "LANG;PROTO;NAMESPACE;OUTPUT" + "LANG;PROTO;NAMESPACE;OUTPUT;WIRE" "" ${ARGN} ) @@ -30,6 +30,10 @@ function(figura_compile) list(APPEND CMD_ARGS --namespace ${ARG_NAMESPACE}) endif() + if (DEFINED ARG_WIRE) + list(APPEND CMD_ARGS --wire ${ARG_WIRE}) + endif() + add_custom_command( OUTPUT ${GENFILE} COMMAND ${CMD_ARGS} diff --git a/cmake/Glaze.cmake b/cmake/Glaze.cmake index 0c56f57fa9..e90d1eaad1 100644 --- a/cmake/Glaze.cmake +++ b/cmake/Glaze.cmake @@ -14,7 +14,7 @@ function(import_glaze) FetchContent_Declare( glaze GIT_REPOSITORY https://github.com/stephenberry/glaze.git - GIT_TAG v7.2.0 + GIT_TAG v8.0.0 GIT_SHALLOW TRUE EXCLUDE_FROM_ALL OVERRIDE_FIND_PACKAGE diff --git a/figura/manager-extension.fig b/figura/manager-extension.fig index 5aeda363b9..df6591e825 100644 --- a/figura/manager-extension.fig +++ b/figura/manager-extension.fig @@ -45,8 +45,8 @@ struct LaunchEventData { service Lifecycle { fn launch(data: LaunchEventData) => bool; fn shutdown() => bool; - fn send_message(msg: string) => bool; - event extension_message(msg: string); + fn send_message(msg: raw) => bool; + event extension_message(msg: raw); // worker can explicitly request to be unloaded. only applies to `no-view` commands which have a predictable termination point. event unload_requested(); diff --git a/figura/manager.fig b/figura/manager.fig index 5757e5c7e1..57ea89d7c4 100644 --- a/figura/manager.fig +++ b/figura/manager.fig @@ -60,8 +60,8 @@ service Manager { // extension loads too fast. fn ready(session_id: string) => bool; - fn messageExtension(session_id: string, payload: string) => bool; + fn messageExtension(session_id: string, payload: raw) => bool; - event extensionMessage(session_id: string, payload: string); + event extensionMessage(session_id: string, payload: raw); event extensionCrash(session_id: string, reason: string); } diff --git a/figura/tsapi.fig b/figura/tsapi.fig index d5be7c21b0..37cbc951b5 100644 --- a/figura/tsapi.fig +++ b/figura/tsapi.fig @@ -109,7 +109,7 @@ struct DesktopNotificationPayload { }; service UI { - fn render(json: string) => void; + fn render(json: raw) => void; fn showToast(id: string, title: string, message: string, style: ToastStyle) => void; fn updateToast(id: string, title: string) => void; fn hideToast(id: string) => void; diff --git a/src/file-indexer/CMakeLists.txt b/src/file-indexer/CMakeLists.txt index cf88a2a564..c0a0b751af 100644 --- a/src/file-indexer/CMakeLists.txt +++ b/src/file-indexer/CMakeLists.txt @@ -19,6 +19,7 @@ figura_compile( LANG glaze NAMESPACE file_indexer_gen OUTPUT file-indexer-server.hpp + WIRE beve ) diff --git a/src/lib/figura/src/codegen/beve-ts.hpp b/src/lib/figura/src/codegen/beve-ts.hpp new file mode 100644 index 0000000000..5d928a3b35 --- /dev/null +++ b/src/lib/figura/src/codegen/beve-ts.hpp @@ -0,0 +1,293 @@ +#pragma once +#include + +inline constexpr std::string_view FIGURA_BEVE_TS = R"ts( +export type WireData = Uint8Array; + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +class BeveWriter { + private buf = new Uint8Array(4096); + private view = new DataView(this.buf.buffer); + private len = 0; + + value(v: unknown): void { + if (v === null || v === undefined) { + this.u8(0x00); + return; + } + + switch (typeof v) { + case "boolean": + this.u8(v ? 0x18 : 0x08); + return; + case "number": + if (Number.isSafeInteger(v)) { + this.u8(0x69); + this.ensure(8); + this.view.setBigInt64(this.len, BigInt(v), true); + this.len += 8; + } else { + this.u8(0x61); + this.ensure(8); + this.view.setFloat64(this.len, v, true); + this.len += 8; + } + return; + case "string": + this.u8(0x02); + this.str(v); + return; + case "object": + break; + default: + this.u8(0x00); + return; + } + + if (v instanceof Uint8Array) { + this.u8(0x14); + this.size(v.length); + this.ensure(v.length); + this.buf.set(v, this.len); + this.len += v.length; + return; + } + + if (Array.isArray(v)) { + this.u8(0x05); + this.size(v.length); + for (const item of v) { + this.value(item === undefined || typeof item === "function" ? null : item); + } + return; + } + + const asJson = (v as { toJSON?: unknown }).toJSON; + if (typeof asJson === "function") { + this.value(asJson.call(v)); + return; + } + + const obj = v as Record; + const keys = Object.keys(obj).filter( + (k) => obj[k] !== undefined && typeof obj[k] !== "function", + ); + this.u8(0x03); + this.size(keys.length); + for (const key of keys) { + this.str(key); + this.value(obj[key]); + } + } + + finish(): Uint8Array { + return this.buf.slice(0, this.len); + } + + private ensure(extra: number) { + if (this.len + extra <= this.buf.length) return; + let capacity = this.buf.length * 2; + while (capacity < this.len + extra) capacity *= 2; + const next = new Uint8Array(capacity); + next.set(this.buf.subarray(0, this.len)); + this.buf = next; + this.view = new DataView(next.buffer); + } + + private u8(v: number) { + this.ensure(1); + this.buf[this.len++] = v; + } + + private size(v: number) { + if (v < 64) { + this.u8(v << 2); + } else if (v < 16384) { + this.ensure(2); + this.view.setUint16(this.len, (v << 2) | 1, true); + this.len += 2; + } else if (v < 1073741824) { + this.ensure(4); + this.view.setUint32(this.len, ((v << 2) | 2) >>> 0, true); + this.len += 4; + } else { + this.ensure(8); + this.view.setBigUint64(this.len, (BigInt(v) << 2n) | 3n, true); + this.len += 8; + } + } + + private str(s: string) { + const bytes = textEncoder.encode(s); + this.size(bytes.length); + this.ensure(bytes.length); + this.buf.set(bytes, this.len); + this.len += bytes.length; + } +} + +class BeveReader { + private view: DataView; + private pos = 0; + + constructor(private readonly buf: Uint8Array) { + this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + } + + value(): any { + const tag = this.buf[this.pos]; + + switch (tag & 7) { + case 0: { + this.pos++; + if (tag === 0x00) return null; + return (tag & 0x10) !== 0; + } + case 1: { + this.pos++; + return this.number(tag); + } + case 2: { + this.pos++; + return this.str(); + } + case 3: { + this.pos++; + if (((tag >> 3) & 3) !== 0) { + throw new Error("BEVE: only string-keyed objects are supported"); + } + const count = this.size(); + const obj: Record = {}; + for (let i = 0; i < count; i++) { + const key = this.str(); + const value = this.value(); + if (key === "__proto__") { + Object.defineProperty(obj, key, { + value, + enumerable: true, + configurable: true, + writable: true, + }); + } else { + obj[key] = value; + } + } + return obj; + } + case 4: { + this.pos++; + return this.typedArray(tag); + } + case 5: { + this.pos++; + const count = this.size(); + const arr = new Array(count); + for (let i = 0; i < count; i++) arr[i] = this.value(); + return arr; + } + default: + throw new Error(`BEVE: unsupported tag 0x${tag.toString(16)}`); + } + } + + private number(tag: number): number { + const kind = (tag >> 3) & 3; + const width = 1 << ((tag >> 5) & 7); + const at = this.pos; + this.pos += width; + + if (kind === 0) { + if (width === 4) return this.view.getFloat32(at, true); + if (width === 8) return this.view.getFloat64(at, true); + throw new Error(`BEVE: unsupported float width ${width}`); + } + + if (kind === 1) { + if (width === 1) return this.view.getInt8(at); + if (width === 2) return this.view.getInt16(at, true); + if (width === 4) return this.view.getInt32(at, true); + if (width === 8) return Number(this.view.getBigInt64(at, true)); + } else { + if (width === 1) return this.view.getUint8(at); + if (width === 2) return this.view.getUint16(at, true); + if (width === 4) return this.view.getUint32(at, true); + if (width === 8) return Number(this.view.getBigUint64(at, true)); + } + + throw new Error(`BEVE: unsupported integer width ${width}`); + } + + private typedArray(tag: number): any { + const kind = (tag >> 3) & 3; + const exponent = (tag >> 5) & 7; + const count = this.size(); + + if (kind === 3) { + if (exponent === 1) { + const arr = new Array(count); + for (let i = 0; i < count; i++) arr[i] = this.str(); + return arr; + } + const arr = new Array(count); + for (let i = 0; i < count; i++) { + const byte = this.buf[this.pos + (i >> 3)]; + arr[i] = (byte & (1 << (i & 7))) !== 0; + } + this.pos += (count + 7) >> 3; + return arr; + } + + const width = 1 << exponent; + + if (kind === 2 && width === 1) { + const bytes = this.buf.slice(this.pos, this.pos + count); + this.pos += count; + return bytes; + } + + const arr = new Array(count); + for (let i = 0; i < count; i++) { + const numberTag = (exponent << 5) | (kind << 3) | 1; + arr[i] = this.number(numberTag); + } + return arr; + } + + private size(): number { + const config = this.buf[this.pos] & 3; + if (config === 0) return this.buf[this.pos++] >> 2; + if (config === 1) { + const v = this.view.getUint16(this.pos, true) >> 2; + this.pos += 2; + return v; + } + if (config === 2) { + const v = this.view.getUint32(this.pos, true) >>> 2; + this.pos += 4; + return v; + } + const v = this.view.getBigUint64(this.pos, true) >> 2n; + this.pos += 8; + return Number(v); + } + + private str(): string { + const n = this.size(); + const s = textDecoder.decode(this.buf.subarray(this.pos, this.pos + n)); + this.pos += n; + return s; + } +} + +function encodeMessage(msg: JsonRpcMessage): WireData { + const writer = new BeveWriter(); + writer.value(msg); + return writer.finish(); +} + +function decodeMessage(data: WireData): JsonRpcMessage { + return new BeveReader(data).value() as JsonRpcMessage; +} +)ts"; diff --git a/src/lib/figura/src/codegen/codegen.hpp b/src/lib/figura/src/codegen/codegen.hpp index 39bfed8686..16288a527a 100644 --- a/src/lib/figura/src/codegen/codegen.hpp +++ b/src/lib/figura/src/codegen/codegen.hpp @@ -1,10 +1,18 @@ #pragma once #include "../parser.hpp" #include +#include +#include + +enum class WireFormat { + Json, + Beve, +}; struct CodegenOptions { std::filesystem::path file; std::optional generationNamespace; + WireFormat wire = WireFormat::Json; }; class AbstractCodeGenerator { @@ -14,3 +22,49 @@ class AbstractCodeGenerator { virtual std::string generateClient(const Tree &ast, const CodegenOptions &opts = {}) = 0; virtual std::string generateServer(const Tree &ast, const CodegenOptions &opts = {}) = 0; }; + +inline std::string glazeWirePrelude(WireFormat wire) { + if (wire == WireFormat::Beve) { + return "inline constexpr auto WIRE_FORMAT = glz::BEVE;\nusing raw_t = std::vector;\n"; + } + return "inline constexpr auto WIRE_FORMAT = glz::JSON;\nusing raw_t = glz::raw_json;\n"; +} + +inline std::string glazeBeveEnumSpecializations(std::string_view ns, std::span enums) { + std::ostringstream oss; + + for (const auto &e : enums) { + std::ostringstream names; + names << "static constexpr auto names = std::array{"; + for (std::size_t i = 0; i != e.values.size(); ++i) { + if (i > 0) names << ", "; + names << '"' << e.values[i] << '"'; + } + names << "};"; + + oss << "namespace glz {\n"; + oss << "template <> struct to {\n"; + oss << "\ttemplate \n"; + oss << "\tstatic void op(const " << ns << "::" << e.name + << " &value, is_context auto &&ctx, Args &&...args) {\n"; + oss << "\t\t" << names.str() << "\n"; + oss << "\t\tconst auto index = static_cast(value);\n"; + oss << "\t\tserialize::op(index < names.size() ? names[index] : names[0], ctx, args...);\n"; + oss << "\t}\n};\n"; + oss << "template <> struct from {\n"; + oss << "\ttemplate \n"; + oss << "\tstatic void op(" << ns << "::" << e.name << " &value, is_context auto &&ctx, Args &&...args) {\n"; + oss << "\t\t" << names.str() << "\n"; + oss << "\t\tstd::string s;\n"; + oss << "\t\tparse::op(s, ctx, args...);\n"; + oss << "\t\tif (static_cast(ctx.error)) return;\n"; + oss << "\t\tfor (std::size_t i = 0; i != names.size(); ++i) {\n"; + oss << "\t\t\tif (names[i] == s) { value = static_cast<" << ns << "::" << e.name << ">(i); return; }\n"; + oss << "\t\t}\n"; + oss << "\t\tctx.error = error_code::unexpected_enum;\n"; + oss << "\t}\n};\n"; + oss << "}\n"; + } + + return oss.str(); +} diff --git a/src/lib/figura/src/codegen/glaze-qt.hpp b/src/lib/figura/src/codegen/glaze-qt.hpp index 50c33da3b4..30b58ddd7c 100644 --- a/src/lib/figura/src/codegen/glaze-qt.hpp +++ b/src/lib/figura/src/codegen/glaze-qt.hpp @@ -12,17 +12,23 @@ #include constexpr const auto COMMON = R"( -struct JsonRpcRequest { +template struct JsonRpcRequestT { std::string jsonrpc; std::string method; int id; - glz::raw_json params; + P params; }; -struct JsonRpcNotification { +template struct JsonRpcNotificationT { std::string jsonrpc; std::string method; - glz::raw_json params; + P params; +}; + +template struct JsonRpcResponseT { + int id; + std::string jsonrpc; + R result; }; struct JsonRpcErrorResponse { @@ -32,15 +38,18 @@ struct JsonRpcErrorResponse { std::string error; }; -struct JsonRpcResponse { - int id; - std::string jsonrpc; - glz::raw_json result; -}; +inline constexpr glz::opts WIRE_OPTS{.format = WIRE_FORMAT}; +inline constexpr glz::opts WIRE_READ_OPTS{.format = WIRE_FORMAT, .error_on_unknown_keys = false}; -using RpcMessage = std::variant; -using IncomingJsonRpcMessage = std::variant; -using OutgoingJsonRpcMessage = std::variant; +template std::expected wireWrite(const T &value, std::string &buf) { + if (auto const err = glz::write(value, buf)) { return std::unexpected(glz::format_error(err)); } + return {}; +} + +template std::expected wireRead(T &value, std::string_view data) { + if (auto const err = glz::read(value, data)) { return std::unexpected(glz::format_error(err)); } + return {}; +} class AbstractTransport { public: @@ -74,8 +83,8 @@ struct RpcIncomingMessage { std::optional id; std::optional method; std::optional error; - glz::raw_json result; - glz::raw_json params; + glz::skip result{}; + glz::skip params{}; }; class RpcTransport { @@ -101,7 +110,7 @@ class RpcTransport { std::expected dispatchMessage(std::string_view data) { RpcIncomingMessage msg; - if (auto const error = glz::read(msg, data)) { return std::unexpected(glz::format_error(error)); } + if (auto const error = wireRead(msg, data); !error) { return error; } if (msg.id) { if (auto it = m_requestMap.find(*msg.id); it != m_requestMap.end()) { @@ -111,7 +120,7 @@ class RpcTransport { it->second.handler(std::unexpected(*msg.error)); } else { if (m_logger) m_logger->onResponse(it->second.method, true, latencyMs); - it->second.handler(std::move(msg.result.str)); + it->second.handler(data); } m_requestMap.erase(it); } @@ -119,7 +128,7 @@ class RpcTransport { if (m_logger) m_logger->onEvent(*msg.method); if (auto it = m_handlers.find(*msg.method); it != m_handlers.end()) { for (const auto &handler : it->second) { - handler(msg.params.str); + handler(data); } } } @@ -128,13 +137,9 @@ class RpcTransport { } template - QFuture> request(std::string_view method, const U ¶ms) { - if (auto const error = glz::write_json(params, m_buf)) { - return QtFuture::makeReadyValueFuture>(std::unexpected(glz::format_error(error))); - } - + QFuture> request(std::string_view method, U params) { int id = m_id++; - auto sendRes = sendMessage(JsonRpcRequest{.jsonrpc = "2.0", .method = std::string{method}, .id = id, .params = m_buf}); + auto sendRes = sendMessage(JsonRpcRequestT{.jsonrpc = "2.0", .method = std::string{method}, .id = id, .params = std::move(params)}); if (!sendRes) return QtFuture::makeReadyValueFuture>(std::unexpected(std::move(sendRes).error())); @@ -149,11 +154,11 @@ class RpcTransport { else promise->addResult(std::unexpected(data.error())); } else { auto value = data.and_then([](std::string_view data) -> Result { - T payload; - if (auto const error = glz::read_json(payload, data)) { - return std::unexpected(glz::format_error(error)); + JsonRpcResponseT payload{}; + if (auto const error = wireRead(payload, data); !error) { + return std::unexpected(std::move(error).error()); } - return payload; + return std::move(payload.result); }); promise->addResult(std::move(value)); @@ -174,11 +179,11 @@ class RpcTransport { else cb(std::unexpected(data.error())); } else { auto value = data.and_then([](std::string_view data) -> Result { - T payload; - if (auto const error = glz::read_json(payload, data)) { - return std::unexpected(glz::format_error(error)); + JsonRpcNotificationT payload{}; + if (auto const error = wireRead(payload, data); !error) { + return std::unexpected(std::move(error).error()); } - return payload; + return std::move(payload.params); }); cb(value); @@ -192,8 +197,9 @@ class RpcTransport { } } - std::expected sendMessage(const IncomingJsonRpcMessage &msg) { - if (auto const res = glz::write_json(msg, m_buf)) { return std::unexpected(glz::format_error(res)); } + template + std::expected sendMessage(const Msg &msg) { + if (auto const res = wireWrite(msg, m_buf); !res) { return res; } m_transport.send(m_buf); return {}; @@ -211,7 +217,12 @@ class RpcTransport { )"; constexpr const auto BASE = R"( - +struct RpcRequestHead { + std::string jsonrpc; + std::string method; + int id; + glz::skip params{}; +}; class RpcTransport { public: @@ -220,31 +231,21 @@ class RpcTransport { void bindReply(int id) { m_transport.bindReply(id); } template - void notify(std::string_view method, const T& params) { - std::string paramsBuf; - { - [[maybe_unused]] auto res = glz::write_json(params, paramsBuf); - } - - send(JsonRpcNotification{ + void notify(std::string_view method, T params) { + send(JsonRpcNotificationT{ .jsonrpc = "2.0", .method = std::string{method}, - .params = paramsBuf + .params = std::move(params) }); } template - void reply(int id, const T& result) { - std::string resultBuf; - { - [[maybe_unused]] auto res = glz::write_json(result, resultBuf); - } - + void reply(int id, T result) { m_transport.activateReply(id); - send(JsonRpcResponse{ + send(JsonRpcResponseT{ .id = id, .jsonrpc = "2.0", - .result = resultBuf + .result = std::move(result) }); } @@ -254,9 +255,10 @@ class RpcTransport { } private: - void send(const OutgoingJsonRpcMessage& msg) { + template + void send(const Msg& msg) { std::string buf; - [[maybe_unused]] auto res = glz::write_json(msg, buf); + [[maybe_unused]] auto res = wireWrite(msg, buf); m_transport.send(buf); } @@ -335,6 +337,8 @@ class GlazeQtGenerator : public AbstractCodeGenerator { return "std::string"; case PrimitiveType::Any: return "glz::generic"; + case PrimitiveType::Raw: + return "raw_t"; default: std::unreachable(); } @@ -530,7 +534,9 @@ class GlazeQtGenerator : public AbstractCodeGenerator { oss << R"( #pragma once +#include #include +#include #include #include #include @@ -539,6 +545,7 @@ class GlazeQtGenerator : public AbstractCodeGenerator { #include #include #include +#include )"; auto const ns = @@ -546,6 +553,7 @@ class GlazeQtGenerator : public AbstractCodeGenerator { oss << "namespace " << ns << " {\n"; + oss << glazeWirePrelude(opts.wire); oss << COMMON << clientCode; generateTypes(oss, ast); @@ -572,7 +580,9 @@ oss << serializeEventParams(s->name, e); for (const auto &m : s->events) { std::string methodId = std::format("{}/{}", s->name, m.name); oss << "transport.subscribe<" << getMethodParamName(s->name, m.name) << ">(" << std::quoted(methodId) - << ", " << "[this](const auto& payload){ if (!payload) return; emit " << m.name << "("; + << ", " + << "[this](const auto& payload){ if (!payload) { qWarning() << \"figura: failed to decode " + << methodId << " event:\" << payload.error().c_str(); return; } emit " << m.name << "("; for (const auto &[idx, param] : m.params | vicinae::enumerate) { if (idx > 0) { oss << ", "; } @@ -657,6 +667,7 @@ oss << serializeEventParams(s->name, e); oss << "\n}"; // end namespace oss << serializeEnumGlazeMetas(ns, ast.enums); + if (opts.wire == WireFormat::Beve) { oss << glazeBeveEnumSpecializations(ns, ast.enums); } return oss.str(); } @@ -666,7 +677,9 @@ oss << serializeEventParams(s->name, e); oss << R"( #pragma once +#include #include +#include #include #include #include @@ -676,12 +689,14 @@ oss << serializeEventParams(s->name, e); #include #include #include +#include )"; auto const ns = opts.generationNamespace.value_or(std::string{stripExtension(opts.file.filename().string())}); oss << "namespace " << ns << " {\n"; + oss << glazeWirePrelude(opts.wire); oss << COMMON << BASE; generateTypes(oss, ast); @@ -724,10 +739,10 @@ oss << serializeEventParams(s->name, e); } void route(std::string_view data) { - JsonRpcRequest msg; - if (auto const error = glz::read(msg, data)) { return; } + RpcRequestHead msg; + if (auto const error = wireRead(msg, data); !error) { return; } if (msg.method.empty()) return; - dispatch(msg); + dispatch(msg, data); } )"; @@ -750,7 +765,7 @@ oss << serializeEventParams(s->name, e); } if (m_logger) m_logger->onResponse(method, true, latencyMs); if constexpr (std::is_void_v) { - m_transport.reply(id, nullptr); + m_transport.reply(id, glz::generic{}); } else { m_transport.reply(id, *result); } @@ -779,7 +794,7 @@ oss << serializeEventParams(s->name, e); } )"; - oss << "\tvoid dispatch(const JsonRpcRequest& req) {\n"; + oss << "\tvoid dispatch(const RpcRequestHead& req, std::string_view data) {\n"; oss << "\t\tauto sentAt = Clock::now();\n"; oss << "\t\tif (m_logger) m_logger->onRequest(req.method);\n"; @@ -791,8 +806,9 @@ oss << serializeEventParams(s->name, e); oss << "\t\t" << (firstMethod ? "if" : "} else if") << " (req.method == " << std::quoted(methodId) << ") {\n"; firstMethod = false; - oss << "\t\t\t" << getMethodParamName(s->name, m.name) << " payload;\n"; - oss << "\t\t\t[[maybe_unused]] auto res = glz::read_json(payload, req.params.str);\n"; + oss << "\t\t\tJsonRpcRequestT<" << getMethodParamName(s->name, m.name) << "> env{};\n"; + oss << "\t\t\t[[maybe_unused]] auto res = wireRead(env, data);\n"; + if (!m.params.empty()) { oss << "\t\t\tauto& payload = env.params;\n"; } oss << "\t\t\t" << "handleResult(req.id, req.method, sentAt, " << "m_" << s->name << "->" << m.name << "("; for (const auto &[idx, param] : m.params | vicinae::enumerate) { @@ -821,6 +837,7 @@ oss << serializeEventParams(s->name, e); oss << "\n}"; // end namespace oss << serializeEnumGlazeMetas(ns, ast.enums); + if (opts.wire == WireFormat::Beve) { oss << glazeBeveEnumSpecializations(ns, ast.enums); } return oss.str(); } diff --git a/src/lib/figura/src/codegen/glaze.hpp b/src/lib/figura/src/codegen/glaze.hpp index 0d63f0df3b..20b5fe3009 100644 --- a/src/lib/figura/src/codegen/glaze.hpp +++ b/src/lib/figura/src/codegen/glaze.hpp @@ -8,17 +8,23 @@ #include constexpr const auto GLAZE_COMMON = R"( -struct JsonRpcRequest { +template struct JsonRpcRequestT { std::string jsonrpc; std::string method; int id; - glz::raw_json params; + P params; }; -struct JsonRpcNotification { +template struct JsonRpcNotificationT { std::string jsonrpc; std::string method; - glz::raw_json params; + P params; +}; + +template struct JsonRpcResponseT { + int id; + std::string jsonrpc; + R result; }; struct JsonRpcErrorResponse { @@ -28,15 +34,18 @@ struct JsonRpcErrorResponse { std::string error; }; -struct JsonRpcResponse { - int id; - std::string jsonrpc; - glz::raw_json result; -}; +inline constexpr glz::opts WIRE_OPTS{.format = WIRE_FORMAT}; +inline constexpr glz::opts WIRE_READ_OPTS{.format = WIRE_FORMAT, .error_on_unknown_keys = false}; -using RpcMessage = std::variant; -using IncomingJsonRpcMessage = std::variant; -using OutgoingJsonRpcMessage = std::variant; +template std::expected wireWrite(const T &value, std::string &buf) { + if (auto const err = glz::write(value, buf)) { return std::unexpected(glz::format_error(err)); } + return {}; +} + +template std::expected wireRead(T &value, std::string_view data) { + if (auto const err = glz::read(value, data)) { return std::unexpected(glz::format_error(err)); } + return {}; +} class AbstractTransport { public: @@ -50,8 +59,8 @@ struct RpcIncomingMessage { std::optional id; std::optional method; std::optional error; - glz::raw_json result; - glz::raw_json params; + glz::skip result{}; + glz::skip params{}; }; class RpcTransport { @@ -64,21 +73,21 @@ class RpcTransport { std::expected dispatchMessage(std::string_view data) { RpcIncomingMessage msg; - if (auto const error = glz::read(msg, data)) { return std::unexpected(glz::format_error(error)); } + if (auto const error = wireRead(msg, data); !error) { return error; } if (msg.id) { if (auto it = m_requestMap.find(*msg.id); it != m_requestMap.end()) { if (msg.error) { it->second(std::unexpected(*msg.error)); } else { - it->second(std::move(msg.result.str)); + it->second(data); } m_requestMap.erase(it); } } else if (msg.method) { if (auto it = m_handlers.find(*msg.method); it != m_handlers.end()) { for (const auto &handler : it->second) { - handler(msg.params.str); + handler(data); } } } @@ -87,14 +96,9 @@ class RpcTransport { } template - void request(std::string_view method, const U ¶ms, std::function)> cb) { - if (auto const error = glz::write_json(params, m_buf)) { - cb(std::unexpected(glz::format_error(error))); - return; - } - + void request(std::string_view method, U params, std::function)> cb) { int id = m_id++; - auto sendRes = sendMessage(JsonRpcRequest{.jsonrpc = "2.0", .method = std::string{method}, .id = id, .params = m_buf}); + auto sendRes = sendMessage(JsonRpcRequestT{.jsonrpc = "2.0", .method = std::string{method}, .id = id, .params = std::move(params)}); if (!sendRes) { cb(std::unexpected(std::move(sendRes).error())); @@ -107,11 +111,11 @@ class RpcTransport { else cb(std::unexpected(data.error())); } else { auto value = data.and_then([](std::string_view data) -> Result { - T payload; - if (auto const error = glz::read_json(payload, data)) { - return std::unexpected(glz::format_error(error)); + JsonRpcResponseT payload{}; + if (auto const error = wireRead(payload, data); !error) { + return std::unexpected(std::move(error).error()); } - return payload; + return std::move(payload.result); }); cb(std::move(value)); @@ -123,11 +127,11 @@ class RpcTransport { void subscribe(std::string_view method, std::function &result)> cb) { auto handler = [cb = std::move(cb)](Result data) { auto value = data.and_then([](std::string_view data) -> Result { - T payload; - if (auto const error = glz::read_json(payload, data)) { - return std::unexpected(glz::format_error(error)); + JsonRpcNotificationT payload{}; + if (auto const error = wireRead(payload, data); !error) { + return std::unexpected(std::move(error).error()); } - return payload; + return std::move(payload.params); }); cb(value); @@ -140,8 +144,9 @@ class RpcTransport { } } - std::expected sendMessage(const IncomingJsonRpcMessage &msg) { - if (auto const res = glz::write_json(msg, m_buf)) { return std::unexpected(glz::format_error(res)); } + template + std::expected sendMessage(const Msg &msg) { + if (auto const res = wireWrite(msg, m_buf); !res) { return res; } m_transport.send(m_buf); return {}; @@ -158,36 +163,32 @@ class RpcTransport { )"; constexpr const auto GLAZE_SERVER_BASE = R"( +struct RpcRequestHead { + std::string jsonrpc; + std::string method; + int id; + glz::skip params{}; +}; class RpcTransport { public: RpcTransport(AbstractTransport& transport): m_transport(transport) {} template - void notify(std::string_view method, const T& params) { - std::string paramsBuf; - { - [[maybe_unused]] auto res = glz::write_json(params, paramsBuf); - } - - send(JsonRpcNotification{ + void notify(std::string_view method, T params) { + send(JsonRpcNotificationT{ .jsonrpc = "2.0", .method = std::string{method}, - .params = paramsBuf + .params = std::move(params) }); } template - void reply(int id, const T& result) { - std::string resultBuf; - { - [[maybe_unused]] auto res = glz::write_json(result, resultBuf); - } - - send(JsonRpcResponse{ + void reply(int id, T result) { + send(JsonRpcResponseT{ .id = id, .jsonrpc = "2.0", - .result = resultBuf + .result = std::move(result) }); } @@ -196,9 +197,10 @@ class RpcTransport { } private: - void send(const OutgoingJsonRpcMessage& msg) { + template + void send(const Msg& msg) { std::string buf; - [[maybe_unused]] auto res = glz::write_json(msg, buf); + [[maybe_unused]] auto res = wireWrite(msg, buf); m_transport.send(buf); } @@ -242,6 +244,8 @@ class GlazeGenerator : public AbstractCodeGenerator { return "std::string"; case PrimitiveType::Any: return "glz::generic"; + case PrimitiveType::Raw: + return "raw_t"; default: std::unreachable(); } @@ -434,9 +438,13 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << R"( #pragma once +#include +#include #include #include #include +#include +#include #include #include #include @@ -449,6 +457,7 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "namespace " << ns << " {\n"; + oss << glazeWirePrelude(opts.wire); oss << GLAZE_COMMON << GLAZE_CLIENT_CODE; generateTypes(oss, ast); @@ -461,7 +470,8 @@ class GlazeGenerator : public AbstractCodeGenerator { for (const auto &m : s->events) { std::string methodId = std::format("{}/{}", s->name, m.name); oss << "transport.subscribe<" << getMethodParamName(s->name, m.name) << ">(" << std::quoted(methodId) - << ", " << "[this](const auto& payload){ if (!payload) return;"; + << ", " << "[this](const auto& payload){ if (!payload) { std::cerr << \"figura: failed to decode " + << methodId << " event: \" << payload.error() << std::endl; return; }"; oss << " if (m_" << m.name << "Handler) m_" << m.name << "Handler("; for (const auto &[idx, param] : m.params | vicinae::enumerate) { @@ -560,6 +570,7 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "\n}"; // end namespace oss << serializeEnumGlazeMetas(ns, ast.enums); + if (opts.wire == WireFormat::Beve) { oss << glazeBeveEnumSpecializations(ns, ast.enums); } return oss.str(); } @@ -569,9 +580,12 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << R"( #pragma once +#include +#include #include #include #include +#include #include #include #include @@ -582,6 +596,7 @@ class GlazeGenerator : public AbstractCodeGenerator { opts.generationNamespace.value_or(std::string{stripExtension(opts.file.filename().string())}); oss << "namespace " << ns << " {\n"; + oss << glazeWirePrelude(opts.wire); oss << GLAZE_COMMON << GLAZE_SERVER_BASE; generateTypes(oss, ast); @@ -608,10 +623,10 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << R"( void route(std::string_view data) { - JsonRpcRequest msg; - if (auto const error = glz::read(msg, data)) { return; } + RpcRequestHead msg; + if (auto const error = wireRead(msg, data); !error) { return; } if (msg.method.empty()) return; - dispatch(msg); + dispatch(msg, data); } )"; @@ -622,7 +637,7 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "private:\n"; - oss << "\tvoid dispatch(const JsonRpcRequest& req) {\n"; + oss << "\tvoid dispatch(const RpcRequestHead& req, std::string_view data) {\n"; bool firstMethod = true; for (const auto &s : ast.services) { @@ -632,8 +647,9 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "\t\t" << (firstMethod ? "if" : "} else if") << " (req.method == " << std::quoted(methodId) << ") {\n"; firstMethod = false; - oss << "\t\t\t" << getMethodParamName(s->name, m.name) << " payload;\n"; - oss << "\t\t\t[[maybe_unused]] auto res = glz::read_json(payload, req.params.str);\n"; + oss << "\t\t\tJsonRpcRequestT<" << getMethodParamName(s->name, m.name) << "> env{};\n"; + oss << "\t\t\t[[maybe_unused]] auto res = wireRead(env, data);\n"; + if (!m.params.empty()) { oss << "\t\t\tauto& payload = env.params;\n"; } if (m.isAsync) { oss << "\t\t\tm_" << s->name << "." << m.name << "("; for (const auto ¶m : m.params) { @@ -645,7 +661,7 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "\t\t\t\t\tm_transport.replyError(id, result.error());\n"; if (isVoid(m.returnType)) { oss << "\t\t\t\t} else {\n"; - oss << "\t\t\t\t\tm_transport.reply(id, nullptr);\n"; + oss << "\t\t\t\t\tm_transport.reply(id, glz::generic{});\n"; } else { oss << "\t\t\t\t} else {\n"; oss << "\t\t\t\t\tm_transport.reply(id, *result);\n"; @@ -664,7 +680,7 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "\t\t\t\tm_transport.replyError(req.id, result.error());\n"; if (isVoid(m.returnType)) { oss << "\t\t\t} else {\n"; - oss << "\t\t\t\tm_transport.reply(req.id, nullptr);\n"; + oss << "\t\t\t\tm_transport.reply(req.id, glz::generic{});\n"; } else { oss << "\t\t\t} else {\n"; oss << "\t\t\t\tm_transport.reply(req.id, *result);\n"; @@ -688,6 +704,7 @@ class GlazeGenerator : public AbstractCodeGenerator { oss << "\n}"; // end namespace oss << serializeEnumGlazeMetas(ns, ast.enums); + if (opts.wire == WireFormat::Beve) { oss << glazeBeveEnumSpecializations(ns, ast.enums); } return oss.str(); } diff --git a/src/lib/figura/src/codegen/typescript.hpp b/src/lib/figura/src/codegen/typescript.hpp index ec59de7e86..179b2742c1 100644 --- a/src/lib/figura/src/codegen/typescript.hpp +++ b/src/lib/figura/src/codegen/typescript.hpp @@ -1,4 +1,5 @@ #include "../parser.hpp" +#include "beve-ts.hpp" #include #include #include "codegen.hpp" @@ -25,6 +26,8 @@ inline std::string_view getTypename(const TypeValue &type) { return "string"; case PrimitiveType::Any: return "any"; + case PrimitiveType::Raw: + return "Uint8Array"; } } @@ -159,10 +162,24 @@ interface JsonRpcMessage { } interface ITransport { - send(data: string): void; + send(data: WireData): void; } )"; +static constexpr const auto jsonWireCode = R"( +export type WireData = string; + +function encodeMessage(msg: JsonRpcMessage): WireData { + return JSON.stringify(msg); +} + +function decodeMessage(data: WireData): JsonRpcMessage { + return JSON.parse(data) as JsonRpcMessage; +} +)"; + +static constexpr auto beveWireCode = FIGURA_BEVE_TS; + static constexpr const auto serverBoilerplate = R"( export class RpcTransport { constructor(private readonly transport: ITransport) { } @@ -176,7 +193,7 @@ export class RpcTransport { } private sendMessage(msg: JsonRpcMessage) { - this.transport.send(JSON.stringify(msg)); + this.transport.send(encodeMessage(msg)); } }; @@ -190,8 +207,8 @@ type EventSubscription = { export class RpcTransport { constructor(private readonly transport: ITransport) { } - dispatchMessage(data: string) { - const msg = JSON.parse(data) as JsonRpcMessage; + dispatchMessage(data: WireData) { + const msg = decodeMessage(data); if (msg.id !== undefined) { const handler = this.requestMap.get(msg.id); @@ -238,7 +255,7 @@ export class RpcTransport { } private sendMessage(msg: JsonRpcMessage) { - this.transport.send(JSON.stringify(msg)); + this.transport.send(encodeMessage(msg)); } @@ -259,6 +276,35 @@ static std::string tab(int n) { class TypeScriptCodeGenerator : public AbstractCodeGenerator { std::string name() const override { return "typescript"; } + static bool isRaw(const TypeValue &value) { + auto ptr = std::get_if(&value.data); + return ptr && *ptr == PrimitiveType::Raw; + } + + static void validateWire(const Tree &tree, const CodegenOptions &opts) { + if (opts.wire == WireFormat::Beve) return; + + auto check = [](const TypeValue &value) { + if (isRaw(value)) { throw std::runtime_error("the raw type requires --wire beve"); } + }; + + for (const auto &s : tree.structs) { + for (const auto &f : s->fields) + check(f.type); + } + for (const auto &s : tree.services) { + for (const auto &m : s->methods) { + check(m.returnType); + for (const auto &p : m.params) + check(p.type); + } + for (const auto &e : s->events) { + for (const auto &p : e.params) + check(p.type); + } + } + } + static bool isVoid(const TypeValue &value) { auto const visitor = overloads{[](PrimitiveType type) { return type == PrimitiveType::Void; }, [](auto &&other) { return false; }}; @@ -279,7 +325,9 @@ class TypeScriptCodeGenerator : public AbstractCodeGenerator { std::string generateClient(const Tree &tree, const CodegenOptions &opts) override { std::ostringstream oss; - oss << commonCode << busCode << "\n"; + validateWire(tree, opts); + + oss << commonCode << (opts.wire == WireFormat::Beve ? beveWireCode : jsonWireCode) << busCode << "\n"; generateTypes(oss, tree); @@ -297,7 +345,7 @@ class TypeScriptCodeGenerator : public AbstractCodeGenerator { oss << "\t}\n"; oss << R"( - route(msg: string): void { this.transport.dispatchMessage(msg); } + route(msg: WireData): void { this.transport.dispatchMessage(msg); } )"; for (auto const &s : tree.services) { @@ -345,7 +393,9 @@ class TypeScriptCodeGenerator : public AbstractCodeGenerator { std::string generateServer(const Tree &ast, const CodegenOptions &opts) override { std::ostringstream oss; - oss << commonCode << serverBoilerplate; + validateWire(ast, opts); + + oss << commonCode << (opts.wire == WireFormat::Beve ? beveWireCode : jsonWireCode) << serverBoilerplate; generateTypes(oss, ast); @@ -393,8 +443,8 @@ class TypeScriptCodeGenerator : public AbstractCodeGenerator { oss << "}\n\n"; oss << R"( - route(raw: string) { - const msg = JSON.parse(raw) as JsonRpcMessage; + route(raw: WireData) { + const msg = decodeMessage(raw); // request if (msg.id !== undefined && msg.method && msg.params) { diff --git a/src/lib/figura/src/compile.hpp b/src/lib/figura/src/compile.hpp index 6369784b42..72cc76bb89 100644 --- a/src/lib/figura/src/compile.hpp +++ b/src/lib/figura/src/compile.hpp @@ -17,6 +17,7 @@ class CompileCommand : public AbstractCommandLineCommand { app->add_option("--client", m_clients, ""); app->add_option("--server", m_servers, ""); app->add_option("--namespace", m_opts.generationNamespace); + app->add_option("--wire", m_wire, "")->check(CLI::IsMember({"json", "beve"})); } bool run(CLI::App *app) override { @@ -40,6 +41,8 @@ class CompileCommand : public AbstractCommandLineCommand { auto tree = std::move(result).value(); + m_opts.wire = m_wire == "beve" ? WireFormat::Beve : WireFormat::Json; + std::vector> generators; generators.emplace_back(std::make_unique()); generators.emplace_back(std::make_unique()); @@ -76,6 +79,7 @@ class CompileCommand : public AbstractCommandLineCommand { private: std::string m_proto; std::string m_out; + std::string m_wire = "json"; CodegenOptions m_opts; std::vector m_clients; std::vector m_servers; diff --git a/src/lib/figura/src/parser.hpp b/src/lib/figura/src/parser.hpp index 2915f72e57..be79b301d4 100644 --- a/src/lib/figura/src/parser.hpp +++ b/src/lib/figura/src/parser.hpp @@ -24,6 +24,7 @@ enum class PrimitiveType { UInt, Double, Any, + Raw, }; struct EnumValue { @@ -148,6 +149,7 @@ class Parser { if (name == "boolean" || name == "bool") return TypeValue{PrimitiveType::Boolean}; if (name == "void") return TypeValue{PrimitiveType::Void}; if (name == "any") return TypeValue{PrimitiveType::Any}; + if (name == "raw") return TypeValue{PrimitiveType::Raw}; for (const auto &s : m_tree.structs) { if (s->name == name) { return TypeValue{s.get()}; } diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index b50efdd38b..3122f9d97d 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -895,6 +895,7 @@ figura_compile( LANG glaze-qt NAMESPACE tsapi OUTPUT tsapi.hpp + WIRE beve ) figura_compile( @@ -903,6 +904,7 @@ figura_compile( LANG glaze-qt NAMESPACE manager OUTPUT manager.hpp + WIRE beve ) figura_compile( @@ -919,6 +921,7 @@ figura_compile( LANG glaze-qt NAMESPACE snippet_gen OUTPUT snippet-client.hpp + WIRE beve ) if (UNIX AND NOT APPLE) @@ -928,7 +931,8 @@ if (UNIX AND NOT APPLE) LANG glaze-qt NAMESPACE file_indexer_gen OUTPUT file-indexer-client.hpp - ) + WIRE beve +) endif() diff --git a/src/server/src/extension/extension-command-runtime.hpp b/src/server/src/extension/extension-command-runtime.hpp index 8b714076f3..83a0254004 100644 --- a/src/server/src/extension/extension-command-runtime.hpp +++ b/src/server/src/extension/extension-command-runtime.hpp @@ -11,7 +11,8 @@ class ExtensionManagerBus : public tsapi::AbstractTransport { ExtensionManagerBus(ExtensionManager &manager) : m_manager(manager) {} void send(std::string_view data) override { - m_manager.client().manager()->messageExtension(m_sessionId, std::string{data}); + auto const *bytes = reinterpret_cast(data.data()); + m_manager.client().manager()->messageExtension(m_sessionId, manager::raw_t{bytes, bytes + data.size()}); } void setSessionId(std::string str) { m_sessionId = std::move(str); } diff --git a/src/server/src/extension/manager/extension-manager.cpp b/src/server/src/extension/manager/extension-manager.cpp index 54c5ea2910..c1f2bd77f4 100644 --- a/src/server/src/extension/manager/extension-manager.cpp +++ b/src/server/src/extension/manager/extension-manager.cpp @@ -69,7 +69,10 @@ ExtensionManager::ExtensionManager() : m_bus(&m_process), m_rpc(m_bus), m_client connect(&m_process, &QProcess::finished, this, &ExtensionManager::finished); connect(&m_process, &QProcess::started, this, &ExtensionManager::processStarted); connect(m_client.manager(), &manager::ManagerService::extensionMessage, this, - &ExtensionManager::extensionMessageReceived); + [this](const std::string &sessionId, const manager::raw_t &payload) { + std::string_view view{reinterpret_cast(payload.data()), payload.size()}; + emit extensionMessageReceived(sessionId, view); + }); connect(m_client.manager(), &manager::ManagerService::extensionCrash, this, &ExtensionManager::extensionCrashed); diff --git a/src/server/src/extension/services/ui-service.hpp b/src/server/src/extension/services/ui-service.hpp index c4f01d77fe..9cfe640299 100644 --- a/src/server/src/extension/services/ui-service.hpp +++ b/src/server/src/extension/services/ui-service.hpp @@ -42,7 +42,7 @@ class ExtUIService : public tsapi::AbstractUI { m_command->setSubtitleOverride(subtitle); } - Void::Future render(std::string json) override { + Void::Future render(tsapi::raw_t json) override { m_renderQueue.push(std::move(json)); processNextRender(); return Void::ok(); @@ -209,8 +209,10 @@ private slots: auto json = std::move(m_renderQueue.front()); m_renderQueue.pop(); - m_modelWatcher.setFuture(QtConcurrent::run( - [json = std::move(json)]() -> ParsedRenderData { return parseRenderPayload(json); })); + m_modelWatcher.setFuture(QtConcurrent::run([json = std::move(json)]() -> ParsedRenderData { + std::string_view view{reinterpret_cast(json.data()), json.size()}; + return parseRenderPayload(view); + })); } ExtensionActionPanelBuilder::NotifyFn makeNotifyFn() { @@ -261,7 +263,7 @@ private slots: } std::vector m_views; - std::queue m_renderQueue; + std::queue m_renderQueue; QFutureWatcher m_modelWatcher; NavigationController *m_navigation; std::shared_ptr m_command; diff --git a/src/snippet/CMakeLists.txt b/src/snippet/CMakeLists.txt index 40b651e0ba..beed8ee1d9 100644 --- a/src/snippet/CMakeLists.txt +++ b/src/snippet/CMakeLists.txt @@ -19,6 +19,7 @@ figura_compile( LANG glaze NAMESPACE snippet_gen OUTPUT snippet-server.hpp + WIRE beve ) add_library(${PROJECT_NAME} STATIC src/server.cpp ${SRCS}) diff --git a/src/typescript/CMakeLists.txt b/src/typescript/CMakeLists.txt index a7f3043099..1f7f2839f0 100644 --- a/src/typescript/CMakeLists.txt +++ b/src/typescript/CMakeLists.txt @@ -49,10 +49,10 @@ set(EXT_MGR_PROTO_GENERATED add_custom_command( OUTPUT ${EXT_MGR_PROTO_GENERATED} - COMMAND ${FIGURA_CC} compile ${FIG_DIR}/manager.fig --server typescript --output ${EXT_PROTO_OUT}/manager.ts - COMMAND ${FIGURA_CC} compile ${FIG_DIR}/manager-extension.fig --client typescript --output ${EXT_PROTO_OUT}/manager-extension.ts - COMMAND ${FIGURA_CC} compile ${FIG_DIR}/manager-extension.fig --server typescript --output ${EXT_PROTO_OUT}/extension-manager.ts - COMMAND ${FIGURA_CC} compile ${FIG_DIR}/tsapi.fig --client typescript --output ${EXT_PROTO_OUT}/api.ts + COMMAND ${FIGURA_CC} compile ${FIG_DIR}/manager.fig --server typescript --wire beve --output ${EXT_PROTO_OUT}/manager.ts + COMMAND ${FIGURA_CC} compile ${FIG_DIR}/manager-extension.fig --client typescript --wire beve --output ${EXT_PROTO_OUT}/manager-extension.ts + COMMAND ${FIGURA_CC} compile ${FIG_DIR}/manager-extension.fig --server typescript --wire beve --output ${EXT_PROTO_OUT}/extension-manager.ts + COMMAND ${FIGURA_CC} compile ${FIG_DIR}/tsapi.fig --client typescript --wire beve --output ${EXT_PROTO_OUT}/api.ts DEPENDS ${FIGURA_CC} ${FIG_DIR}/manager.fig ${FIG_DIR}/manager-extension.fig ${FIG_DIR}/tsapi.fig COMMENT "Figura codegen: extension manager protos (typescript)" ) diff --git a/src/typescript/api/src/api/proto/api.ts b/src/typescript/api/src/api/proto/api.ts index 170348de8c..164198d451 100644 --- a/src/typescript/api/src/api/proto/api.ts +++ b/src/typescript/api/src/api/proto/api.ts @@ -11,7 +11,296 @@ interface JsonRpcMessage { } interface ITransport { - send(data: string): void; + send(data: WireData): void; +} + +export type WireData = Uint8Array; + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +class BeveWriter { + private buf = new Uint8Array(4096); + private view = new DataView(this.buf.buffer); + private len = 0; + + value(v: unknown): void { + if (v === null || v === undefined) { + this.u8(0x00); + return; + } + + switch (typeof v) { + case "boolean": + this.u8(v ? 0x18 : 0x08); + return; + case "number": + if (Number.isSafeInteger(v)) { + this.u8(0x69); + this.ensure(8); + this.view.setBigInt64(this.len, BigInt(v), true); + this.len += 8; + } else { + this.u8(0x61); + this.ensure(8); + this.view.setFloat64(this.len, v, true); + this.len += 8; + } + return; + case "string": + this.u8(0x02); + this.str(v); + return; + case "object": + break; + default: + this.u8(0x00); + return; + } + + if (v instanceof Uint8Array) { + this.u8(0x14); + this.size(v.length); + this.ensure(v.length); + this.buf.set(v, this.len); + this.len += v.length; + return; + } + + if (Array.isArray(v)) { + this.u8(0x05); + this.size(v.length); + for (const item of v) { + this.value(item === undefined || typeof item === "function" ? null : item); + } + return; + } + + const asJson = (v as { toJSON?: unknown }).toJSON; + if (typeof asJson === "function") { + this.value(asJson.call(v)); + return; + } + + const obj = v as Record; + const keys = Object.keys(obj).filter( + (k) => obj[k] !== undefined && typeof obj[k] !== "function", + ); + this.u8(0x03); + this.size(keys.length); + for (const key of keys) { + this.str(key); + this.value(obj[key]); + } + } + + finish(): Uint8Array { + return this.buf.slice(0, this.len); + } + + private ensure(extra: number) { + if (this.len + extra <= this.buf.length) return; + let capacity = this.buf.length * 2; + while (capacity < this.len + extra) capacity *= 2; + const next = new Uint8Array(capacity); + next.set(this.buf.subarray(0, this.len)); + this.buf = next; + this.view = new DataView(next.buffer); + } + + private u8(v: number) { + this.ensure(1); + this.buf[this.len++] = v; + } + + private size(v: number) { + if (v < 64) { + this.u8(v << 2); + } else if (v < 16384) { + this.ensure(2); + this.view.setUint16(this.len, (v << 2) | 1, true); + this.len += 2; + } else if (v < 1073741824) { + this.ensure(4); + this.view.setUint32(this.len, ((v << 2) | 2) >>> 0, true); + this.len += 4; + } else { + this.ensure(8); + this.view.setBigUint64(this.len, (BigInt(v) << 2n) | 3n, true); + this.len += 8; + } + } + + private str(s: string) { + const bytes = textEncoder.encode(s); + this.size(bytes.length); + this.ensure(bytes.length); + this.buf.set(bytes, this.len); + this.len += bytes.length; + } +} + +class BeveReader { + private view: DataView; + private pos = 0; + + constructor(private readonly buf: Uint8Array) { + this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + } + + value(): any { + const tag = this.buf[this.pos]; + + switch (tag & 7) { + case 0: { + this.pos++; + if (tag === 0x00) return null; + return (tag & 0x10) !== 0; + } + case 1: { + this.pos++; + return this.number(tag); + } + case 2: { + this.pos++; + return this.str(); + } + case 3: { + this.pos++; + if (((tag >> 3) & 3) !== 0) { + throw new Error("BEVE: only string-keyed objects are supported"); + } + const count = this.size(); + const obj: Record = {}; + for (let i = 0; i < count; i++) { + const key = this.str(); + const value = this.value(); + if (key === "__proto__") { + Object.defineProperty(obj, key, { + value, + enumerable: true, + configurable: true, + writable: true, + }); + } else { + obj[key] = value; + } + } + return obj; + } + case 4: { + this.pos++; + return this.typedArray(tag); + } + case 5: { + this.pos++; + const count = this.size(); + const arr = new Array(count); + for (let i = 0; i < count; i++) arr[i] = this.value(); + return arr; + } + default: + throw new Error(`BEVE: unsupported tag 0x${tag.toString(16)}`); + } + } + + private number(tag: number): number { + const kind = (tag >> 3) & 3; + const width = 1 << ((tag >> 5) & 7); + const at = this.pos; + this.pos += width; + + if (kind === 0) { + if (width === 4) return this.view.getFloat32(at, true); + if (width === 8) return this.view.getFloat64(at, true); + throw new Error(`BEVE: unsupported float width ${width}`); + } + + if (kind === 1) { + if (width === 1) return this.view.getInt8(at); + if (width === 2) return this.view.getInt16(at, true); + if (width === 4) return this.view.getInt32(at, true); + if (width === 8) return Number(this.view.getBigInt64(at, true)); + } else { + if (width === 1) return this.view.getUint8(at); + if (width === 2) return this.view.getUint16(at, true); + if (width === 4) return this.view.getUint32(at, true); + if (width === 8) return Number(this.view.getBigUint64(at, true)); + } + + throw new Error(`BEVE: unsupported integer width ${width}`); + } + + private typedArray(tag: number): any { + const kind = (tag >> 3) & 3; + const exponent = (tag >> 5) & 7; + const count = this.size(); + + if (kind === 3) { + if (exponent === 1) { + const arr = new Array(count); + for (let i = 0; i < count; i++) arr[i] = this.str(); + return arr; + } + const arr = new Array(count); + for (let i = 0; i < count; i++) { + const byte = this.buf[this.pos + (i >> 3)]; + arr[i] = (byte & (1 << (i & 7))) !== 0; + } + this.pos += (count + 7) >> 3; + return arr; + } + + const width = 1 << exponent; + + if (kind === 2 && width === 1) { + const bytes = this.buf.slice(this.pos, this.pos + count); + this.pos += count; + return bytes; + } + + const arr = new Array(count); + for (let i = 0; i < count; i++) { + const numberTag = (exponent << 5) | (kind << 3) | 1; + arr[i] = this.number(numberTag); + } + return arr; + } + + private size(): number { + const config = this.buf[this.pos] & 3; + if (config === 0) return this.buf[this.pos++] >> 2; + if (config === 1) { + const v = this.view.getUint16(this.pos, true) >> 2; + this.pos += 2; + return v; + } + if (config === 2) { + const v = this.view.getUint32(this.pos, true) >>> 2; + this.pos += 4; + return v; + } + const v = this.view.getBigUint64(this.pos, true) >> 2n; + this.pos += 8; + return Number(v); + } + + private str(): string { + const n = this.size(); + const s = textDecoder.decode(this.buf.subarray(this.pos, this.pos + n)); + this.pos += n; + return s; + } +} + +function encodeMessage(msg: JsonRpcMessage): WireData { + const writer = new BeveWriter(); + writer.value(msg); + return writer.finish(); +} + +function decodeMessage(data: WireData): JsonRpcMessage { + return new BeveReader(data).value() as JsonRpcMessage; } type EventSubscription = { @@ -21,8 +310,8 @@ type EventSubscription = { export class RpcTransport { constructor(private readonly transport: ITransport) { } - dispatchMessage(data: string) { - const msg = JSON.parse(data) as JsonRpcMessage; + dispatchMessage(data: WireData) { + const msg = decodeMessage(data); if (msg.id !== undefined) { const handler = this.requestMap.get(msg.id); @@ -69,7 +358,7 @@ export class RpcTransport { } private sendMessage(msg: JsonRpcMessage) { - this.transport.send(JSON.stringify(msg)); + this.transport.send(encodeMessage(msg)); } @@ -305,7 +594,7 @@ class ApplicationService { class UIService { constructor(private readonly transport: RpcTransport) {} - render(json: string): Promise { + render(json: Uint8Array): Promise { return this.transport.request("UI/render", { json}); } @@ -536,7 +825,7 @@ export class Client { this.EventCore = new EventCoreService(this.transport); } - route(msg: string): void { this.transport.dispatchMessage(msg); } + route(msg: WireData): void { this.transport.dispatchMessage(msg); } Application: ApplicationService; UI: UIService; WindowManagement: WindowManagementService; diff --git a/src/typescript/api/src/proto/ipc.ts b/src/typescript/api/src/proto/ipc.ts index fded94e2f6..b92d31db2b 100644 --- a/src/typescript/api/src/proto/ipc.ts +++ b/src/typescript/api/src/proto/ipc.ts @@ -11,7 +11,17 @@ interface JsonRpcMessage { } interface ITransport { - send(data: string): void; + send(data: WireData): void; +} + +export type WireData = string; + +function encodeMessage(msg: JsonRpcMessage): WireData { + return JSON.stringify(msg); +} + +function decodeMessage(data: WireData): JsonRpcMessage { + return JSON.parse(data) as JsonRpcMessage; } type EventSubscription = { @@ -21,8 +31,8 @@ type EventSubscription = { export class RpcTransport { constructor(private readonly transport: ITransport) { } - dispatchMessage(data: string) { - const msg = JSON.parse(data) as JsonRpcMessage; + dispatchMessage(data: WireData) { + const msg = decodeMessage(data); if (msg.id !== undefined) { const handler = this.requestMap.get(msg.id); @@ -69,7 +79,7 @@ export class RpcTransport { } private sendMessage(msg: JsonRpcMessage) { - this.transport.send(JSON.stringify(msg)); + this.transport.send(encodeMessage(msg)); } @@ -242,7 +252,7 @@ export class Client { this.Ipc = new IpcService(this.transport); } - route(msg: string): void { this.transport.dispatchMessage(msg); } + route(msg: WireData): void { this.transport.dispatchMessage(msg); } Ipc: IpcService; } diff --git a/src/typescript/api/tsconfig.json b/src/typescript/api/tsconfig.json index eef9a1112d..bbaad33373 100644 --- a/src/typescript/api/tsconfig.json +++ b/src/typescript/api/tsconfig.json @@ -2,6 +2,7 @@ "include": ["src/components", "types", "src", "src/commands"], "compilerOptions": { "jsx": "react-jsx", + "target": "es2022", "module": "nodenext", "declaration": true, "declarationMap": false, diff --git a/src/typescript/cmake/Api.cmake b/src/typescript/cmake/Api.cmake index f7fce8e3cf..371a8bed8c 100644 --- a/src/typescript/cmake/Api.cmake +++ b/src/typescript/cmake/Api.cmake @@ -42,7 +42,7 @@ endif() # Step 1: generate TS protos from .fig + figura binary add_custom_command( OUTPUT ${API_PROTO_GENERATED} - COMMAND ${FIGURA_CC} compile ${API_FIG_FILE} --client typescript --output ${API_PROTO_GENERATED} + COMMAND ${FIGURA_CC} compile ${API_FIG_FILE} --client typescript --wire beve --output ${API_PROTO_GENERATED} DEPENDS ${FIGURA_CC} ${API_FIG_FILE} COMMENT "Figura codegen: API client (typescript)" ) diff --git a/src/typescript/extension-manager/src/index.ts b/src/typescript/extension-manager/src/index.ts index 7a1c2745e0..1794432578 100644 --- a/src/typescript/extension-manager/src/index.ts +++ b/src/typescript/extension-manager/src/index.ts @@ -9,7 +9,7 @@ import * as manager from "./proto/manager"; import * as extension from "./proto/manager-extension"; import * as path from "node:path"; -import type { EnvironmentType } from "./types"; +import type { EnvironmentType, WorkerManagerMessage } from "./types"; import { Logger } from "./logger"; import { setTimeout } from "node:timers"; @@ -31,7 +31,7 @@ type WorkerInfo = { export const logger = new Logger(); class ExtensionManager extends manager.ManagerService { - constructor(transport: manager.RpcTransport) { + constructor(private readonly transport: manager.RpcTransport) { super(transport); this.workerPool.push(this.createWorker("production")); } @@ -115,10 +115,14 @@ class ExtensionManager extends manager.ManagerService { this.unload(sessionId); }); - worker.on("message", (data) => { - client.route(data); // try routing to us - if (workerInfo.status !== "running") return; - this.emit_extensionMessage(sessionId, data); // regular extension stuff + worker.on("message", (data: Uint8Array | WorkerManagerMessage) => { + if (data instanceof Uint8Array) { + if (workerInfo.status !== "running") return; + this.emit_extensionMessage(sessionId, data); + return; + } + + if (data?.channel === "manager") client.route(data.data); }); worker.on("messageerror", (error) => { @@ -135,7 +139,7 @@ class ExtensionManager extends manager.ManagerService { logger.error(`worker error: ${error}`); }); - worker.on("online", () => {}); + worker.on("online", () => { }); const stdoutStream = fs.createWriteStream(stdoutLog); const stderrStream = fs.createWriteStream(stderrLog); @@ -229,7 +233,7 @@ class ExtensionManager extends manager.ManagerService { async messageExtension( session_id: string, - payload: string, + payload: Uint8Array, ): Promise { const worker = this.workerMap.get(session_id); worker?.client.Lifecycle.send_message(payload); @@ -303,7 +307,7 @@ class Vicinae { const packet = this.currentMessage.data.subarray(4, length + 4); - this.server.route(packet.toString("utf8"))?.catch((error) => { + this.server.route(packet)?.catch((error) => { logger.error(`Uncaught exception from handler: ${error}`); }); diff --git a/src/typescript/extension-manager/src/loaders/load-view-command.tsx b/src/typescript/extension-manager/src/loaders/load-view-command.tsx index a8ebf6d550..de30a3456f 100644 --- a/src/typescript/extension-manager/src/loaders/load-view-command.tsx +++ b/src/typescript/extension-manager/src/loaders/load-view-command.tsx @@ -50,7 +50,7 @@ export default async function (data: extensionServer.LaunchEventData) { const module = await import(pathToFileURL(data.entrypoint).href); const Component = module.default.default; const sendRender = (views: ViewData[]) => { - globalState.client.UI.render(JSON.stringify({ views })); + globalState.client.UI.render(Buffer.from(JSON.stringify({ views }))); }; const renderer = createRenderer({ onInitialRender: sendRender, diff --git a/src/typescript/extension-manager/src/types.ts b/src/typescript/extension-manager/src/types.ts index 38a5e936dd..9f8fd26edc 100644 --- a/src/typescript/extension-manager/src/types.ts +++ b/src/typescript/extension-manager/src/types.ts @@ -1,3 +1,8 @@ const values = ["development", "production"] as const; export type EnvironmentType = (typeof values)[number]; + +export type WorkerManagerMessage = { + channel: "manager"; + data: Uint8Array; +}; diff --git a/src/typescript/extension-manager/src/worker.tsx b/src/typescript/extension-manager/src/worker.tsx index d709dbd3e1..b3d8d9082e 100644 --- a/src/typescript/extension-manager/src/worker.tsx +++ b/src/typescript/extension-manager/src/worker.tsx @@ -15,7 +15,7 @@ import loadView from "./loaders/load-view-command"; import { patchRequire } from "./patch-require"; import * as api from "./proto/api"; import * as extensionServer from "./proto/extension-manager"; -import type { EnvironmentType } from "./types"; +import type { EnvironmentType, WorkerManagerMessage } from "./types"; class Lifecycle extends extensionServer.LifecycleService { async launch(data: extensionServer.LaunchEventData): Promise { @@ -45,7 +45,7 @@ class Lifecycle extends extensionServer.LifecycleService { return true; } - async send_message(msg: string): Promise { + async send_message(msg: Uint8Array): Promise { client.route(msg); return true; } @@ -53,14 +53,15 @@ class Lifecycle extends extensionServer.LifecycleService { const serverRpc = new extensionServer.RpcTransport({ send: (msg) => { - parentPort?.postMessage(msg); + const message: WorkerManagerMessage = { channel: "manager", data: msg }; + parentPort?.postMessage(message); }, }); const server = new extensionServer.Server(serverRpc, new Lifecycle(serverRpc)); const clientRpc = new api.RpcTransport({ - send: (msg: string) => { + send: (msg) => { parentPort?.postMessage(msg); }, }); diff --git a/src/typescript/extension-manager/tsconfig.json b/src/typescript/extension-manager/tsconfig.json index 49d5e18502..c022114b7f 100644 --- a/src/typescript/extension-manager/tsconfig.json +++ b/src/typescript/extension-manager/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "jsx": "react-jsx" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ From 6f319790296e7cd723438daf37c42749f1f6eafc Mon Sep 17 00:00:00 2001 From: Aurelien Brabant Date: Sun, 9 Aug 2026 06:46:20 +0200 Subject: [PATCH 2/3] fix: format --- src/lib/figura/src/codegen/codegen.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/figura/src/codegen/codegen.hpp b/src/lib/figura/src/codegen/codegen.hpp index 16288a527a..9728b9cdef 100644 --- a/src/lib/figura/src/codegen/codegen.hpp +++ b/src/lib/figura/src/codegen/codegen.hpp @@ -53,7 +53,8 @@ inline std::string glazeBeveEnumSpecializations(std::string_view ns, std::span struct from {\n"; oss << "\ttemplate \n"; - oss << "\tstatic void op(" << ns << "::" << e.name << " &value, is_context auto &&ctx, Args &&...args) {\n"; + oss << "\tstatic void op(" << ns << "::" << e.name + << " &value, is_context auto &&ctx, Args &&...args) {\n"; oss << "\t\t" << names.str() << "\n"; oss << "\t\tstd::string s;\n"; oss << "\t\tparse::op(s, ctx, args...);\n"; From 56b4f3bd5d8230045e759afa56485df1eb84a116 Mon Sep 17 00:00:00 2001 From: Aurelien Brabant Date: Sun, 9 Aug 2026 06:50:22 +0200 Subject: [PATCH 3/3] fix: typescript format --- src/typescript/extension-manager/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typescript/extension-manager/src/index.ts b/src/typescript/extension-manager/src/index.ts index 1794432578..e1d88c0462 100644 --- a/src/typescript/extension-manager/src/index.ts +++ b/src/typescript/extension-manager/src/index.ts @@ -139,7 +139,7 @@ class ExtensionManager extends manager.ManagerService { logger.error(`worker error: ${error}`); }); - worker.on("online", () => { }); + worker.on("online", () => {}); const stdoutStream = fs.createWriteStream(stdoutLog); const stderrStream = fs.createWriteStream(stderrLog);