Synurang

"The gRPC-over-FFI bridge for Go and Flutter"

Synurang is a high-performance bridge connecting Flutter and Go using gRPC over FFI.

It decouples the Transport Layer from the Application Layer, enabling hybrid apps where the UI lives in Flutter and the business logic runs natively in Goβ€”without the overhead of standard platform channels.

Note: This project serves as the underlying engine for Synura, a content viewer application. While Synura is the product, Synurang is the reusable infrastructure.

Synura:


⚑ Why Synurang?

Stop choosing between Performance and Productivity.

Synurang combines the native speed of Go with the reactive beauty of Flutter, without the fragility of Platform Channels or the overhead of running a local HTTP server.

Feature Platform Channels Localhost HTTP (Sidecar) Synurang (FFI)
Transport OS Messaging TCP/IP Loopback Direct Memory
Typing Loose (Map/JSON) Loose (JSON) Strict (Protobuf)
Performance Slow Medium Native Speed
Streaming Difficult Chunked Native gRPC Streams
Bidirectional Complex WebSocket Native gRPC Bidi

πŸ— Architecture & Philosophy

Synurang enforces a strict architectural separation:

  1. VIEW (Flutter): Responsible only for rendering the UI and handling user input. It should contain zero complex business logic.
  2. LOGIC (Go): The "Brain" of the application. Handles database access (SQLite), complex parsing, networking, and state calculation.
  3. TRANSPORT (Synurang): The spinal cord connecting the two. It moves data using Protobuf messages over direct FFI calls, avoiding the overhead of HTTP/TCP or Platform Channels.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Flutter (UI/View)   β”‚          β”‚    Go (Logic/Data)   β”‚
β”‚                      β”‚          β”‚                      β”‚
β”‚  [ Widget Tree ]     β”‚          β”‚  [ Business Logic ]  β”‚
β”‚         β”‚            β”‚          β”‚           β”‚          β”‚
β”‚  [ Dart Client ]     β”‚          β”‚   [ gRPC Server ]    β”‚
β”‚  [ gRPC Server ] ◄───┼──FFI─────┼───► [ gRPC Client ]  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                                   β”‚
          β”‚         Synurang Bridge           β”‚
          β”‚     (Direct FFI / Protobuf)       β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Important

In-Process by Default: The Go "backend" runs inside the same process as your Flutter app via FFIβ€”compiled into a shared library (.so/.dylib). The term "gRPC" refers to the protocol semantics (typed messages, streaming), not network transport. However, Synurang also supports UDS (Unix Domain Socket for IPC) and TCP (for remote debugging or distributed setups). See UDS & TCP "Side Doors" below.

πŸ–₯️ Desktop-First Development

This architecture empowers a Desktop-First workflow. You can develop and test the entire application (Logic + UI) on Linux, macOS, or Windows. Since the Go backend runs identically on desktop and mobile:

  • Iterate Fast: Develop core features on your desktop without slow emulator/device builds.
  • Seamless Porting: Deploy to Android/iOS with confidence, knowing the logic layer is identical.
  • Hybrid Debugging: Run the Logic layer on your desktop while connecting a mobile UI to it (via TCP) for granular debugging.

πŸ“œ API-First Design

By defining your data contracts in .proto files before writing a single line of code, Synurang enforces a disciplined API-First workflow.

  • Clear Contracts: The Protobuf definition is the single source of truth for both frontend and backend developers.
  • Type Safety: Generated Dart and Go code ensures that your UI and Logic always speak the same language.
  • Scenario-Based Design: You can design and mock your APIs for specific user stories (e.g., "Offline Mode", "Video Streaming") without worrying about implementation details initially.

πŸ§ͺ Experimental C++ Support

Synurang includes experimental support for C++ backends.

  • Code Generation: The protoc-gen-synurang-ffi plugin supports --lang=cpp to generate C++ dispatchers.
  • Runtime: A C++ runtime header (synurang.hpp) provides the interface for implementing services.
  • Note: C++ support requires a manual build setup (CMake) for grpc++ dependencies.

πŸ¦€ Experimental Rust Support

Synurang also includes experimental support for Rust backends.

  • Code Generation: The plugin cmd/protoc-gen-synurang-ffi generates the client-side glue code that calls the backend via FFI.
  • Note: Rust support requires a manual build setup (Cargo) for dependencies.

πŸ’‘ Common Use Cases

1. High-Performance Data Processing

Offload heavy computational tasks like image processing, cryptography, or complex data analysis to Go. Go's efficient memory model and goroutines provide superior performance compared to Dart isolates for raw compute.

2. Embedded Database Management

