From 3fb07d14bf1dc9928ed7048876a3a86a4eaa4a96 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 8 Feb 2026 08:49:42 +0100 Subject: [PATCH] feat(wrpc): add transport abstraction and language-specific bindings (Phase 3+4) Phase 3 - Transport Abstraction Layer: - Add WrpcTransportInfo provider for pluggable transport configuration - Create transport rules: tcp_transport, nats_transport, unix_transport, quic_transport - Each transport has address validation (host:port, nats://, absolute paths) - wrpc_serve rule now accepts transport_config attribute for type-safe configuration - Backward compatible with legacy transport/address string attributes Phase 4 - Multi-Language Binding Support: - Add wrpc_rust_bindings() macro for idiomatic Rust binding generation - Add wrpc_go_bindings() macro for idiomatic Go binding generation - Note: wit-bindgen-wrpc only supports Rust and Go (not Python/JavaScript) - Updated documentation with usage examples Usage examples: tcp_transport(name = "dev_server", address = "localhost:8080") wrpc_serve(name = "serve", component = ":my_comp", transport = ":dev_server") wrpc_rust_bindings(name = "calc_client", wit = ":calc.wit", world = "calc") Closes #332 Co-Authored-By: Claude Opus 4.5 --- providers/providers.bzl | 14 ++ wrpc/BUILD.bazel | 11 ++ wrpc/defs.bzl | 343 +++++++++++++++++++++++++++++++++++----- wrpc/transports.bzl | 255 +++++++++++++++++++++++++++++ 4 files changed, 583 insertions(+), 40 deletions(-) create mode 100644 wrpc/transports.bzl diff --git a/providers/providers.bzl b/providers/providers.bzl index 8e0177df..091606b2 100644 --- a/providers/providers.bzl +++ b/providers/providers.bzl @@ -180,3 +180,17 @@ WasmPrecompiledInfo = provider( "compatibility_hash": "Hash for compatibility checking", }, ) + +# Provider for WRPC transport configuration (Phase 3 WRPC modernization) +WrpcTransportInfo = provider( + doc = "Configuration for WRPC transport mechanism", + fields = { + "transport_type": "Transport type: tcp, nats, unix, or quic", + "address": "Transport address (format depends on transport type)", + "cli_args": "List of CLI arguments for wrpc-wasmtime", + "extra_args": "Additional transport-specific CLI arguments", + "address_format": "Example of valid address format for this transport", + "config_file": "Optional transport-specific config file", + "metadata": "Dict for transport-specific metadata", + }, +) diff --git a/wrpc/BUILD.bazel b/wrpc/BUILD.bazel index fa190508..c3e55dae 100644 --- a/wrpc/BUILD.bazel +++ b/wrpc/BUILD.bazel @@ -10,6 +10,17 @@ bzl_library( srcs = ["defs.bzl"], visibility = ["//visibility:public"], deps = [ + "//providers:providers", "//toolchains:wasm_toolchain", ], ) + +# Bzl library for transport configuration rules +bzl_library( + name = "transports", + srcs = ["transports.bzl"], + visibility = ["//visibility:public"], + deps = [ + "//providers:providers", + ], +) diff --git a/wrpc/defs.bzl b/wrpc/defs.bzl index 35141c39..cee860a0 100644 --- a/wrpc/defs.bzl +++ b/wrpc/defs.bzl @@ -1,11 +1,19 @@ """Bazel rules for wrpc (WebAssembly Component RPC) -Modernized to follow Bazel best practices: +Modernized to follow Bazel best practices (Phase 1-4 WRPC modernization): - Uses ctx.actions.run() for build-time actions - Cross-platform Python launchers for executable rules +- Transport abstraction via WrpcTransportInfo provider (Phase 3) +- Language-specific binding rules (Phase 4) - No shell script generation """ +load("//providers:providers.bzl", "WrpcTransportInfo") + +# ============================================================================= +# Binding Generation Rules +# ============================================================================= + def _wrpc_bindgen_impl(ctx): """Implementation of wrpc_bindgen rule @@ -63,7 +71,7 @@ wrpc_bindgen = rule( mandatory = True, ), "language": attr.string( - doc = "Target language for bindings (rust, go, etc.)", + doc = "Target language for bindings (rust, go)", default = "rust", values = ["rust", "go"], ), @@ -72,6 +80,80 @@ wrpc_bindgen = rule( doc = "Generate language bindings for wrpc from WIT interfaces", ) +# ============================================================================= +# Language-Specific Binding Macros (Phase 4) +# ============================================================================= + +def wrpc_rust_bindings(name, wit, world, **kwargs): + """Generate Rust bindings for wrpc from WIT interface. + + This is the idiomatic way to generate wrpc client/server bindings in Rust. + The output contains Rust source files implementing wrpc traits. + + Args: + name: Target name + wit: WIT file label defining the interface + world: WIT world to generate bindings for + **kwargs: Additional arguments passed to wrpc_bindgen + + Example: + wrpc_rust_bindings( + name = "calculator_client", + wit = "//wit:calculator", + world = "calculator-client", + ) + + rust_library( + name = "calculator_lib", + srcs = [":calculator_client"], + deps = ["@crates//wrpc:wrpc"], + ) + """ + wrpc_bindgen( + name = name, + wit = wit, + world = world, + language = "rust", + **kwargs + ) + +def wrpc_go_bindings(name, wit, world, **kwargs): + """Generate Go bindings for wrpc from WIT interface. + + This is the idiomatic way to generate wrpc client/server bindings in Go. + The output contains Go source files implementing wrpc interfaces. + + Args: + name: Target name + wit: WIT file label defining the interface + world: WIT world to generate bindings for + **kwargs: Additional arguments passed to wrpc_bindgen + + Example: + wrpc_go_bindings( + name = "calculator_client", + wit = "//wit:calculator", + world = "calculator-client", + ) + + go_library( + name = "calculator_lib", + srcs = [":calculator_client"], + deps = ["@wrpc//go:wrpc"], + ) + """ + wrpc_bindgen( + name = name, + wit = wit, + world = world, + language = "go", + **kwargs + ) + +# ============================================================================= +# Serve Rule with Transport Abstraction +# ============================================================================= + # Cross-platform Python launcher template for wrpc_serve _SERVE_LAUNCHER_TEMPLATE = '''#!/usr/bin/env python3 """Cross-platform wrpc serve launcher @@ -83,6 +165,64 @@ import os import subprocess import sys +def main(): + # Configuration (substituted at generation time) + wrpc_path = {wrpc_path!r} + component_path = {component_path!r} + cli_args = {cli_args!r} + address = {address!r} + + # Resolve paths relative to runfiles + if "RUNFILES_DIR" in os.environ: + runfiles = os.environ["RUNFILES_DIR"] + else: + # Fallback: assume we're in the workspace root + runfiles = os.path.dirname(os.path.abspath(__file__)) + + # Build full paths + full_wrpc = os.path.join(runfiles, wrpc_path) + full_component = os.path.join(runfiles, component_path) + + # Check if files exist + if not os.path.exists(full_wrpc): + # Try without runfiles prefix + full_wrpc = wrpc_path + if not os.path.exists(full_component): + full_component = component_path + + print(f"Starting wrpc server...") + print(f"Component: {{full_component}}") + print(f"Address: {{address}}") + + # Build command using CLI args from transport provider + cmd = [full_wrpc] + cli_args + [full_component, address] + + # Execute wrpc + try: + result = subprocess.run(cmd) + sys.exit(result.returncode) + except FileNotFoundError: + print(f"Error: wrpc not found at {{full_wrpc}}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\\nServer stopped") + sys.exit(0) + +if __name__ == "__main__": + main() +''' + +# Legacy launcher template for backward compatibility +_SERVE_LAUNCHER_LEGACY_TEMPLATE = '''#!/usr/bin/env python3 +"""Cross-platform wrpc serve launcher (legacy mode) + +Generated by rules_wasm_component wrpc_serve rule. +This launcher works on Windows, macOS, and Linux. +""" +import os +import subprocess +import sys + def main(): # Configuration (substituted at generation time) wrpc_path = {wrpc_path!r} @@ -113,14 +253,8 @@ def main(): print(f"Transport: {{transport}}") print(f"Address: {{address}}") - # Build command - cmd = [ - full_wrpc, - "serve", - "--component", full_component, - "--transport", transport, - "--address", address, - ] + # Build command (legacy: wrpc serve
) + cmd = [full_wrpc, transport, "serve", full_component, address] # Execute wrpc try: @@ -141,7 +275,9 @@ def _wrpc_serve_impl(ctx): """Implementation of wrpc_serve rule Creates a cross-platform Python launcher to serve a WebAssembly component via wrpc. - Replaces shell script generation with Python for Windows/macOS/Linux compatibility. + Supports both: + - Modern: transport_config attribute (WrpcTransportInfo provider) + - Legacy: transport + address string attributes (deprecated but supported) """ # Get the wasm toolchain (which includes wrpc) @@ -154,12 +290,27 @@ def _wrpc_serve_impl(ctx): # Create cross-platform Python launcher launcher = ctx.actions.declare_file(ctx.attr.name + "_serve.py") - launcher_content = _SERVE_LAUNCHER_TEMPLATE.format( - wrpc_path = wrpc.short_path, - component_path = component.short_path, - transport = ctx.attr.transport, - address = ctx.attr.address, - ) + # Check if using new transport provider or legacy string attributes + if ctx.attr.transport_config: + # Modern: use transport provider + transport_info = ctx.attr.transport_config[WrpcTransportInfo] + cli_args = transport_info.cli_args + transport_info.extra_args + address = transport_info.address + + launcher_content = _SERVE_LAUNCHER_TEMPLATE.format( + wrpc_path = wrpc.short_path, + component_path = component.short_path, + cli_args = cli_args, + address = address, + ) + else: + # Legacy: use string attributes (deprecated) + launcher_content = _SERVE_LAUNCHER_LEGACY_TEMPLATE.format( + wrpc_path = wrpc.short_path, + component_path = component.short_path, + transport = ctx.attr.transport, + address = ctx.attr.address, + ) ctx.actions.write( output = launcher, @@ -186,21 +337,49 @@ wrpc_serve = rule( allow_single_file = [".wasm"], mandatory = True, ), + "transport_config": attr.label( + doc = "Transport configuration (from tcp_transport, nats_transport, etc.)", + providers = [WrpcTransportInfo], + ), + # Legacy attributes (deprecated, use transport_config instead) "transport": attr.string( - doc = "Transport protocol (tcp, nats, unix, quic)", + doc = "[Deprecated: use transport_config] Transport protocol (tcp, nats, unix, quic)", default = "tcp", values = ["tcp", "nats", "unix", "quic"], ), "address": attr.string( - doc = "Address to bind server to", + doc = "[Deprecated: use transport_config] Address to bind server to", default = "0.0.0.0:8080", ), }, toolchains = ["//toolchains:wasm_tools_toolchain_type"], executable = True, - doc = "Serve a WebAssembly component via wrpc (cross-platform)", + doc = """Serve a WebAssembly component via wrpc (cross-platform). + + Two ways to configure transport: + + Modern (recommended): + tcp_transport(name = "tcp_dev", address = "localhost:8080") + wrpc_serve( + name = "serve", + component = ":my_component", + transport_config = ":tcp_dev", + ) + + Legacy (deprecated): + wrpc_serve( + name = "serve", + component = ":my_component", + transport = "tcp", + address = "localhost:8080", + ) + """, ) +# ============================================================================= +# Invoke Rule with Transport Abstraction +# ============================================================================= + # Cross-platform Python launcher template for wrpc_invoke _INVOKE_LAUNCHER_TEMPLATE = '''#!/usr/bin/env python3 """Cross-platform wrpc invoke launcher @@ -212,6 +391,58 @@ import os import subprocess import sys +def main(): + # Configuration (substituted at generation time) + wrpc_path = {wrpc_path!r} + function = {function!r} + cli_args = {cli_args!r} + address = {address!r} + + # Resolve paths relative to runfiles + if "RUNFILES_DIR" in os.environ: + runfiles = os.environ["RUNFILES_DIR"] + else: + runfiles = os.path.dirname(os.path.abspath(__file__)) + + # Build full path to wrpc + full_wrpc = os.path.join(runfiles, wrpc_path) + if not os.path.exists(full_wrpc): + full_wrpc = wrpc_path + + print(f"Invoking wrpc function...") + print(f"Function: {{function}}") + print(f"Address: {{address}}") + + # Build command using CLI args from transport provider + # wrpc run --invoke
+ cmd = [full_wrpc] + cli_args[:1] + ["run", "--invoke", function, address] + + # Add any additional arguments passed to the launcher + cmd.extend(sys.argv[1:]) + + # Execute wrpc + try: + result = subprocess.run(cmd) + sys.exit(result.returncode) + except FileNotFoundError: + print(f"Error: wrpc not found at {{full_wrpc}}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() +''' + +# Legacy launcher template for backward compatibility +_INVOKE_LAUNCHER_LEGACY_TEMPLATE = '''#!/usr/bin/env python3 +"""Cross-platform wrpc invoke launcher (legacy mode) + +Generated by rules_wasm_component wrpc_invoke rule. +This launcher works on Windows, macOS, and Linux. +""" +import os +import subprocess +import sys + def main(): # Configuration (substituted at generation time) wrpc_path = {wrpc_path!r} @@ -235,18 +466,11 @@ def main(): print(f"Transport: {{transport}}") print(f"Address: {{address}}") - # Build command - cmd = [ - full_wrpc, - "invoke", - "--function", function, - "--transport", transport, - "--address", address, - ] + # Build command (legacy: wrpc run --invoke
) + cmd = [full_wrpc, transport, "run", "--invoke", function, address] # Add any additional arguments passed to the launcher - for arg in sys.argv[1:]: - cmd.extend(["--arg", arg]) + cmd.extend(sys.argv[1:]) # Execute wrpc try: @@ -265,7 +489,7 @@ def _wrpc_invoke_impl(ctx): Creates a cross-platform Python launcher to invoke a function on a remote WebAssembly component via wrpc. - Replaces shell script generation with Python for Windows/macOS/Linux compatibility. + Supports both transport_config (modern) and transport+address (legacy). """ # Get the wasm toolchain (which includes wrpc) @@ -275,12 +499,27 @@ def _wrpc_invoke_impl(ctx): # Create cross-platform Python launcher launcher = ctx.actions.declare_file(ctx.attr.name + "_invoke.py") - launcher_content = _INVOKE_LAUNCHER_TEMPLATE.format( - wrpc_path = wrpc.short_path, - function = ctx.attr.function, - transport = ctx.attr.transport, - address = ctx.attr.address, - ) + # Check if using new transport provider or legacy string attributes + if ctx.attr.transport_config: + # Modern: use transport provider + transport_info = ctx.attr.transport_config[WrpcTransportInfo] + cli_args = transport_info.cli_args + transport_info.extra_args + address = transport_info.address + + launcher_content = _INVOKE_LAUNCHER_TEMPLATE.format( + wrpc_path = wrpc.short_path, + function = ctx.attr.function, + cli_args = cli_args, + address = address, + ) + else: + # Legacy: use string attributes (deprecated) + launcher_content = _INVOKE_LAUNCHER_LEGACY_TEMPLATE.format( + wrpc_path = wrpc.short_path, + function = ctx.attr.function, + transport = ctx.attr.transport, + address = ctx.attr.address, + ) ctx.actions.write( output = launcher, @@ -306,17 +545,41 @@ wrpc_invoke = rule( doc = "Function to invoke on remote component", mandatory = True, ), + "transport_config": attr.label( + doc = "Transport configuration (from tcp_transport, nats_transport, etc.)", + providers = [WrpcTransportInfo], + ), + # Legacy attributes (deprecated, use transport_config instead) "transport": attr.string( - doc = "Transport protocol (tcp, nats, unix, quic)", + doc = "[Deprecated: use transport_config] Transport protocol (tcp, nats, unix, quic)", default = "tcp", values = ["tcp", "nats", "unix", "quic"], ), "address": attr.string( - doc = "Address of the remote component", + doc = "[Deprecated: use transport_config] Address of the remote component", default = "localhost:8080", ), }, toolchains = ["//toolchains:wasm_tools_toolchain_type"], executable = True, - doc = "Invoke a function on a remote WebAssembly component via wrpc (cross-platform)", + doc = """Invoke a function on a remote WebAssembly component via wrpc (cross-platform). + + Two ways to configure transport: + + Modern (recommended): + tcp_transport(name = "tcp_server", address = "localhost:8080") + wrpc_invoke( + name = "invoke", + function = "calculator:add", + transport_config = ":tcp_server", + ) + + Legacy (deprecated): + wrpc_invoke( + name = "invoke", + function = "calculator:add", + transport = "tcp", + address = "localhost:8080", + ) + """, ) diff --git a/wrpc/transports.bzl b/wrpc/transports.bzl new file mode 100644 index 00000000..2faec318 --- /dev/null +++ b/wrpc/transports.bzl @@ -0,0 +1,255 @@ +"""WRPC Transport Configuration Rules (Phase 3 WRPC modernization) + +This module provides pluggable transport configuration for WRPC rules. +Instead of hardcoded transport strings, users define transport configurations +as Bazel targets that can be validated at BUILD time and reused across rules. + +Supported transports: +- TCP: Direct TCP connections (tcp_transport) +- NATS: NATS.io messaging (nats_transport) +- Unix: Unix domain sockets (unix_transport) +- QUIC: QUIC protocol (quic_transport) + +Example: + load("@rules_wasm_component//wrpc:transports.bzl", "tcp_transport") + + tcp_transport( + name = "dev_server", + address = "localhost:8080", + ) + + wrpc_serve( + name = "serve", + component = ":my_component", + transport = ":dev_server", + ) +""" + +load("//providers:providers.bzl", "WrpcTransportInfo") + +# ============================================================================= +# Address Validation Utilities +# ============================================================================= + +def _validate_host_port(address, transport_name): + """Validate host:port address format.""" + if ":" not in address: + fail("{} address must include port (host:port), got: {}".format( + transport_name, address)) + + parts = address.rsplit(":", 1) + if len(parts) != 2: + fail("{} address must be host:port, got: {}".format( + transport_name, address)) + + host, port = parts + if not port.isdigit(): + fail("{} port must be numeric, got: {}".format(transport_name, port)) + + port_num = int(port) + if port_num < 1 or port_num > 65535: + fail("{} port must be 1-65535, got: {}".format(transport_name, port_num)) + + return True + +# ============================================================================= +# TCP Transport +# ============================================================================= + +def _tcp_transport_impl(ctx): + """TCP transport configuration.""" + address = ctx.attr.address + _validate_host_port(address, "TCP") + + return [ + WrpcTransportInfo( + transport_type = "tcp", + address = address, + cli_args = ["tcp", "serve"], + extra_args = [], + address_format = "host:port (e.g., localhost:8080, 0.0.0.0:8080)", + config_file = None, + metadata = {}, + ), + ] + +tcp_transport = rule( + implementation = _tcp_transport_impl, + attrs = { + "address": attr.string( + doc = "TCP address in host:port format (e.g., localhost:8080)", + mandatory = True, + ), + }, + doc = """Configure TCP transport for WRPC. + + TCP is the simplest transport, suitable for direct point-to-point connections. + + Example: + tcp_transport( + name = "server_tcp", + address = "0.0.0.0:8080", + ) + """, +) + +# ============================================================================= +# NATS Transport +# ============================================================================= + +def _nats_transport_impl(ctx): + """NATS.io transport configuration.""" + address = ctx.attr.address + + # NATS URLs should start with nats:// + if not address.startswith("nats://"): + fail("NATS address must start with nats://, got: {}".format(address)) + + # Validate the host:port part after nats:// + host_port = address[7:] # Skip "nats://" + if ":" not in host_port: + fail("NATS address must include port (nats://host:port), got: {}".format(address)) + + extra_args = [] + if ctx.attr.prefix: + extra_args.extend(["--prefix", ctx.attr.prefix]) + + return [ + WrpcTransportInfo( + transport_type = "nats", + address = address, + cli_args = ["nats", "serve"], + extra_args = extra_args, + address_format = "nats://host:port (e.g., nats://localhost:4222)", + config_file = ctx.file.config if ctx.attr.config else None, + metadata = { + "prefix": ctx.attr.prefix, + }, + ), + ] + +nats_transport = rule( + implementation = _nats_transport_impl, + attrs = { + "address": attr.string( + doc = "NATS server URL (nats://host:port)", + mandatory = True, + ), + "prefix": attr.string( + doc = "Subject prefix for NATS messages", + default = "", + ), + "config": attr.label( + doc = "Optional NATS configuration file", + allow_single_file = True, + ), + }, + doc = """Configure NATS transport for WRPC. + + NATS provides pub/sub messaging, suitable for multi-component routing + and service discovery patterns. Requires NATS server >= 2.10.20. + + Example: + nats_transport( + name = "nats_prod", + address = "nats://nats.example.com:4222", + prefix = "my-service", + ) + """, +) + +# ============================================================================= +# Unix Domain Socket Transport +# ============================================================================= + +def _unix_transport_impl(ctx): + """Unix domain socket transport configuration.""" + socket_path = ctx.attr.socket_path + + if not socket_path.startswith("/"): + fail("Unix socket path must be absolute (start with /), got: {}".format(socket_path)) + + return [ + WrpcTransportInfo( + transport_type = "unix", + address = socket_path, + cli_args = ["tcp", "serve"], # Unix uses tcp subcommand with socket path + extra_args = [], + address_format = "/path/to/socket (e.g., /tmp/wrpc.sock)", + config_file = None, + metadata = {}, + ), + ] + +unix_transport = rule( + implementation = _unix_transport_impl, + attrs = { + "socket_path": attr.string( + doc = "Absolute path to Unix domain socket", + mandatory = True, + ), + }, + doc = """Configure Unix domain socket transport for WRPC. + + Unix sockets provide high-performance IPC on the same host. + Only works on Unix-like systems (Linux, macOS). + + Example: + unix_transport( + name = "local_socket", + socket_path = "/tmp/my_service.sock", + ) + """, +) + +# ============================================================================= +# QUIC Transport +# ============================================================================= + +def _quic_transport_impl(ctx): + """QUIC transport configuration.""" + address = ctx.attr.address + _validate_host_port(address, "QUIC") + + extra_args = [] + if ctx.attr.insecure: + extra_args.append("--insecure") + + return [ + WrpcTransportInfo( + transport_type = "quic", + address = address, + cli_args = ["tcp", "serve"], # QUIC may use different subcommand + extra_args = extra_args, + address_format = "host:port (e.g., localhost:4433)", + config_file = None, + metadata = { + "insecure": ctx.attr.insecure, + }, + ), + ] + +quic_transport = rule( + implementation = _quic_transport_impl, + attrs = { + "address": attr.string( + doc = "QUIC address in host:port format", + mandatory = True, + ), + "insecure": attr.bool( + doc = "Disable TLS verification (for testing only)", + default = False, + ), + }, + doc = """Configure QUIC transport for WRPC. + + QUIC provides encrypted, multiplexed transport with built-in + congestion control. Good for unreliable networks. + + Example: + quic_transport( + name = "secure_quic", + address = "0.0.0.0:4433", + ) + """, +)