synurang 0.1.6
synurang: ^0.1.6 copied to clipboard
Flutter FFI + gRPC bridge for bidirectional Go/Dart communication
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:
- Play Store: https://play.google.com/store/apps/details?id=io.tempage.synura
- Docs & API: https://github.com/tempage/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:
- VIEW (Flutter): Responsible only for rendering the UI and handling user input. It should contain zero complex business logic.
- LOGIC (Go): The "Brain" of the application. Handles database access (SQLite), complex parsing, networking, and state calculation.
- 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-ffiplugin supports--lang=cppto 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-ffigenerates 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-ffito auto-generate type-safe bindings from your.protofiles. - πΎ 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 likegrpcurlor 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
- Linux:
- 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 #
-
Place the Library:
- Linux: Place
libmyapp.soin a location accessible to the runner (or useLD_LIBRARY_PATHduring dev). - Android: Place
.sofiles inandroid/app/src/main/jniLibs/<arch>/.
- Linux: Place
-
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.