• Stars
    star
    166
  • Rank 227,748 (Top 5 %)
  • Language
    Nim
  • License
    MIT License
  • Created almost 7 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Protobuf implementation in pure Nim that leverages the power of the macro system to not depend on any external tools

protobuf

This is a pure Nim implementation of protobuf, meaning that it doesn't rely on the protoc compiler. The entire implementation is based on a macro that takes in either a string or a file containing the proto3 format as specified at https://developers.google.com/protocol-buffers/docs/proto3. It then produces procedures to read, write, and calculate the length of a message, along with types to hold the data in your Nim program. The data types are intended to be as close as possible to what you would normally use in Nim, making it feel very natural to use these types in your program in contrast to some protobuf implementations. Protobuf 3 however has all fields as optional fields, this means that the types generated have a little bit of special sauce going on behind the scenes. This will be explained in a later section. The entire read/write structure is built on top of the Stream interface from the streams module, meaning it can be used directly with anything that uses streams.

Example

To whet your appetite the following example shows how this protobuf macro can be used to generate the required code and read and write protobuf messages. This example can also be found in the examples folder. Note that it is also possible to read in the protobuf specification from a file.

import protobuf, streams

# Define our protobuf specification and generate Nim code to use it
const protoSpec = """
syntax = "proto3";

message ExampleMessage {
  int32 number = 1;
  string text = 2;
  SubMessage nested = 3;
  message SubMessage {
    int32 a_field = 1;
  }
}
"""
parseProto(protoSpec)

# Create our message
var msg = new ExampleMessage
msg.number = 10
msg.text = "Hello world"
msg.nested = initExampleMessage_SubMessage(aField = 100)

# Write it to a stream
var stream = newStringStream()
stream.write msg

# Read the message from the stream and output the data, if it's all present
stream.setPosition(0)
var readMsg = stream.readExampleMessage()
if readMsg.has(number, text, nested) and readMsg.nested.has(aField):
  echo readMsg.number
  echo readMsg.text
  echo readMsg.nested.aField

Generated code

Since all the code is generated from the macro on compile-time and not stored anywhere the generated code is made to be deterministic and easy to understand. If you would like to see the code however you can pass -d:echoProtobuf switch on compile-time and the macro will output the generated code.

Optional fields

As mentioned earlier protobuf 3 makes all fields optional. This means that each field can either exist or not exist in a message. In many other protobuf implementations you notice this by having to use special getter or setter procs for field access. In Nim however we have strong meta-programming powers which can hide much of this complexity for us. As can be seen in the above example it looks just like normal Nim code except from one thing, the call to has. Whenever a field is set to something it will register its presence in the object. Then when you access the field Nim will first check if it is present or not, throwing a runtime ValueError if it isn't set. If you want to remove a value already set in an object you simply call reset with the name of the field as seen in example 3. To check if a value exists or not you can call has on it as seen in the above example. Since it's a varargs call you can simply add all the fields you require in a single check. In the below sections we will have a look at what the protobuf macro outputs. Since the actual field names are hidden behind this abstraction the following sections will show what the objects "feel" like they are defined as. Notice also that since the fields don't actually have these names a regular object initialiser wouldn't work, therefore you have to use the "init" procs created as seen in the above example.

Messages

The types generated are named after the path of the message, but with dots replaced by underscores. So if the protobuf specification contains a package name it starts with that, then the name of the message. If the message is nested then the parent message is put between the package and the message. As an example we can look at a protobuf message defined like this:

syntax = "proto3"; // The only syntax supported
package = our.package;
message ExampleMessage {
    int32 simpleField = 1;
}

The type generated for this message would be named our_package_ExampleMessage. Since Nim is case and underscore insensitive you can of course write this with any style you desire, be it camel-case, snake-case, or a mix as seen above. For this specific instance the type would appear to be:

type
  our_package_ExampleMessage = ref object
    simpleField: int32

Messages also generate a reader, writer, and length procedure to read, write, and get the length of a message on the wire respectively. All write procs are simply named write and are only differentiated by their types. This write procedure takes two arguments plus an optional third parameter, the Stream to write to, an instance of the message type to write, and a boolean telling it to prepend the message with a varint of its length or not. This boolean is used for internal purposes, but might also come in handy if you want to stream multiple messages as described in https://developers.google.com/protocol-buffers/docs/techniques#streaming. The read procedure is named similarily to all the streams module readers, simply "read" appended with the name of the type. So for the above message the reader would be named read_our_package_ExampleMessage. Notice again how you can write it in different styles in Nim if you'd like. One could of course also create an alias for this name should it prove too verbose. Analagously to the write procedure the reader also takes an optional maxSize argument of the maximum size to read for the message before returning. If the size is set to 0 the stream would be read until atEnd returns true. The len procedure is slightly simpler, it only takes an instance of the message type and returns the size this message would take on the wire, in bytes. This is used internally, but might have some other applications elsewhere as well. Notice that this size might vary from one instance of the type to another as varints can have multiple sizes, repeated fields different amount of elements, and oneofs having different choices to name a few.

