synurang 0.3.0 copy "synurang: ^0.3.0" to clipboard
synurang: ^0.3.0 copied to clipboard

Flutter FFI + gRPC bridge for bidirectional Go/Dart communication

Synurang #

gRPC over FFI and IPC

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 := synurang.NewFfiClientConn(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"})

Use Cases #

  • 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 .so binaries. 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) spawns child in any language.
  • Debugging: Enable TCP/UDS alongside FFI/IPC. Use grpcurl or Postman while app runs.

Quick start #

# Install the code generator
go install github.com/ivere27/synurang/cmd/protoc-gen-synurang-ffi@latest

# Generate FFI bindings from your .proto
protoc --synurang-ffi_out=. --synurang-ffi_opt=lang=go service.proto

See Installation & Quick Start for the full setup.


How It Works #

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.

Transports #

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.


Language Support #

Plugin Mode (FFI) #

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

Process Mode (IPC) #

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

Note: Java/Android uses socketpair on Unix (requires grpc-okhttp on classpath), TCP loopback otherwise. 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.


Simultaneous Transports #

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 + Go Architecture #

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: .proto defines the contract between frontend and backend

Plugin System (In-Process Microservices) #

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.


Process Mode (Parent-Child IPC) #

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)

Child Process Examples #

Parent can be Go, Dart, C++, or Rust. 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();
}

Benefits #

  • StartProcess returns a standard *grpc.ClientConn
  • NewIPCListener returns a standard net.Listener
  • Child process language is transparent to parent
  • No network configuration required

Memory Model #

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.


📦 Installation & Quick Start #

Prerequisites #

  • Go 1.22+
  • Flutter 3.19+ (if using Flutter)
  • protoc (protobuf compiler)
# Install protoc plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/ivere27/synurang/cmd/protoc-gen-synurang-ffi@latest

# For Dart
dart pub global activate protoc_plugin

# For C++ (Process Mode)
sudo apt-get install protobuf-compiler-grpc

Step 1: Define API #

// api/service.proto
syntax = "proto3";
package api;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

Step 2: Generate Code #

# 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

Step 3: Implement Server #

// 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
}

Step 4: FFI Entry Point #

// 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() {}

Step 5: Build Shared Library #

CGO_ENABLED=1 go build -buildmode=c-shared -o libmyapp.so cmd/server/main.go

Step 6: Call from Dart #

import '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!"
}

Testing Transports #

# 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/SayHello

Features #

  • 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

Experimental #

C++: --synurang-ffi_opt=lang=cpp. Full support including all streaming types.

Rust: --synurang-ffi_opt=lang=rust. Full support including all streaming types.

Java/Android: --synurang-ffi_opt=lang=java. Full support including all streaming types via JNI.


API Reference #

Dart #

// Start/stop the embedded server
await startGrpcServerAsync();
await stopGrpcServerAsync();

// Cache API (Go-managed SQLite)
await cacheGetRaw(store, key);
await cachePutRaw(store, key, data, ttl);

Go #

// FFI client connection (for embeddable libraries)
conn := api.NewFfiClientConn(server)
client := pb.NewMyServiceClient(conn)

// Plugin loader
plugin, _ := synurang.LoadPlugin("./plugin.so")
conn := synurang.NewPluginClientConn(plugin, "MyService")

Java/Android #

// Plugin mode — drop-in grpc-java Channel (recommended)
PluginHost plugin = PluginHost.load("./libplugin.so");
Channel channel = SynurangChannel.create(plugin, "MyService");

// Standard protoc-gen-grpc-java stubs — zero custom codegen
MyServiceGrpc.MyServiceBlockingStub stub = MyServiceGrpc.newBlockingStub(channel);
HelloReply reply = stub.sayHello(request);  // goes through FFI, not TCP

// 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

// Low-level API (when you don't want grpc-java dependency)
byte[] resp = plugin.invoke("MyService", "/pkg.MyService/Method", requestBytes);
PluginStream stream = plugin.openStream("MyService", "/pkg.MyService/StreamMethod");

plugin.close();

Android Example #

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 socketpair transport (no TCP overhead).

To run the demo:

make run_android_java

Project Structure #

synurang/
├── cmd/
│   ├── server/main.go                # FFI entry point example
│   └── protoc-gen-synurang-ffi/      # Code generator
├── 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
│   ├── src/main/java/                # PluginHost, PluginStream, etc.
│   └── src/main/c/                   # JNI native layer
├── 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

Low-Level FFI API (No gRPC Dependency) #

All 5 host languages 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)

Engine for Synura, a content viewer app.

License #

MIT

0
likes
140
points
649
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter FFI + gRPC bridge for bidirectional Go/Dart communication

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

MIT (license)

Dependencies

ffi, fixnum, flutter, grpc, protobuf

More

Packages that depend on synurang

Packages that implement synurang