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.
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 stalegen-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
| Class | Members |
|---|---|
CodedInputStream | static 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 |
CodedOutputStream | static 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 |
MessageLite | writeTo(CodedOutputStream), getSerializedSize(), toByteArray(). Parsing is a static parseFrom on the generated class. |
WireFormat | WIRETYPE_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) |
InvalidProtocolBufferException | An 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 noequals/hashCode. - Enums are
intconstants in a constants-only class named after the enum (Demo.Mood.HAPPY; javac inlines them, so the class never loads), withgetXValue()/setXValue(int)and, for repeated enums,getXsCount()/getXsValue(i)/addXsValue(v); no Javaenum(one class each). bytesfields andreadBytesarebyte[]; there is noByteString.- No
InputStream/OutputStreamsources or sinks (the runtime has neither), no extension registries, nomap,oneof, groups,Any, well-known types, reflection or text/JSON formats. proto3 syntax only. readStringdoes not validate UTF-8: the runtime’s strings are byte strings.- A
CodedOutputStreamthat runs out of space throwsOutOfSpaceExceptionafter the bytes that did fit, as javalite does.