Enums

Enums are named the same way as messages, and are always declared as pure. So an enum defined like this:

syntax = "proto3"; // The only syntax supported
package = our.package;
enum Langs {
  UNIVERSAL = 0;
  NIM = 1;
  C = 2;
}

Would end up with a type like this:

type
  our_package_Langs {.pure.} = enum
    UNIVERSAL = 0, NIM = 1, C = 2

For internal use enums also generate a reader and writer procedure. These are basically a wrapper around the reader and writer for a varint, only that they convert to and from the enum type. Using these by themselves is seldom useful.

OneOfs

In order for oneofs to work with Nims type system they generate their own type. This might change in the future. Oneofs are named the same way as their parent message, but with the name of the oneof field, and _OneOf appended. All oneofs contain a field named option of a ranged integer from 0 to the number of options. This type is used to create an object variant for each of the fields in the oneof. So a oneof defined like this:

syntax = "proto3"; // The only syntax supported
package our.package;
message ExampleMessage {
  oneof choice {
    int32 firstField = 1;
    string secondField = 1;
  }
}

Will generate the following message and oneof type:

type
  our_package_ExampleMessage_choice_OneOf = object
    case option: range[0 .. 1]
    of 0: firstField: int32
    of 1: secondField: string
  our_package_ExampleMessage = ref object
    choice: our_package_ExampleMessage_choice_OneOf

Exporting message definitions

If you want to re-use the same message definitions in multiple places in your code it's a good idea to create a module for you definition. This can also be useful if you want to rename some of the fields protobuf declares, or if you want to hide particular messages or create extra functionality. Since protobuf uses a little bit of magic under the hood a special exportMessage macro exists that will create the export statements you need in order to export a message definition from the module that reads the protobuf specification, to any module that imports it. Note however that it doesn't export sub-messages or any dependent types, so be sure to export those manually. Anything that's not a message (such as an enum) should be exported by the normal export statement.

Limitations

This library is still in an early phase and has some limitations over the official version of protobuf. Noticably it only supports the "proto3" syntax, so no optional or required fields. It also doesn't currently support maps but you can use the official workaround found here: https://developers.google.com/protocol-buffers/docs/proto3#maps. This is planned to be added in the future. It also doesn't support options, meaning you can't set default values for enums and can't control packing options. That being said it follows the proto3 specification and will pack all scalar fields. It also doesn't support services.

These limitations apply to the parser as well, so if you are using an existing protobuf specification you must remove these fields before being able to parse them with this library.

If you find yourself in need of these features then I'd suggest heading over to https://github.com/oswjk/nimpb which uses the official protoc compiler with an extension to parse the protobuf file.

Rationale

Some might be wondering why I've decided to create this library. After all the protobuf compiler is extensible and there are some other attempts at using protobuf within Nim by using this. The reason is three-fold, first off no-one likes to add an extra step to their compilation process. Running protoc before compiling isn't a big issue, but it's an extra compile-time dependency and it's more work. By using a regular Nim macro this is moved to a simple step in the compilation process. The only requirement is Nim and this library meaning tools can be automatically installed through nimble and still use protobuf. It also means that all of Nims targets are supported, and sending data between code compiled to C and Javascript should be a breeze and can share the exact same code for generating the messages. This is not yet tested, but any issues arising should be easy enough to fix. Secondly the programatic protobuf interface created for some languages are not the best. Python for example has some rather awkward and un-natural patterns for their protobuf library. By using a Nim macro the code can be customised to Nim much better and has the potential to create really native-feeling code resulting in a very nice interface. And finally this has been an interesting project in terms of pushing the macro system to do something most languages would simply be incapable of doing. It's not only a showcase of how much work the Nim compiler is able to do for you through its meta-programming, but has also been highly entertaining to work on.

This file is automatically generated from the documentation found in protobuf.nim. Use nim doc2 protobuf.nim to get the full documentation.

More Repositories

1

nimlsp

Language Server Protocol implementation for Nim
Nim
410
star
2

futhark

Automatic wrapping of C headers in Nim
Nim
341
star
3

ratel

Nim
122
star
4

nimcr

Nim
81
star
5

badger

Keyboard firmware written from scratch using Nim
Nim
73
star
6

binaryparse

Binary parser for Nim
Nim
69
star
7

macroutils

A package that makes creating macros easier
Nim
59
star
8

nancy

Nancy - Nim fancy ANSI tables
Nim
53
star
9

nim-playground-frontend

The front-end for https://play.nim-lang.org
Nim
44
star
10