Run a robust database engine (like SQLite, DuckDB, or specialized Go-based DBs) entirely within the Go runtime. Your Flutter UI can query data via strictly typed gRPC methods, while Go handles the complex persistence logic, migrations, and concurrency.

3. System-Level Integration

Use Go's cgo capabilities to interface with legacy C/C++ libraries or OS-specific APIs that might be cumbersome to access directly from Dart. Wrap these interactions in a clean gRPC API for your Flutter frontend.


πŸš€ Key Features

  • ⚑ Direct Memory Access: Request payloads use zero-copy via unsafe.Slice; responses are copied once via C malloc. See Memory Model.
  • πŸ“‘ Full gRPC Support: Supports Unary, Server Streaming, Client Streaming, and Bidirectional Streaming RPCs.
  • πŸ”„ Bidirectional Communication:
    • Flutter -> Go: Standard client calls.
    • Go -> Flutter: Dart acts as a gRPC Server via reverse-FFI callbacks, allowing Go to push updates or request UI state.
  • 🧡 Thread Safety: Automatically manages Dart Isolates and Go Goroutines to ensure the UI thread never blocks.
  • πŸ›  Code Generation: Includes protoc-gen-synurang-ffi to auto-generate type-safe bindings from your .proto files.
  • πŸ’Ύ Built-in Caching: High-performance L2 cache implementation using SQLite (via Go) exposed directly to Dart.

Supported RPC Types

Type Go Method Generated Dart Client FFI Streaming
Unary Bar() GoGreeterServiceFfi.Bar() βœ… Future<T>
Server Stream BarServerStream() GoGreeterServiceFfi.BarServerStream() βœ… Stream<T>
Client Stream BarClientStream() GoGreeterServiceFfi.BarClientStream() βœ… Future<T>(Stream)
Bidi Stream BarBidiStream() GoGreeterServiceFfi.BarBidiStream() βœ… Stream<T>(Stream)

🧠 Memory Model

Synurang's FFI layer minimizes copies where possible. The memory behavior varies by backend language:

Go Backend

The protoc plugin generates two invoke functions for flexibility:

Function Returns FFI Mode TCP/UDS Mode
Invoke(...) []byte βœ… Works (1 copy via C.CBytes) βœ… Works
InvokeFfi(...) unsafe.Pointer βœ… Zero-copy ❌ Not applicable

Memory behavior:

Operation Direction Zero-Copy? Mechanism
Unary Request Dart β†’ Go βœ… Yes unsafe.Slice – Go reads Dart's memory directly
Unary Response Go β†’ Dart βœ… Yes C.malloc + direct serialize via InvokeFfi
Cache Put Dart β†’ Go βœ… Yes unsafe.Slice – synchronous write, no copy
Cache Get Go β†’ Dart ❌ No C.CBytes – malloc + copy
Stream Data (in) Dart β†’ Go ⚠️ Option C.GoBytes (safe) or unsafe.Slice (zero-copy)
Stream Data (out) Go β†’ Dart ⚠️ Option SendFromStream (1 copy) or SendFromStreamFfi (zero-copy)

Note

Stream Input Zero-Copy: By default, SendStreamData in cmd/server/main.go uses C.GoBytes (safe, 1 copy). To enable zero-copy, replace it with unsafe.Sliceβ€”but only if you guarantee the data is not accessed after the function returns. See the commented code in SendStreamData for the zero-copy option.

Tip

Both work for FFI! Use InvokeFfi for zero-copy performance with large payloads. Use Invoke + C.CBytes when you prefer simpler code (the copy overhead is negligible for small messages). See example/cmd/server/main.go for both patterns.

Summary: Use InvokeFfi for maximum performance. Use Invoke for code reuse between FFI and TCP/UDS modes.

Warning

Trade-off: Zero-copy eliminates GC overhead for high performance but requires strict manual memory management, as any violation causes immediate application crashes.

Rust Backend (Experimental)

Operation Direction Zero-Copy? Mechanism
Unary Request Dart β†’ Rust βœ… Yes slice::from_raw_parts – view of Dart's memory
Unary Response Rust β†’ Dart βœ… Yes Vec::leak() – ownership transferred, no copy

Rust achieves full zero-copy because it has manual memory control. The Vec is leaked (not freed), and Dart calls FreeFfiData which reconstructs and drops the Vec properly.

C++ Backend (Experimental)

Operation Direction Zero-Copy? Mechanism
Unary Request Dart β†’ C++ βœ… Yes Direct pointer access
Unary Response C++ β†’ Dart βœ… Yes Returns malloc'd pointer directly

C++ allocates response data directly in C heap, so the FFI layer just passes the pointer. Dart frees it via FreeFfiData.


πŸ”Œ UDS & TCP "Side Doors"

While the primary communication happens via Direct FFI, synurang can also expose standard network interfaces for specific use cases:

1. Unix Domain Socket (UDS)

Use Case: Local IPC & Extension Isolation Useful when running unstable code (like third-party extensions) in separate processes. These external processes can communicate with the main Go engine via UDS without crashing the main application if they fail.

2. TCP Server

Use Case: Independent Debugging (UI & Logic) Enables the Dart (UI server) and Golang (Logic server) to be debugged independently.

  • Debug Logic: Connect to the Go backend via TCP (e.g., via adb forward) to test business logic in isolation using tools like grpcurl or Postman.
  • Debug UI: Verify the View layer by mocking backend responses or triggering UI events remotely, without needing the full backend state.

Testing UDS/TCP Transports

Dart Console Example (spawns Go server process in UDS/TCP modes):

# FFI mode (default) - embedded Go via shared library
make run_console_example

# TCP mode - spawns separate Go server process
dart run example/console_main.dart --mode=tcp --port=18000

# UDS mode - spawns separate Go server process  
dart run example/console_main.dart --mode=uds --socket=/tmp/synurang.sock

Go CLI Client (for testing from command line):

# Test Go server via TCP
go run example/cmd/client/main.go --target=go --transport=tcp --addr=localhost:18000

# Test Go server via UDS
go run example/cmd/client/main.go --target=go --transport=uds --socket=/tmp/synurang.sock

# Test Flutter server via TCP
go run example/cmd/client/main.go --target=flutter --transport=tcp --addr=localhost:10050

# Test Flutter server via UDS
go run example/cmd/client/main.go --target=flutter --transport=uds --socket=/tmp/flutter_view.sock

Flutter GUI Example (interactive transport testing):

# Build shared libraries and run Flutter app
make run_flutter_example

Use the toggle buttons in the header (Go UDS, Go TCP, Flutter UDS, Flutter TCP) to switch transports. The "ALL (Mixed)" button runs comprehensive tests across all transport combinations.


πŸ“¦ Installation & Quick Start

Synurang is a bridge library. You do not run it directly; instead, you integrate it into your own Go and Flutter project.

Prerequisites

  • Go (1.22+)
  • Flutter (3.19+)
  • Protobuf Compiler (protoc)
    • Linux: sudo apt install protobuf-compiler
    • Mac: brew install protobuf
  • Protoc Plugins:
    # Go plugins
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
        
    # Synurang plugin (for FFI bindings)
    go install github.com/ivere27/synurang/cmd/protoc-gen-synurang-ffi@latest
    
    # Dart plugin
    dart pub global activate protoc_plugin
    
    # Add Go bin to PATH (if not already added)
    export PATH=$PATH:$(go env GOPATH)/bin
    

Step 1: Project Setup

Add synurang to your pubspec.yaml:

dependencies:
  synurang: ^0.1.6

Add synurang to your Go module:

go get github.com/ivere27/synurang

Step 2: Define Protocol

Create a .proto file (e.g., api/service.proto) to define your API.

syntax = "proto3";
package api;

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

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Step 3: Generate Code

Generate the Go and Dart code, including the special FFI bindings.

Tip: If you haven't installed the protoc-gen-synurang-ffi plugin yet, you can build it from source if you have the repository checked out:

make build_plugin

Or ensure it is in your PATH.

# Create output directories
mkdir -p pkg/api lib/src/generated

# 1. Generate Go gRPC code
protoc -Iapi --go_out=./pkg/api --go_opt=paths=source_relative \
    --go-grpc_out=./pkg/api --go-grpc_opt=paths=source_relative \
    service.proto

# 2. Generate Dart gRPC code
protoc -Iapi --dart_out=grpc:lib/src/generated service.proto

# 3. Generate Synurang FFI Glue Code (Go)
protoc -Iapi --plugin=protoc-gen-synurang-ffi=$(which protoc-gen-synurang-ffi) \
    --synurang-ffi_out=./pkg/api --synurang-ffi_opt=lang=go \
    service.proto

# 4. Generate Synurang FFI Glue Code (Dart)
protoc -Iapi --plugin=protoc-gen-synurang-ffi=$(which protoc-gen-synurang-ffi) \
    --synurang-ffi_out=./lib/src/generated \
    --synurang-ffi_opt=lang=dart,dart_package=my_app \
    service.proto

Note

Package Imports: The dart_package option is recommended. It ensures generated files use package:my_app/... imports instead of relative paths (e.g., ../). If omitted, relative imports are used by default.

