Skip to content

Protocol Buffers

picodroid.protobuf.* — CodedInputStream, CodedOutputStream, MessageLite, WireFormat and InvalidProtocolBufferException with the method names, signatures and semantics of com.google.protobuf (protobuf-javalite; the picodroid namespace rule makes it picodroid.protobuf). A .proto file becomes Java message classes over these streams with protoc-gen-picodroid (tools/protoc-gen-picodroid/). See Java API overview for the full API index.

Protobuf is a board capability, like JSON: a board opts in with has_protobuf = true in its board.toml. Every RP2350 board ships it. testbench_rp2040 leaves it off, which drops the five classes from that board’s embedded SDK and compiles the native codec out, so the board pays nothing for it — and an app that references picodroid.protobuf fails that board’s verifyApiContract at build time (EXCLUDED ON BOARD testbench_rp2040) rather than on the device.

Quick example

With examples/claudeusage/proto/usage.proto compiled by scripts/gen-proto.sh, the display reads the bridge’s reply in one call:

import claudeusage.proto.UsageReply;
import picodroid.protobuf.InvalidProtocolBufferException;
try {
UsageReply reply = UsageReply.parseFrom(buf, 0, total); // unknown fields are skipped
int pct = reply.getSession().getPct();
for (int i = 0; i < reply.getModelCapsCount(); i++) {
String name = reply.getModelCaps(i).getName();
}
byte[] again = reply.toByteArray(); // getSerializedSize() bytes
} catch (InvalidProtocolBufferException e) {
// truncated input, a malformed varint, a zero field number, a group
}

The streams work on their own too, the way nanopb’s callbacks or javalite’s generated code do:

CodedInputStream in = CodedInputStream.newInstance(bytes);
int tag;
while ((tag = in.readTag()) != 0) {
switch (WireFormat.getTagFieldNumber(tag)) {
case 1: version = in.readUInt32(); break;
case 2: name = in.readString(); break;
default: in.skipField(tag);
}
}
byte[] out = new byte[CodedOutputStream.computeUInt32Size(1, 7) + CodedOutputStream.computeStringSize(2, "pico")];
CodedOutputStream os = CodedOutputStream.newInstance(out);
os.writeUInt32(1, 7);
os.writeString(2, "pico");
os.checkNoSpaceLeft();

How it works

The byte-at-a-time work — tags, varints, fixed-width values, skipping an unknown field — is native (the micropb codec reading the Java byte[] in place); slicing a string or a byte[] out of the buffer, zigzag, limits and size arithmetic are Java. A parse allocates only what the message holds: no wrappers, no boxing, no pool. readDouble/writeDouble are native as well because the runtime has no Double.longBitsToDouble.

A generated message is a plain mutable class: private fields with proto3 defaults, getX(), hasX() for message and optional fields, setX(v), clearX(), repeated fields as primitive or typed arrays behind getXCount()/getX(int)/addX(v), static parseFrom(byte[])/parseFrom(byte[], int, int)/parseFrom(CodedInputStream), mergeFrom(CodedInputStream), writeTo, getSerializedSize, toByteArray. Each message is one class, which is what it costs on the device: about 1–2 KB of class metadata per message type.

Generating message classes

An app keeps its schema in examples/<app>/proto/<name>.proto, and the generated Java lands under examples/<app>/java/, in the package that option java_package names. The generated files are committed, so building an app does not need protoc.

Terminal window
pip install -r tools/protoc-gen-picodroid/requirements.txt
./scripts/gen-proto.sh # regenerate every examples/*/proto/*.proto
./scripts/gen-proto.sh --check # fail if the committed output is stale

gen-proto.sh runs the plugin with the Python named by PICODROID_PROTO_PYTHON (default python3). When the app has a bridge/ directory, protoc’s own Python output lands there as <name>_pb2.py.

Stream API

ClassMembers
CodedInputStreamstatic newInstance(byte[] buf), newInstance(byte[] buf, int off, int len); readTag() (0 at the end of the stream or of the current limit), getLastTag(), checkLastTagWas(int), skipField(int tag), skipMessage(); readInt32, readUInt32, readSInt32, readFixed32, readSFixed32, readEnum, readInt64, readUInt64, readSInt64, readFixed64, readSFixed64, readBool, readFloat, readDouble, readString, readBytes; readRawVarint32, readRawVarint64, readRawByte, readRawBytes(int size); pushLimit(int byteLimit), popLimit(int oldLimit), getBytesUntilLimit(), isAtEnd(), getTotalBytesRead(); static decodeZigZag32, decodeZigZag64
CodedOutputStreamstatic newInstance(byte[] buf), newInstance(byte[] buf, int off, int len); writeTag(int fieldNumber, int wireType); for each of Int32, UInt32, SInt32, Fixed32, SFixed32, Int64, UInt64, SInt64, Fixed64, SFixed64, Bool, Enum, Float, Double, String, Bytes and Message: writeX(int fieldNumber, value), writeXNoTag(value), static computeXSize(int fieldNumber, value) and static computeXSizeNoTag(value); writeRawVarint32, writeRawVarint64, writeRawByte, writeRawBytes; getTotalBytesWritten(), spaceLeft(), checkNoSpaceLeft(), flush(); static computeTagSize(int fieldNumber), encodeZigZag32, encodeZigZag64
MessageLitewriteTo(CodedOutputStream), getSerializedSize(), toByteArray(). Parsing is a static parseFrom on the generated class.
WireFormatWIRETYPE_VARINT (0), WIRETYPE_FIXED64 (1), WIRETYPE_LENGTH_DELIMITED (2), WIRETYPE_START_GROUP (3), WIRETYPE_END_GROUP (4), WIRETYPE_FIXED32 (5); static getTagWireType(int tag), getTagFieldNumber(int tag), makeTag(int fieldNumber, int wireType)
InvalidProtocolBufferExceptionAn IOException, as is CodedOutputStream.OutOfSpaceException. The read and write methods declare IOException.

examples/protodemo/ round-trips every field kind on the runtime.

Deviations from protobuf-javalite

  • Messages are mutable and have setters; there are no Builders (a Builder doubles the class count) and no equals/hashCode.
  • Enums are int constants in a constants-only class named after the enum (Demo.Mood.HAPPY; javac inlines them, so the class never loads), with getXValue()/setXValue(int) and, for repeated enums, getXsCount()/getXsValue(i)/addXsValue(v); no Java enum (one class each).
  • bytes fields and readBytes are byte[]; there is no ByteString.
  • No InputStream/OutputStream sources or sinks (the runtime has neither), no extension registries, no map, oneof, groups, Any, well-known types, reflection or text/JSON formats. proto3 syntax only.
  • readString does not validate UTF-8: the runtime’s strings are byte strings.
  • A CodedOutputStream that runs out of space throws OutOfSpaceException after the bytes that did fit, as javalite does.