# sproto trades schema richness for speed in Lua

> sproto is a serialization library written in C whose main target is Lua: a small schema language compiled to a binary blob, table to blob encoding with optional packing, and an RPC layer on top of the same types. The README publishes a benchmark table against a protocol buffers implementation and a JSON library, and the design decisions behind those numbers explain where sproto fits.

**cloudwu/sproto** — Yet another protocol library like google protocol buffers , but simple and fast.

- Repository: https://github.com/cloudwu/sproto
- Stars: 985 · Forks: 254
- Language: C
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloudwu-sproto

## A C core with a Lua binding as the first class citizen

sproto is described in its own documentation as an efficient serialization library for C that focuses on Lua binding, and the repository layout matches that: `sproto.c` and `sproto.h` hold the C implementation, `lsproto.c` and `msvcint.h` deal with Lua specifics, and `sproto.lua` and `sprotoparser.lua` are the Lua side. The whole tree is small and flat, with no build system beyond a Makefile and no dependency list, and the licence is MIT. The library can also be bound to other dynamic languages or used directly from C, which is the design intent: one encoding format implemented once in C, exposed to whichever runtime needs it. What it deliberately is not is a general purpose protocol framework. The type set is short, the messages are not self describing, and the schema has to be compiled before any of it can run.

## The schema language is smaller than protocol buffers on purpose

A schema in sproto looks like this:

```
..Person {
    name 0 : string
    id 1 : integer
    email 2 : string

    .PhoneNumber {
        number 0 : string
        type 1 : integer
    }

    phone 3 : *PhoneNumber
}
```

A leading dot marks a user defined type, a leading asterisk marks an array of that type, and a parenthesised integer after the type name means a fixed point number with that many decimal places, so `integer(2)` stores a value scaled by one hundred. Binary data gets its own type, `double` covers IEEE 754 floating point, and `boolean` is true or false. The `integer` type is signed 64 bit at maximum, and can also be read back with a declared precision. One feature worth noting is the optional main index, written as `*Person(id)`, which marks one field of an array element as its key. The same document also defines a self describing form of the schema itself, with `.type`, `.protocol` and `.group` structures, which is what lets the format describe its own metadata without a separate spec.

## Encoding a Lua table and unpacking it again

The Lua API is a small set of methods on a schema object. Loading a schema text at runtime goes through `sproto.parse`, and the resulting object offers `sproto:encode(typename, luatable)` to turn a table into a binary string and `sproto:decode(typename, blob)` to read it back. A second pair, `sproto:pencode` and `sproto:pdecode`, does the same thing but packs the result, which is the compressed form used on the wire. The object also exposes `exist_type` to check whether a type is defined, and `default` to build a table of default values for a type, with the type argument accepting nil, `REQUEST` or `RESPONSE`. There is also a lower level entry point, `sproto.core`, which creates a protocol object from a C pointer rather than a Lua table, and `sharenew` for sharing one protocol between the C and Lua layers. The typical session in Lua looks like this:

```lua
local sproto = require "sproto"
local sprotocore = require "sproto.core" -- optional
```

The point of the two modules is that the pure Lua path is easier to debug, while the core path keeps the hot path in C.

## The schema is compiled offline so the runtime carries no parser

Separating the parser from the runtime is the decision that shapes the whole design. The `sprotoparser` module exposes a single call, `parser.parse`, which takes a schema text and returns a binary string, and the documentation is explicit that the parser is needed only to produce that binary. The schema text and the parser are not needed while your program is running, so a deployment can ship the compiled blob and skip the parser entirely. That also means the schema can be compiled at build time and checked into a release artifact, and the runtime cost of a message becomes the cost of the encoding itself rather than the cost of parsing a grammar. The trade is that the schema is not available at runtime either: the type information lives in the compiled binary, not in a text file the running process can read, which is a meaningful constraint for any system that wants to inspect or reload its message definitions on the fly.

## The RPC layer reuses the same types instead of defining its own

