gRPC over FFI and IPC
For asynchronous services shared between native libraries and browser/Node WebAssembly, use the module call runtime. C/C++ compile with Clang directly; Rust and Go use their own runtimes. Typed clients work with or without workers and do not require gRPC. Go/Rust backends do not depend on the C callback runtime.
FFI and IPC transport for gRPC. Implements grpc.ClientConnInterface — same client code works over FFI, IPC, or network.
// Network gRPC (the usual way)
conn, _ := grpc.Dial("localhost:50051")
// Synurang FFI - source (compiled-in backend)
conn := api.NewMyServiceFfiClientConn(server)
// Synurang FFI - plugin (loaded at runtime)
plugin := synurang.LoadPlugin("./plugin.so")
conn := synurang.NewPluginClientConn(plugin, "MyService")
// Synurang IPC (child process)
conn, _ := synurang.StartProcess(ctx, exec.Command("./child"))
// Same client code works for all!
client := pb.NewGreeterClient(conn)
resp, _ := client.SayHello(ctx, &pb.HelloRequest{Name: "World"})TypeScript follows the same model with @grpc/grpc-js: PluginChannel is a grpc-js channel, so a generated client runs unchanged over the network or FFI.
// Network gRPC (the usual way)
const client = new GreeterClient("localhost:50051", grpc.credentials.createInsecure());
// Synurang FFI - plugin host
const client = new GreeterClient("synurang", grpc.credentials.createInsecure(), {
channelOverride: new PluginChannel(host, GreeterService),
});
// Same client code and the same ServiceError for both
client.sayHello(new HelloRequest({ name: "World" }), (err, resp) => { /* ... */ });- Flutter + Go apps: Compile Go as a shared library, call via FFI. No separate server process.
- Android + native plugins: Load Go/C++/Rust plugins via JNI. Write
.proto, never touch JNI again. - In-process microservices: Ship proprietary gRPC services as
.sobinaries. No network, no source exposure. - Sidecar processes: Spawn isolated child processes with different privileges or crash isolation.
- Polyglot backends: Parent in any language (Go/Dart/C++/Rust/Java/C#) spawns child in any language.
- Debugging: Enable TCP/UDS alongside FFI/IPC. Use grpcurl or Postman while app runs.
# Install the Rust-based code generator from this checkout
cargo install --locked --path cmd/protoc-gen-synurang-ffi
# Generate FFI bindings from your .proto
protoc --synurang-ffi_out=. --synurang-ffi_opt=lang=go service.proto
# Generate native C ABI (flattened parameters, no protobuf at call site)
protoc --synurang-ffi_out=. --synurang-ffi_opt=lang=rust,mode=native service.protoSee Installation & Quick Start for the full setup.
Synurang implements grpc.ClientConnInterface. The gRPC client works unchanged whether messages go over TCP or FFI.
┌─────────────────┐ ┌─────────────────┐
│ Your Client │ │ Your Server │
│ │ │ │
│ gRPC Client │ ──── Synurang ────► │ gRPC Server │
│ (unchanged) │ (FFI) │ (unchanged) │
└─────────────────┘ └─────────────────┘
All four gRPC patterns work: unary, server streaming, client streaming, and bidirectional streaming.
| Transport | Description | Use Case |
|---|---|---|
| FFI (source) | Compile Go into app | Embedded backend |
| FFI (plugin) | Load .so at runtime |
Proprietary / in-process microservices |
| Process Mode | Parent-Child IPC | Sidecar processes, isolation, privileges |
| TCP/UDS | Standard network gRPC | Debugging, distributed |
FFI and TCP/UDS run simultaneously on the same server.
The canonical generator executable is implemented in Rust. lang selects the
language it emits (so lang=go remains supported), while mode selects the
kind of output. Omitting lang preserves the legacy combined Go/Dart output.
| Language | Mode | Output | Description |
|---|---|---|---|
| c / cpp | module |
_lite.h/.c + _ffi.h/.c |
Async callback provider, native and direct Clang WASM |
| rust | module |
_ffi.rs |
Async Future/Waker provider, native and raw WASM |
| go | module |
_ffi.pb.go |
Goroutine/context provider, native and Go WASM |
| typescript | client |
_ffi.ts + _lite.ts + runtime |
Promise/stream client over native, WASM or workers |
| dart / java / csharp / python / swift | client |
Typed client and language-appropriate codecs | New call host; see the capability table |
| go | (default) | _ffi.pb.go |
gRPC FFI client/server bindings |
| dart | (default) | _ffi.pb.dart |
Dart FFI client |
| cpp | (default) | _ffi.h |
C++ FFI host header |
| rust | (default) | _ffi.rs |
Rust FFI host bindings |
| rust | native |
_ffi_native.rs |
Flattened C ABI (per-method functions, no protobuf at call site) |
| rust | wasm |
_wasm.rs |
WASM exports via #[wasm_bindgen] |
| java | (default) | _ffi.java |
Java FFI host bindings |
| csharp | (default) | _ffi.cs |
C# FFI host bindings |
| python | (default) | _ffi.py + _lite.py |
Python 3.10+ transport-neutral client, FFI compatibility wrapper, and dependency-free messages |
| python | lite |
_lite.py |
Python dependency-free protobuf-lite messages only |
| typescript | (default) | _ffi.ts + _lite.ts |
TypeScript FFI client wrapper plus companion protobuf-lite message module when services are generated |
| typescript | lite |
_lite.ts |
TypeScript protobuf-lite message classes with binary parse/serialize and JSON output |
| swift | lite |
_lite.swift |
Swift protobuf-lite messages and FFI host bindings |
| c | (default) | _lite.h + _lite.c + _ffi.h + _ffi.c |
Complete dependency-free C messages, typed service implementation, direct-call functions, and plugin ABI |
| c | lite |
_lite.h + _lite.c |
Dependency-free fixed-width enums, messages, and protobuf codecs |
| c | native |
Same as C default | Deprecated compatibility alias for the complete C binding |
| c | activex |
_activex.h |
COM/ActiveX dispatch header |
The existing plugin-server outputs and default C binding export the service-specific ABI (Synurang_Invoke_<Service>(method, bytes, len, &out_len)). New mode=module outputs use the instance/call ABI exposed by Synurang_GetApi and require a module host.
Rust native and WASM modes generate per-method functions with flattened parameters (e.g., cache_put(store, store_len, key, key_len, value, value_len, ttl, cost)). The complete C binding also retains compatible flattened unary entry points, with _pb fallbacks for methods whose inputs contain repeated, oneof, or map fields, while adding typed service handlers and the standard raw-protobuf plugin ABI.
The normal C command is deliberately the short one:
protoc --synurang-ffi_out=. --synurang-ffi_opt=lang=c service.protoFor api/service.proto, this produces source-relative files:
api/service_lite.h
api/service_lite.c
api/service_ffi.h
api/service_ffi.c
Files without a selected service emit only the two lite files. Applications and service implementations include only service_ffi.h; it includes service_lite.h and <synurang/c_runtime.h>. Use mode=lite when only messages and enums are needed. mode=native remains accepted as a deprecated alias for the default output and no longer creates _ffi_native* or separate server headers.
Generated streaming handlers are event-driven: on_open, on_message, on_half_close, on_writable, on_cancel, and on_destroy. on_open may return per-stream state, and generated typed *_send, *_finish, and *_fail helpers handle serialization and completion. finish/fail terminate the whole RPC, reject later input, and preserve already queued responses before EOF/error. Callback message and stream pointers are borrowed; retain/release the stream when asynchronous work outlives a callback.
The generated *_register(&handlers, user_data) function uses the process-wide threaded runtime by default. For libuv or single-threaded WebAssembly, create a manual runtime, arrange for its wakeup callback to schedule work on the embedding loop, register with *_register_with_runtime(...), and call synurang_runtime_poll() without blocking. A wakeup can mean callback work or newly readable stream output, so the loop should also retry pending receives. In manual mode, use Synurang_Stream_TrySend/Synurang_Stream_TryRecv; receive status SYNURANG_PENDING means no result is ready yet, and SYNURANG_WOULD_BLOCK is handled after on_writable.
Before unloading a C service shared library, close its streams. Destroy an explicitly created runtime before unregistering the service. For the default runtime, unregister every service that uses it and then call synurang_runtime_shutdown_default() before dlclose. See Low-Level FFI API for the complete API and lifecycle.
Host loads plugin as .so/.dll via dlopen/LoadLibrary.
| Host (Parent) | Plugin | Unix | Windows | Status |
|---|---|---|---|---|
| Go | Go/C++/Rust | ✓ | Experimental | Stable |
| Dart/Flutter | Go | ✓ | Experimental | Stable |
| C++ | Go/C++/Rust | ✓ | Experimental | Experimental |
| Rust | Go/C++/Rust | ✓ | Experimental | Experimental |
| Java/Android | Go/C++/Rust | ✓ | ✓ | Experimental |
| C# (.NET) | Go/C++/Rust | ✓ | ✓ | Experimental |
| Python 3.10+ | Go/C++/Rust | ✓ | ✓ | Experimental |
Parent spawns child process with gRPC over IPC.
| Host (Parent) | Child | Unix | Windows | Status |
|---|---|---|---|---|
| Go | Go/C++/Rust | socketpair | named pipes | Stable |
| Dart | Go/C++/Rust | TCP loopback | TCP loopback | Stable |
| C++ | Go/C++/Rust | socketpair | TCP loopback | Experimental |
| Rust | Go/C++/Rust | socketpair | named pipes | Experimental |
| Java/Android | Go/C++/Rust | socketpair | TCP loopback | Experimental |
| C# (.NET) | Go/C++/Rust | socketpair | named pipes | Experimental |
Note: Java/Android uses socketpair on Unix (requires grpc-okhttp on classpath), TCP loopback otherwise. C# uses socketpair on Unix (with TCP loopback fallback), named pipes on Windows (with TCP loopback fallback). Dart uses TCP loopback on all platforms (dart-lang/sdk#46196). C++ uses TCP loopback on Windows only (grpc/grpc#13447). For TCP loopback, child must print
SYNURANG_PORT:<port>to stdout. For named pipes, child must printSYNURANG_PIPE:<pipename>to stdout.
FFI and TCP/UDS can run at the same time on the same server.
cfg := &service.Config{
EngineTcpPort: "50051", // TCP
EngineSocketPath: "/tmp/synurang.sock", // UDS
// FFI always available
}App uses FFI for performance. Debug via TCP with grpcurl, Postman, or IDE — no restart required.
Flutter handles UI, Go handles logic. Bidirectional communication supported.
┌──────────────────────┐ ┌──────────────────────┐
│ Flutter (UI/View) │ │ Go (Logic/Data) │
│ │ │ │
│ Widget Tree │ │ Business Logic │
│ │ │ │ │ │
│ Dart gRPC Client │──FFI────►│ gRPC Server │
│ Dart gRPC Server │◄──FFI────│ gRPC Client │ (optional)
└──────────────────────┘ └──────────────────────┘
- Dart → Go: Requests, queries, commands
- Go → Dart (optional): Push updates, UI state requests via reverse-FFI callbacks
Benefits:
- Desktop-first development: Develop on desktop, deploy to mobile unchanged
- API-first design:
.protodefines the contract between frontend and backend
Ship proprietary gRPC services as compiled .so/.dll binaries. Host loads at runtime via dlopen. Standard gRPC interface, no network overhead, no source exposure.
┌─────────────────────────────────────────┐
│ Host Process │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ Host App │ ◄──► │ Plugin (.so) │ │
│ │ │ gRPC │ [proprietary] │ │
│ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
Plugin side:
package main
import "C"
import pb "my-plugin/pkg/api"
type MyPlugin struct{}
func (s *MyPlugin) DoSomething(ctx context.Context, req *pb.Request) (*pb.Response, error) {
return &pb.Response{Result: "done"}, nil
}
func init() {
pb.RegisterMyServicePlugin(&MyPlugin{})
}
func main() {}Build:
CGO_ENABLED=1 go build -buildmode=c-shared -o plugin.so ./plugin/Host side:
plugin, _ := synurang.LoadPlugin("./plugin.so")
defer plugin.Close()
conn := synurang.NewPluginClientConn(plugin, "MyService")
client := pb.NewMyServiceClient(conn)
resp, _ := client.DoSomething(ctx, req)All RPC types supported including streaming.
Run gRPC services in a separate child process with a secure, private IPC channel.
Note: Parent process is always the gRPC client, child process is the gRPC server. The child can be written in any language (Go, C++, Rust, etc.).
- Unix/Linux/macOS/Android: Uses
socketpair(Go/C++/Rust/Java) or TCP loopback (Dart). - Windows: Uses Named Pipes (Go/Rust) or TCP loopback (C++/Dart/Java). See Process Mode table.
Parent Process (Go):
cmd := exec.Command("./child-process")
conn, err := synurang.StartProcess(ctx, cmd)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := pb.NewMyServiceClient(conn)
resp, _ := client.DoSomething(ctx, req)Parent can be Go, Dart, C++, Rust, Java, or C#. Child can be any language that supports gRPC.
Go Child:
ln, err := synurang.NewIPCListener()
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
pb.RegisterMyServiceServer(s, &server{})
s.Serve(ln)C++ Child (Unix - socketpair):
#include <grpcpp/grpcpp.h>
#include <grpcpp/server_posix.h>
#include <cstdlib>
int main() {
int fd = std::stoi(std::getenv("SYNURANG_IPC"));
grpc::ServerBuilder builder;
builder.RegisterService(&service);
auto server = builder.BuildAndStart();
// Add the already-connected socketpair fd to the server
grpc::AddInsecureChannelFromFd(server.get(), fd);
server->Wait();
}C++ Child (Windows - TCP loopback):
#include <grpcpp/grpcpp.h>
#include <iostream>
int main() {
grpc::ServerBuilder builder;
int selected_port = 0;
builder.AddListeningPort("127.0.0.1:0",
grpc::InsecureServerCredentials(),
&selected_port);
builder.RegisterService(&service);
auto server = builder.BuildAndStart();
// Report port to parent (required for Windows)
std::cout << "SYNURANG_PORT:" << selected_port << std::endl;
server->Wait();
}Rust Child (tonic - Unix socketpair):
use std::os::unix::io::FromRawFd;
use tonic::transport::Server;
use tokio::net::UnixStream;
#[tokio::main]
async fn main() {
let fd: i32 = std::env::var("SYNURANG_IPC").unwrap().parse().unwrap();
// socketpair gives us an already-connected stream, not a listener
let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd) };
std_stream.set_nonblocking(true).unwrap();
let stream = UnixStream::from_std(std_stream).unwrap();
Server::builder()
.add_service(MyServiceServer::new(MyService::default()))
.serve_with_incoming(futures::stream::once(async { Ok::<_, std::io::Error>(stream) }))
.await
.unwrap();
}StartProcessreturns a standard*grpc.ClientConnNewIPCListenerreturns a standardnet.Listener- Child process language is transparent to parent
- No network configuration required
| Direction | Zero-copy | Mechanism |
|---|---|---|
| Request (Dart → Go) | Yes | unsafe.Slice |
| Response (Go → Dart) | Yes | C.malloc + FreeFfiData |
| Streaming | Configurable | Safe (1 copy) or zero-copy |
C++ and Rust backends: zero-copy in both directions.
- Go 1.22+ (for the Go runtime and generated Go bindings)
- Flutter 3.19+ (if using Flutter)
- protoc (protobuf compiler)
- Rust 1.80+ and Cargo (only when building the generator from source)
- Python 3.10+ (only when using the Python host)
protoc-gen-synurang-ffi is now a Rust binary. The old Go implementation and
the temporary protoc-gen-synurang-ffi-rs name are gone; lang=go still
generates Go bindings.
For an installation that does not require Rust, download the archive for your
platform and SHA256SUMS from GitHub Releases,
verify it, extract it, and place protoc-gen-synurang-ffi (or the Windows
.exe) on PATH. Release archives are provided for Linux x86-64, Linux
AArch64, and Windows x86-64.
# Linux x86-64 example
VERSION=0.8.0
TARGET=x86_64-unknown-linux-musl
ASSET="protoc-gen-synurang-ffi-${VERSION}-${TARGET}.tar.gz"
BASE="https://github.com/ivere27/synurang/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/${ASSET}" -O "${BASE}/SHA256SUMS"
grep -F " ${ASSET}" SHA256SUMS | sha256sum --check
tar -xzf "${ASSET}"
mkdir -p ~/.local/bin
install -m 0755 "${ASSET%.tar.gz}/protoc-gen-synurang-ffi" ~/.local/bin/To build and install it with Cargo:
git clone https://github.com/ivere27/synurang.git
cargo install --locked --path synurang/cmd/protoc-gen-synurang-ffiInstall any language-specific protobuf plugins separately:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# For Dart
dart pub global activate protoc_plugin
# For C++ (Process Mode)
sudo apt-get install protobuf-compiler-grpcRelease binaries are cross-compiled inside the version-pinned
Dockerfile.codegen toolchain. CODEGEN_VERSION is required and may include
a leading v.
# Optional: build only the release image
make docker_codegen_image
# Build all default targets into dist/codegen/
make docker_codegen CODEGEN_VERSION=0.8.0
# Publishing requires a clean checkout whose pushed v0.8.0 tag points at HEAD
git tag -a v0.8.0 -m v0.8.0
git push origin v0.8.0
# Build and create a one-shot v0.8.0 release with all assets
make publish_github_codegen CODEGEN_VERSION=0.8.0
# `make publish_github ...` is an alias for the same generator release flow.The default bundles are static Linux x86-64/AArch64 .tar.gz archives and a
Windows x86-64 .zip, named
protoc-gen-synurang-ffi-<version>-<target>.<extension>, plus SHA256SUMS.
Override the whitespace-separated target list with CODEGEN_TARGETS, the
image with CODEGEN_DOCKER_IMAGE, or the destination repository with
CODEGEN_GITHUB_REPO. CODEGEN_DIST_DIR selects the host output directory.
Publishing requires an authenticated gh command, refuses dirty or mismatched
tagged source, and refuses to replace an existing release.
// api/service.proto
syntax = "proto3";
package api;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }# Go gRPC
protoc -Iapi --go_out=./pkg/api --go_opt=paths=source_relative \
--go-grpc_out=./pkg/api --go-grpc_opt=paths=source_relative \
service.proto
# Dart gRPC
protoc -Iapi --dart_out=grpc:lib/src/generated service.proto
# Synurang FFI bindings
protoc -Iapi --synurang-ffi_out=./pkg/api --synurang-ffi_opt=lang=go service.proto
protoc -Iapi --synurang-ffi_out=./lib/src/generated --synurang-ffi_opt=lang=dart service.proto
protoc -Iapi --synurang-ffi_out=./java/src --synurang-ffi_opt=lang=java,java_package=com.example.api service.proto
protoc -Iapi --synurang-ffi_out=./csharp/src --synurang-ffi_opt=lang=csharp,csharp_namespace=Example.Api service.proto
protoc -Iapi --synurang-ffi_out=./web/src/generated --synurang-ffi_opt=lang=typescript service.proto
# Python 3.10+ neutral client (generates lite messages and service bindings)
python3 -m pip install ./python
protoc -Iapi --synurang-ffi_out=./generated \
--synurang-ffi_opt=lang=python service.proto
# Rust native C ABI (flattened parameters, no protobuf at call site)
protoc -Iapi --synurang-ffi_out=./rust/src --synurang-ffi_opt=lang=rust,mode=native service.proto
# Rust WASM (wasm-bindgen exports)
protoc -Iapi --synurang-ffi_out=./wasm/src --synurang-ffi_opt=lang=rust,mode=wasm service.proto
# Complete C binding (lite messages + typed service/plugin implementation)
protoc -Iapi --synurang-ffi_out=./native --synurang-ffi_opt=lang=c service.proto
# C messages/enums only (no service functions)
protoc -Iapi --synurang-ffi_out=./native --synurang-ffi_opt=lang=c,mode=lite service.proto// pkg/service/greeter.go
package service
import (
"context"
"fmt"
pb "my-app/pkg/api"
)
type GreeterServer struct {
pb.UnimplementedGreeterServer
}
func (s *GreeterServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: fmt.Sprintf("Hello, %s!", req.Name)}, nil
}// cmd/server/main.go
package main
import "C"
import (
"github.com/ivere27/synurang/pkg/service"
_ "github.com/ivere27/synurang/src" // Exports FFI symbols
pb "my-app/pkg/api"
myservice "my-app/pkg/service"
"google.golang.org/grpc"
)
func init() {
service.RegisterGrpcServer(func(s *grpc.Server) {
pb.RegisterGreeterServer(s, &myservice.GreeterServer{})
})
}
func main() {}CGO_ENABLED=1 go build -buildmode=c-shared -o libmyapp.so cmd/server/main.goimport 'package:synurang/synurang.dart';
import 'src/generated/service_ffi.pb.dart';
import 'src/generated/service.pb.dart';
void main() async {
configureSynurang(libraryPath: './libmyapp.so');
await startGrpcServerAsync();
final response = await GreeterFfi.SayHello(HelloRequest(name: "World"));
print(response.message); // "Hello, World!"
}# FFI
dart run example/console_main.dart
# TCP
dart run example/console_main.dart --mode=tcp --port=18000
# UDS
dart run example/console_main.dart --mode=uds --socket=/tmp/synurang.sock
# grpcurl
grpcurl -plaintext localhost:18000 api.Greeter/SayHelloStructured FfiError(core.v1.Error) usage, plugin-side return examples, and end-to-end testing are documented in FFI-API.md.
- Full gRPC semantics: Unary, Server/Client/Bidi Streaming
- Drop-in replacement: Implements
grpc.ClientConnInterface - Zero-copy memory via
unsafe.Slice - Code generation:
protoc-gen-synurang-ffi - Thread-safe: Isolates and goroutines managed automatically
- Platforms: Android, iOS, macOS, Windows, Linux, WebAssembly
- Complete C binding: Dependency-free messages, typed unary/stream handlers, plugin ABI, and threaded/manual-poll runtime
C++: --synurang-ffi_opt=lang=cpp. Full support including all streaming types.
Rust: --synurang-ffi_opt=lang=rust. Full support including all streaming types.
Rust Native: --synurang-ffi_opt=lang=rust,mode=native. Generates per-method C ABI functions with flattened parameters — each scalar field becomes a direct function argument instead of going through serialized protobuf bytes. Methods with repeated, oneof, or map fields get a _pb fallback that accepts raw protobuf bytes. Error handling via thread-local last_error() (dlerror-style). Unary only.
Rust WASM: --synurang-ffi_opt=lang=rust,mode=wasm. Generates #[wasm_bindgen] exports for browser/JS execution. Same flattened signature convention as native mode with idiomatic Rust types (&str, &[u8]), plus stream exports for streaming RPCs (*_stream_open/send/recv/close_send/close). *_stream_recv returns [status, payload...] where status is 0=data, 1=eof, 2=error, 3=pending.
C: --synurang-ffi_opt=lang=c. Generates source-relative <schema>_lite.h/.c plus <schema>_ffi.h/.c for files with services. The single FFI header contains flattened unary callers, typed unary and event-driven streaming handler contracts, and the standard Synurang_Invoke_* / Synurang_Stream_*_Open plugin ABI. The generated source uses the shared <synurang/c_runtime.h> runtime. mode=native is a deprecated alias for this same output.
C Lite: --synurang-ffi_opt=lang=c,mode=lite. Generates only dependency-free <schema>_lite.h and <schema>_lite.c files for proto3 schemas, containing fixed-width enums, owned message values, explicit allocation helpers, and protobuf binary encode/decode/merge functions. It emits no service functions and requires no libprotobuf runtime. Numeric aliases are preserved; value-to-name uses the first declaration while parsing accepts every declared alias. Output keeps the input proto's relative directory, so imports remain unambiguous.
The C codec supplies google.protobuf.Empty, google.protobuf.Timestamp, and google.protobuf.Duration itself, so schemas using them need no protobuf runtime. Any other google.protobuf.* type is rejected up front rather than emitting C that will not compile. Nested decoding uses a shared recursion limit, including inside these well-known messages.
C enum constant style: --synurang-ffi_opt=lang=c,enum_names=short. A C enum constant is its prefix plus the full proto value name, so protobuf's convention of prefixing values with the enum name repeats it: enum DataType with value DATA_TYPE_F32 becomes EXAMPLE_V1_DATA_TYPE_DATA_TYPE_F32. enum_names=short drops the repeated segment — EXAMPLE_V1_DATA_TYPE_F32 — matching protobuf-c and hand-written C APIs. It is opt-in because it renames every constant. Only short and qualified (the default) are accepted, and the option affects C output only.
ActiveX: --synurang-ffi_opt=lang=c,mode=activex. Generates a Windows COM/ActiveX dispatch header with DISPID constants, name lookup table, and re-includable dispatch macros. Driven by (synurang.v1.activex_service) service option in .proto files.
Java/Android: --synurang-ffi_opt=lang=java. Full support including all streaming types via JNI.
C# (.NET Framework 4.0+ / .NET Core 3.1+): --synurang-ffi_opt=lang=csharp. Full support including all streaming types. Pure managed interop — no native bridge required.
TypeScript: --synurang-ffi_opt=lang=typescript. Generates lightweight TypeScript schema constants plus service FFI client wrappers backed by a PluginHost abstraction. For proto files with generated services, the same invocation also emits the companion _lite.ts module used for fromBinary()/parseFrom(), toBinary()/toByteArray(), and toJson() helpers. --synurang-ffi_opt=lang=typescript,mode=lite generates only the protobuf-lite message module.
Message classes share binary/JSON conversion methods within each generated module. Each class retains its own fields, constructor defaults and concrete decoder return type. Static decoders also work as callbacks, for example payloads.map(Response.fromBinary), including in minified bundles.
The lite module emits compact field declarations and expands them once when the module loads. Message.fields retains the complete metadata, and MessageFields derives its field numbers from that same declaration. Enums and oneof cases use constant objects with shared numeric reverse lookup, plus TypeScript value types. Use Status.READY as a value, Status as the enum type, and typeof Status.READY as a single-member type. These helpers require neither an external runtime nor dynamic code evaluation; message initialization and serialization keep their existing execution paths.
TypeScript gRPC (@grpc/grpc-js): --synurang-ffi_opt=lang=typescript,grpc=js also emits <schema>_grpc.ts with grpc-js service definitions (GreeterService), server interfaces (GreeterServer) and clients (GreeterClient) for the lite messages, plus one shared synurang_grpc.ts transport. The clients are ordinary grpc-js clients: the same unary, server-, client- and bidi-streaming calls with metadata/options overloads, returning ClientUnaryCall/stream objects and failing with ServiceError. To run a client over FFI, pass new PluginChannel(host, GreeterService) as channelOverride. Like Go's NewPluginClientConn, it implements the transport interface itself, so interceptors, deadlines, waitForReady and call.cancel() behave as they do over the network. Clients from other grpc-js generators work with it too. This output requires Node.js; _ffi.ts and _lite.ts stay dependency-free for browsers, and IPC is not yet available from TypeScript.
The host bridges to the plugin ABI (a native addon, koffi, a wasm module, ...): invoke(serviceName, methodName, data), and for streaming methods openStream(serviceName, methodName) returning send/recv/closeSend/close. Each may return a value or a promise. For an error payload, throw FfiError.fromPayload(payload): the call fails with code set to grpc_code (UNKNOWN when unset), details set to the message, and the core.v1.Error in the standard grpc-status-details-bin trailer. FfiError.fromStatus(err) decodes that trailer over FFI, and also over the network when the server attaches the error with status.WithDetails. The plugin ABI carries no metadata, so outgoing metadata is ignored and responses carry none. test/typescript/grpc_plugin_e2e.ts implements a koffi host and runs one client against both the Go plugin and a Go gRPC server.
Python 3.10+: --synurang-ffi_opt=lang=python (or lang=py). Generates dependency-free _lite.py protobuf message classes and a transport-neutral _ffi.py service client for proto3 schemas. The generated ServiceClient accepts either FfiTransport for an in-process shared library or the optional synchronous GrpcTransport for a remote server; the existing ServiceFfi(PluginHost) form remains available. All four RPC cardinalities are supported. mode=lite emits only messages. No google.protobuf runtime or protoc --python_out step is needed. Python 2 is not supported. Python does not currently provide a plugin server or process host.
| Target | Plugin (FFI) | CallInvoker (gRPC) | Process (TCP) | Process (named pipe) | Process (socketpair) |
|---|---|---|---|---|---|
| .NET Framework 4.0+ | Full | — | — | — | — |
| .NET Core 3.1 | Full | Full | Full | — | — |
| .NET 5.0+ | Full | Full | Full | Full (Windows) | Full (Unix) |
- .NET Framework 4.0 — Plugin/FFI mode only. Uses
kernel32!LoadLibrary/GetProcAddressfor native loading. Windows XP compatible. - .NET Core 3.1 — Full plugin + gRPC support. Uses
NativeLibraryfor native loading. TCP loopback on all platforms. - .NET 5.0+ — All features. Named pipes on Windows via
SocketsHttpHandler.ConnectCallback. Socketpair on Unix.
// Start/stop the embedded native core
await startGrpcServerAsync(token: 'xxx', cachePath: '/path/to/cache');
await stopGrpcServerAsync();
// Reusable options object (recommended — also required for recoverCoreAsync)
final opts = StartGrpcServerOptions(
token: 'xxx',
cachePath: '/path/to/cache',
);
await startGrpcServerWithOptionsAsync(opts);The default Dart-side wait for any FFI call is 30 seconds. A timeout marks the
core unhealthy and cancels every other in-flight request. For calls that may
legitimately exceed 30s (heavy queries, batch ops, blob uploads), pass an
explicit timeout:. The backend enforces the deadline first; Dart waits
timeout + 5s grace so callers receive a clean DEADLINE_EXCEEDED rather than
a CoreTimeoutException.
await invokeBackendAsync(method, data, timeout: Duration(minutes: 5));
// gRPC clients flow through CallOptions.timeout automatically
final stub = MyServiceClient(channel,
options: CallOptions(timeout: Duration(minutes: 5)));If the native core hangs (e.g., a Rust deadlock holding a global lock),
recoverCoreAsync performs a Dart-side reset plus native Stop/Start
without reloading the shared library.
CoreState |
meaning | accepts RPCs? |
|---|---|---|
healthy |
normal operation | yes |
unhealthy |
a request timed out; recovery not yet attempted | no |
recovering |
recovery is in flight | no |
restartRequired |
recovery itself timed out — native lib likely deadlocked | never (sticky) |
try {
await client.someRpc(...);
} on CoreTimeoutException {
final result = await recoverCoreAsync(
previousStartOptions: opts,
stopTimeout: Duration(seconds: 2),
startTimeout: Duration(seconds: 5),
);
if (result.recovered) {
// Re-create gRPC channels / providers as needed
} else if (result.restartRequired) {
// Native lib is wedged — process restart is the only remedy
showRestartRequiredUi();
} else {
// result.failed — surface error, retry later if appropriate
}
}
// Inspect state directly
final state = getCoreState();recoverCoreAsync is idempotent under concurrent callers: a second call while
one is in flight returns the same future. The caller owns
previousStartOptions — synurang does not cache them. There is no automatic
RPC retry: timed-out FFI calls may still complete later natively, and retrying
non-idempotent operations is the caller's choice.
resetCoreState() kills helper isolates and fails pending requests/streams but
does not unload or reload the shared library. Use it for app-restart-style
cleanup, not for recovery from a native hang.
| Exception | When | gRPC mapping |
|---|---|---|
CoreTimeoutException |
FFI request exceeded its timeout | unavailable |
CoreUnavailableException |
RPC issued while unhealthy / recovering |
unavailable |
CoreRestartRequiredException |
core is in restartRequired (or recovery timed out) |
unavailable |
CoreShutdownException |
core was reset under the caller | unavailable |
FfiClientChannel automatically maps the above to GrpcError.unavailable; any
other error → internal.
await cacheGetRaw(store, key);
await cachePutRaw(store, key, data, ttl);Note:
recoverCoreAsynconly applies to the embedded core (the mode activated bystartGrpcServerAsync— what the Transports table calls "FFI (source)"). In plugin mode (registerPlugin) there are noStart/Stopsymbols to call, so recovery always returnsrestartRequired— a process restart is required for native-side hangs.
// FFI client connection (for embeddable libraries)
conn := api.NewMyServiceFfiClientConn(server)
client := pb.NewMyServiceClient(conn)
// Plugin loader
plugin, _ := synurang.LoadPlugin("./plugin.so")
conn := synurang.NewPluginClientConn(plugin, "MyService")Four Maven packages — two for Android (AAR), two for desktop (JAR with embedded natives):
| Package | Platform | Contents |
|---|---|---|
synurang-android |
Android | Core: PluginHost, ProcessHost, JNI bridge (AAR) |
synurang-android-grpc |
Android | SynurangChannel, SynurangClientCall (AAR) |
synurang-desktop |
Linux/macOS/Windows | Core + embedded JNI natives for 5 platforms (JAR) |
synurang-desktop-grpc |
Linux/macOS/Windows | SynurangChannel, SynurangClientCall (JAR) |
The desktop JAR auto-extracts the native library at runtime — no -Djava.library.path needed.
// Android (build.gradle)
implementation 'io.github.ivere27:synurang-android:0.8.0'
implementation 'io.github.ivere27:synurang-android-grpc:0.8.0' // optional, for gRPC channel
implementation 'io.grpc:grpc-api:1.60.0' // required if using -grpc
// Desktop (build.gradle)
implementation 'io.github.ivere27:synurang-desktop:0.8.0'
implementation 'io.github.ivere27:synurang-desktop-grpc:0.8.0' // optional, for gRPC channel
implementation 'io.grpc:grpc-api:1.60.0' // required if using -grpc// Low-level API (core only, no gRPC dependency)
PluginHost plugin = PluginHost.load("./libplugin.so");
byte[] resp = plugin.invoke("MyService", "/pkg.MyService/Method", requestBytes);
PluginStream stream = plugin.openStream("MyService", "/pkg.MyService/StreamMethod");
// Process mode — socketpair IPC on Unix, TCP loopback fallback on Windows
ProcessHost proc = ProcessHost.start("./child-process");
ManagedChannel channel = (ManagedChannel) proc.channel(); // socketpair, no TCP
// Plugin mode — drop-in grpc-java Channel (requires synurang-android-grpc)
Channel channel = SynurangChannel.create(plugin, "MyService");
MyServiceGrpc.MyServiceBlockingStub stub = MyServiceGrpc.newBlockingStub(channel);
HelloReply reply = stub.sayHello(request); // goes through FFI, not TCP
plugin.close();// Plugin mode — drop-in gRPC CallInvoker (recommended)
using var plugin = PluginHost.Load("./libplugin.so");
var invoker = new SynurangCallInvoker(plugin, "MyService");
// Standard protoc-gen-grpc-csharp stubs — zero custom codegen
var client = new MyService.MyServiceClient(invoker);
var reply = client.SayHello(request); // goes through FFI, not TCP
// Process mode — socketpair IPC on Unix, TCP loopback on Windows
using var proc = ProcessHost.Start("./child-process");
var channel = proc.Channel; // GrpcChannel over socketpair
// Low-level API (when you don't want grpc-dotnet dependency)
byte[] resp = plugin.Invoke("MyService", "/pkg.MyService/Method", requestBytes);
using var stream = plugin.OpenStream("MyService", "/pkg.MyService/StreamMethod");The Kotlin demo in example/java/android/ showcases:
- Go Backend: Standard gRPC services running as a plugin or child process.
- Rust Media Pipeline: Zero-copy YUV frame processing via FFI.
- Native IPC: High-performance
socketpairtransport (no TCP overhead).
To run the demo:
make run_android_javasynurang/
├── cmd/
│ ├── server/main.go # FFI entry point example
│ └── protoc-gen-synurang-ffi/ # Rust generator (Go/Dart/C++/Rust/Java/C#/TS/C/Swift/Python output)
├── pkg/
│ ├── synurang/ # Runtime library
│ │ ├── synurang.go # FfiClientConn
│ │ ├── plugin.go # Plugin loader
│ │ └── plugin_conn.go # PluginClientConn
│ └── service/ # Server implementation
├── lib/ # Dart package
│ ├── synurang.dart # Main entry point
│ └── src/generated/ # Generated proto
├── java/ # Java host library (multi-module Gradle)
│ ├── core/ # Core module: PluginHost, ProcessHost, JNI bridge (zero dependencies)
│ │ ├── src/main/java/ # PluginHost, PluginStream, BidiStream, SynurangJni, etc.
│ │ └── src/main/c/ # JNI native layer
│ └── grpc/ # gRPC module: SynurangChannel, SynurangClientCall (requires grpc-api)
├── csharp/ # C# host library (pure managed)
│ └── Synurang/ # PluginHost, SynurangCallInvoker, ProcessHost
├── python/ # Python 3.10+ FFI + optional remote gRPC transports
├── example/ # Working examples
│ ├── go/ # Go examples
│ │ ├── service/ # Shared service logic
│ │ ├── process/ # Process mode entry
│ │ └── plugin/ # Plugin mode entry
│ ├── cpp/ # C++ examples
│ │ ├── service/ # Shared service logic
│ │ ├── process/ # Process mode entry
│ │ └── plugin/ # Plugin mode entry
│ ├── rust/ # Rust examples
│ │ ├── service/ # Shared service logic
│ │ ├── process/ # Process mode entry
│ │ └── plugin/ # Plugin mode entry
│ └── java/ # Java/Android examples
│ ├── android/ # Kotlin Android app
│ └── rust-plugin/ # Rust media plugin
└── test/ # Test suites
All 7 low-level hosts (Go, Dart, C++, Rust, Java, C#, and Python) can call
plugins directly without any gRPC library — just raw protobuf bytes via
invoke() / openStream(). See FFI-API.md for details.
make test_ffi # Run all FFI API tests (Go, C++, Rust, Java, Dart, C#, Python)Engine for Synura, a content viewer app.
MIT