Note

Well-Known Types: The protoc-gen-synurang-ffi plugin automatically maps google/protobuf/* imports to package:protobuf/well_known_types/*. This avoids duplicating well-known types locally and ensures compatibility with the protobuf package. If you use other libraries that generate their own google/protobuf/*.pb.dart files, you may encounter type conflicts.

Step 4: Implement Go Service

Implement the server interface in Go.

// 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 5: Create Go Entry Point

Create a main.go file (e.g., cmd/server/main.go). This is the most critical step, as it defines the shared library entry point.

You must import github.com/ivere27/synurang/src to export the necessary C symbols.

package main

import (
	"C" // Required for CGO
	"context"
	
	"github.com/ivere27/synurang/pkg/service"
	// IMPORT THIS TO EXPORT FFI SYMBOLS
	_ "github.com/ivere27/synurang/src" 
	
	pb "my-app/pkg/api"
	myservice "my-app/pkg/service"
	"google.golang.org/grpc"
)

// Init is called by Synurang when the library is loaded
func init() {
	service.RegisterGrpcServer(func(s *grpc.Server) {
		pb.RegisterGreeterServer(s, &myservice.GreeterServer{})
	})
}

func main() {
    // Empty main is required for buildmode=c-shared
}

Step 6: Build Shared Library

Compile your Go code into a C-shared library.

Linux:

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

Android: Requires the Android NDK and a configured environment (see the synurang makefile for reference).

Step 7: Flutter Integration

  1. Place the Library:

    • Linux: Place libmyapp.so in a location accessible to the runner (or use LD_LIBRARY_PATH during dev).
    • Android: Place .so files in android/app/src/main/jniLibs/<arch>/.
  2. Initialize & Call:

import 'package:synurang/synurang.dart';
import 'src/generated/service_ffi.pb.dart'; // Generated FFI client
import 'src/generated/service.pb.dart';     // Generated messages

void main() async {
  // 1. Load your shared library
  configureSynurang(libraryName: 'myapp', libraryPath: './libmyapp.so');
  
  // 2. Start the embedded server
  await startGrpcServerAsync();

  // 3. Make a call!
  final response = await GreeterFfi.SayHello(HelloRequest(name: "World"));
  print(response.message); // "Hello, World!"
}

πŸ›  Development Workflow

πŸ“š API Reference

Dart API (package:synurang/synurang.dart)

Server Management

  • startGrpcServerAsync({ ... }): Starts the embedded Go server on a background isolate.
    • storagePath: Path to persist data.
    • enableCache: Enable the internal SQLite cache.
  • stopGrpcServerAsync(): Gracefully stops the server.
  • invokeBackendAsync(method, data): Raw FFI invocation (mostly used by generated code).

Cache API

Direct access to the Go-managed SQLite cache.

  • cacheGetRaw(store, key): Retrieve data.
  • cachePutRaw(store, key, data, ttl): Store data with expiration.
  • cachePutPtr(...): Store data from a C-pointer (Zero-Copy).

Go API (package:synurang/pkg/service)

Configuration

  • NewCoreService(config): Creates the main service hub.
  • NewGrpcServer(core, config, registrars...): Creates the gRPC server and registers your custom services.

Streaming Handlers

  • RegisterServerStreamHandler: Register a callback for server-side streaming.
  • RegisterBidiStreamHandler: Register a callback for bidirectional streaming.

πŸ“‚ Directory Structure

synurang/
β”œβ”€β”€ api/core.proto           # Core Protocol definitions
β”œβ”€β”€ cmd/server/main.go       # Go main with FFI exports
β”œβ”€β”€ example/                 # Example Flutter Application
β”‚   β”œβ”€β”€ api/example.proto    # Example service definitions
β”‚   β”œβ”€β”€ cmd/                  # Go CLI tools
β”‚   β”œβ”€β”€ lib/main.dart        # Flutter Example App
β”‚   └── test/                # Integration tests
β”œβ”€β”€ pkg/
β”‚   β”œβ”€β”€ api/                 # Generated Go proto
β”‚   └── service/             # Go service implementations
β”‚       β”œβ”€β”€ cache.go         # SQLite cache service
β”‚       β”œβ”€β”€ server.go        # gRPC server setup
β”‚       └── stream_handler.go # FFI stream protocol
β”œβ”€β”€ src/                     # Shared libraries (.so/.dylib output)
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ synurang.dart        # Main Dart entry point
β”‚   └── src/generated/       # Generated Dart proto
β”œβ”€β”€ makefile
└── pubspec.yaml

βš–οΈ License

MIT License. See LICENSE for details.