On top of message encoding, sproto ships an RPC layer built from the same schema language, with a protocol block in the schema declaring a tag, a request type and a response type. At runtime, `sproto:host` creates a host object bound to a package name, `host:dispatch` unpacks and decodes an incoming blob, and `host:attach` returns a function that packs and encodes an outgoing request. The dispatch contract is worth reading closely, because it is where most of the complexity of a request and response protocol lives: if the decoded message has a `.type` field, it is a REQUEST and dispatch returns the string `REQUEST`, the protocol name, the message, a responser function and the user data, with the responser nil when there is no session; if there is no `.type`, the message is a RESPONSE and dispatch returns `RESPONSE`, the session, the message and the user data. The README also lists the lower level `request_encode`, `response_encode`, `request_decode` and `response_decode` calls for anyone who does not want to use the host object, and points at `testrpc.lua` for working examples.

## What the benchmark table shows and what it leaves out

The documentation includes a comparison table measured on a machine the README describes as an i5-2500 at 3.3GHz. Encoding one million messages took 2.15 seconds for sproto and 1.58 seconds for the unpacked variant, and decoding one million took 7.84 seconds and 6.93 seconds respectively, with a message size of 83 bytes packed and 130 bytes unpacked. The same run lists the protocol buffers implementation pbc-lua at 6.94 seconds to encode and 16.9 seconds to decode with 69 byte messages, and the JSON library lua-cjson at 4.92 seconds to encode and 8.30 seconds to decode with 183 byte messages. Those numbers are the authors own figures on one machine with one schema, so the useful reading is the shape rather than the precision: sproto trades message size for speed relative to JSON, and packing buys a smaller payload at a modest cost in time. The table does not cover memory use, different payload shapes, or behaviour on a different processor.

## Building the shared library is three Makefile targets

The Makefile offers the usual targets with no dependencies to install first. On Linux the default target builds a shared object from the two C files with the flags passed on the command line:

```bash
make sproto.so "DLLFLAGS = -shared -fPIC"
```

For macOS the same idea is expressed as a bundle with dynamic lookup, and for Windows there is a separate target that builds `sproto.dll` and links against Lua through `pkgconf`. The `win` target is a shortcut to that DLL, and `clean` removes both shared objects. Since the library is plain C with a small header, integrating it into a larger build means compiling `sproto.c` and `lsproto.c` and exposing the C API, or loading the shared object into a Lua runtime. The repository also ships a set of standalone Lua scripts used as tests, including `testrpc.lua` for the RPC path and `testall.lua` as the entry point, plus `print_r.lua` for inspecting decoded values.

## Conclusion

sproto is a good fit for a Lua service that already treats message schemas as a stable contract and wants a smaller wire format than JSON, in particular an open source game server that needs request and response dispatch in the same library as its payload encoding. It is not a fit if you need to talk to arbitrary protocol buffers clients, because sproto is not wire compatible with that format, and it is not a fit if you need a wide type system, since the schema language supports a short list of types and gives up self description to get there. Before you adopt it, check three things: that the runtime you care about is Lua or something you can bind the C core to, that the schema you need can be expressed in the small type set, and that the last push date of 2026-08-15 matches your expectations for how often this library changes.

## FAQ

### How do I load an sproto schema in Lua?

Call `sproto.parse(schema)` with the schema text, or use `sproto.new(spbin)` with a binary string produced offline by `parser.parse` from the sprotoparser module. The schema text and the parser are not needed once the program is running.

### Is sproto wire compatible with protocol buffers?

No. The documentation shows an sproto schema and the equivalent protocol buffers schema side by side to make the mapping clear, but the two encodings are different formats and a message produced by one is not readable by the other.

### What types can an sproto schema use?

String, binary, integer, double and boolean. Integers are signed 64 bit at maximum and can be fixed point with a declared precision, and a leading asterisk on a type name declares an array.

### How do I build sproto on Linux?

The Makefile's default target builds the shared object, and the Linux line is `make sproto.so "DLLFLAGS = -shared -fPIC"`. There is a macOS bundle target and a separate `sproto.dll` target for Windows that links Lua through `pkgconf`.

### How do I handle a request and its response with sproto?

Create a host object with `sproto:host` and call `host:dispatch` on an incoming blob, which returns `REQUEST` with the protocol name, the message and a responser function, or `RESPONSE` with the session and the message. The lower level `request_encode` and `response_decode` calls are available when you do not want the host object.

## Sources

- [cloudwu/sproto on GitHub](https://github.com/cloudwu/sproto)
- [Issues](https://github.com/cloudwu/sproto/issues)
- [License: MIT](https://github.com/cloudwu/sproto/blob/master/LICENSE)
- [README](https://github.com/cloudwu/sproto/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cloudwu-sproto