jsonschema

Schema validation of JSON for Nim
Nim
42
star
11

nim-optionsutils

Utility macros for easier handling of options in Nim
Nim
35
star
12

superlog

Nim
28
star
13

i3ColourChanger

Python
28
star
14

termstyle

Easy to use styles for terminal output
Nim
28
star
15

nim-persistent-vector

Implementation of Clojures persistent vector in Nim for easy immutable lists.
Nim
25
star
16

SDLGamelib

A library of functions to make creating games using Nim and SDL2 easier. This does not intend to be a full blown engine and tries to keep all the components loosely coupled so that individual parts can be used separately.
Nim
25
star
17

notifishower

Nim
21
star
18

nim-electron

Nim
18
star
19

notificatcher

Simple program to read freedesktop notifications and format them as strings
Nim
18
star
20

Configuration

Shell
18
star
21

combparser

A parser combinator library for easy generation of complex parsers
Nim
18
star
22

strslice

Nim
17
star
23

nim-pcap

Tiny pure Nim library to read PCAP files
Nim
16
star
24

labeltry

A new approach to dealing with exceptions
Nim
16
star
25

drawille-nim

Drawing in terminal with Unicode Braille characters. This is the Nim version of the Python original.
Nim
16
star
26

plotter

Simple tool to plot input piped to it
Nim
14
star
27

ikeahomesmart

IKEA Home Smart library for Nim
Nim
13
star
28

libkeepass

Library for reading KeePass files and decrypt the passwords within it
Nim
12
star
29

autotemplate

Nim
12
star
30

nim-cache

Simple cache module for Nim, supports LRU and max-count pruning
Nim
12
star
31

genui

This is what might become a really kick-ass cross-platform native UI toolkit
Nim
10
star
32

getmac

Package to get MAC addresses from an IP address in Nim
Nim
9
star
33

ansiparse

Nim library to parse ANSI escape codes
Nim
9
star
34

xevloop

Library to more easily create X11 event loops
Nim
9
star
35

aoc2021

Advent of Code 2021 solutions in Nim
Nim
9
star
36

stacklang

A stack based calculator/minimal language
Nim
8
star
37

gtkgenui

The genui DSL for creating GUIs implemented for the gtk2 bindings in nimble.
Nim
7
star
38

nimscriptexamples

Examples for my article on embedding NimScript
Nim
7
star
39

nimbleutils

A Nimble package to inspect Nimble packages
Nim
7
star
40

mapm-nim

Matt's Arbitrary Precision Math library - Nim wrapper
Nim
6
star
41

deriveables

Deriveable types in Nim
Nim
6
star
42

ratel-bme280

BME280 implementation for Ratel
Nim
6
star
43

libcoap

Nim wrapper for libcoap
Nim
6
star
44

skeletal

HTML
6
star
45

gtk3genui

The genui DSL for creating GUIs implemented for the gtk3 bindings by StefanSalewski/ngtk3.
Nim
6
star
46

libfuse-nim

Nim
5
star
47

nimtours

A tour of Nim, in multiple parts. Available online at https://play.nim-lang.org/tours/index.html
HTML
5
star
48

MannBarSchwein-arduboy

Arduboy game jam game
C++
4
star
49

aoc2020

Nim
4
star
50

Imlib2

Nim
4
star
51

termfm

Nim
4
star
52

aoc2022

Nim
4
star
53

webexperiment

Nim
3
star
54

TromsoGameJam2017

Nim
3
star
55

fibers

Fibers in Nim, experiment
Nim
3
star
56

zhangshasha

This module is a port of the Java implementation of the Zhang-Shasha algorithm for tree edit distance
Nim
3
star
57

statusbar

Libfuse based statusbar for Nimdow and other xsetroot -name WMs
Nim
3
star
58

ansitohtml

Converts ANSI colour codes to HTML span tags with style tags
Nim
2
star
59

conf.nim-lang.org

Simple conference information page for NimConf2020
CSS
2
star
60

nim-coroutines

A simple coroutines library for Nim based in iterators, untested and mostly for experimentation
Nim
2
star
61

ArcticGameJam2014

JavaScript
2
star
62

libbuilder

Tool to create condensed Nim standard libraries for NimScript integration
Nim
2
star
63

nim-homeassistant

Nim
1
star
64

aoc2023

Nim
1
star
65

echooverride

Simple test to override the Nim standard output in the entire project
Nim
1
star
66

colourfinder

Create nice spectrum images of colours
Nim
1
star
67

femtozip

Nim
1
star
68

pangoterm-altfonts

A copy of the original pangoterm found here: https://www.leonerd.org.uk/code/pangoterm/ but with improved support for alternate fonts
C
1
star
69

data.vm

Experiment into a data based VM
Nim
1
star