How to Use protobuf-go-lite with TinyGo for WebAssembly and Embedded Systems

Use protobuf-go-lite to generate static, reflection-free protobuf code that compiles seamlessly with TinyGo for WebAssembly and embedded targets by enabling only the marshal+unmarshal+size features during code generation.

The aperturerobotics/protobuf-go-lite repository provides a lightweight fork of the official Go protobuf implementation specifically designed to eliminate reflection-based code generation. This architectural choice makes it fully compatible with TinyGo, a Go compiler that targets constrained environments where the standard reflect package is unavailable or severely limited.

Why protobuf-go-lite Works with TinyGo

Traditional protobuf implementations rely heavily on Go's reflection package to handle message serialization dynamically. TinyGo, particularly when targeting WebAssembly or microcontrollers, either omits or significantly trims the reflect package to reduce binary size.

The protobuf-go-lite generator solves this by producing static code generation for all serialization logic. In features/marshal/marshal.go, the generator emits methods like MarshalVT() that unroll wire encoding logic at compile time rather than using reflection. Similarly, features/unmarshal/unmarshal.go generates UnmarshalVT() methods that parse protobuf wire formats using only primitive operations and the minimal protowire utilities.

Key architectural benefits include:

  • Zero reflection dependencies: The generated code in generator/helpers.go uses only wire-type constants and primitive sizing logic, never reflect.TypeOf or similar calls.
  • Configurable build tags: The generator in generator/generator.go supports prepending custom //go:build constraints to generated files, allowing you to create TinyGo-specific variants.
  • Minimal runtime: The protobuf-go-lite.go runtime file contains only thin utility functions, keeping the final binary size small for embedded targets.
  • Feature toggles: You can enable only the features you need via the --features flag in cmd/protoc-gen-go-lite/main.go, minimizing code size by excluding unnecessary functionality.

Generating TinyGo-Compatible Protobuf Code

To use protobuf-go-lite with TinyGo, you must generate code using specific feature flags that exclude reflection-dependent functionality.

First, install the plugin:

go install github.com/aperturerobotics/protobuf-go-lite/cmd/protoc-gen-go-lite@latest

Next, generate code with only the essential features enabled. The cmd/protoc-gen-go-lite/main.go entry point accepts a --features flag that controls which generators run:

protoc \
  --go-lite_out=. \
  --go-lite_opt=features=marshal+unmarshal+size \
  message.proto

This command generates a .pb.go file containing MarshalVT, UnmarshalVT, and SizeVT methods. The features/size/size.go generator ensures buffer pre-allocation logic is also emitted, optimizing performance without requiring reflection.

Complete TinyGo Example: WebAssembly and Embedded

Below is a complete workflow demonstrating protobuf serialization with TinyGo for both WebAssembly and embedded ARM targets.

Protocol Definition

Create a message.proto file:

syntax = "proto3";

package example;

message SensorReading {
  string sensor_id = 1;
  int32  value     = 2;
  bool   alert     = 3;
}

TinyGo Application

After running the protoc command from the previous section, create a main.go file:

package main

import (
	"fmt"
	"example"
)

func main() {
	// Create a message
	reading := &example.SensorReading{
		SensorId: "temp_01",
		Value:    42,
		Alert:    true,
	}

	// Serialize using the generated MarshalVT method
	data, err := reading.MarshalVT()
	if err != nil {
		panic(err)
	}
	fmt.Printf("Serialized %d bytes\n", len(data))

	// Deserialize using the generated UnmarshalVT method
	var decoded example.SensorReading
	if err := decoded.UnmarshalVT(data); err != nil {
		panic(err)
	}
	fmt.Printf("Decoded: sensor_id=%s value=%d alert=%t\n", 
		decoded.SensorId, decoded.Value, decoded.Alert)
}

Building for WebAssembly

Compile to WebAssembly using TinyGo's wasm target:

tinygo build -target wasm -o sensor.wasm .

The resulting sensor.wasm file contains the complete protobuf serialization logic without any reflection overhead, making it suitable for browser environments or Wasm edge runtimes.

Building for Embedded Systems

For ARM Cortex-M microcontrollers, specify the appropriate bare-metal target:

tinygo build -target=thumbv6m-none-eabi -o sensor.bin .

The thumbv6m-none-eabi target produces a bare-metal binary for Cortex-M0/M0+ processors. Because protobuf-go-lite generates only static code, the binary size remains minimal—critical for microcontrollers with limited flash memory.

Key Repository Files for TinyGo Integration

Understanding the source structure helps troubleshoot build issues and customize the generator for specific embedded constraints.

  • cmd/protoc-gen-go-lite/main.go – Entry point that parses the --features flag and initializes the code generator. Controls which serialization features are emitted.

  • generator/generator.go – Core driver that emits the //go:build header and orchestrates feature-specific generators. Handles file-level code generation and build tag injection.

  • features/marshal/marshal.go – Generates the MarshalVT() method family. Creates static wire-encoding logic without reflection.

  • features/unmarshal/unmarshal.go – Generates the UnmarshalVT() method family. Parses protobuf wire format using only primitive operations.

  • features/size/size.go – Generates SizeVT() for buffer pre-allocation. Optimizes memory allocation during serialization.

  • protobuf-go-lite.go – Minimal runtime utilities. Contains only thin helper functions to keep binary size small.

  • generator/helpers.go – Wire-type utilities and sizing logic used across all generators. Defines the mapping between protobuf types and wire formats.

Summary

  • protobuf-go-lite generates static, reflection-free protobuf serialization code that compiles with TinyGo for WebAssembly and embedded targets.
  • Enable only essential features (marshal+unmarshal+size) via the --go-lite_opt=features flag to minimize binary size and eliminate reflection dependencies.
  • The generated MarshalVT and UnmarshalVT methods in features/marshal/marshal.go and features/unmarshal/unmarshal.go operate without reflect package usage.
  • TinyGo can compile the generated code to WebAssembly (-target wasm) or bare-metal embedded targets (-target thumbv6m-none-eabi) without modification.
  • The minimal runtime in protobuf-go-lite.go ensures small binary sizes critical for microcontrollers and browser Wasm environments.

Frequently Asked Questions

Does protobuf-go-lite support JSON serialization with TinyGo?

Yes, but with caveats. The JSON feature in features/json/message.go generates code that relies on github.com/valyala/fastjson, which TinyGo can import. However, including JSON support increases binary size compared to using only marshal+unmarshal. For the smallest TinyGo binaries, disable JSON and use binary protobuf format exclusively.

Can I use the standard google.golang.org/protobuf library with TinyGo instead?

No. The standard google.golang.org/protobuf library relies heavily on the reflect package for message marshaling and unmarshaling. TinyGo either omits or severely limits reflection support, particularly in WebAssembly and embedded targets, causing compilation failures or runtime panics. Use protobuf-go-lite specifically because it generates static code without reflection.

How do I reduce binary size further for microcontrollers?

Enable only the minimal feature set using --go-lite_opt=features=marshal+unmarshal. Omit the size feature if you can tolerate slightly less efficient buffer allocation, or omit json to avoid pulling in additional dependencies. Additionally, use TinyGo's -opt=z optimization flag for aggressive size reduction: tinygo build -target=thumbv6m-none-eabi -opt=z -o firmware.bin ..

What build tags does protobuf-go-lite support?

The generator supports custom build tags via the --go-lite_opt=buildTag=<tag> flag. In generator/generator.go, this prepends //go:build <tag> to every generated file. This allows you to maintain separate TinyGo-specific generated files alongside standard Go builds, or exclude files that depend on unsupported features in specific environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →