Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Last active September 5, 2025 13:25
Show Gist options
  • Save peterhellberg/65346cabe20318a61fc4e6cb817fe342 to your computer and use it in GitHub Desktop.
Save peterhellberg/65346cabe20318a61fc4e6cb817fe342 to your computer and use it in GitHub Desktop.
Tiny example in Go of using CBOR tags to distinguish between two different types of messages using github.com/fxamacker/cbor/v2
package main
import (
"fmt"
"log"
"github.com/fxamacker/cbor/v2"
)
const (
tagChatMessage = 10001
tagChatPresence = 10002
)
type ChatEvent interface {
ChatEventType() string
}
type chatMessage struct {
User string `cbor:"user"`
Message string `cbor:"message"`
}
func (c chatMessage) ChatEventType() string {
return "chatMessage"
}
type chatPresence struct {
User string `cbor:"user"`
Status string `cbor:"status"`
}
func (c chatPresence) ChatEventType() string {
return "chatPresence"
}
func marshal(tagNum uint64, v any) ([]byte, error) {
payloadBytes, err := cbor.Marshal(v)
if err != nil {
return nil, err
}
return cbor.Marshal(cbor.RawTag{
Number: tagNum,
Content: payloadBytes,
})
}
func unmarshal(data []byte) (ChatEvent, error) {
var tag cbor.RawTag
if err := cbor.Unmarshal(data, &tag); err != nil {
return nil, fmt.Errorf("failed to unmarshal tag: %w", err)
}
switch tag.Number {
case tagChatMessage:
var msg chatMessage
return msg, cbor.Unmarshal(tag.Content, &msg)
case tagChatPresence:
var pres chatPresence
return pres, cbor.Unmarshal(tag.Content, &pres)
default:
return nil, fmt.Errorf("unknown tag: %d", tag.Number)
}
}
func main() {
var (
msg = chatMessage{User: "alice", Message: "hello!"}
pres = chatPresence{User: "bob", Status: "away"}
)
msgBytes, err := marshal(tagChatMessage, msg)
if err != nil {
log.Fatal(err)
}
presBytes, err := marshal(tagChatPresence, pres)
if err != nil {
log.Fatal(err)
}
for _, data := range [][]byte{
msgBytes,
presBytes,
} {
event, err := unmarshal(data)
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Decoded data: %#v\n", event)
}
switch e := event.(type) {
case chatMessage:
fmt.Println("Message from:", e.User)
case chatPresence:
fmt.Println("Presence update:", e.User, e.Status)
}
}
}
@peterhellberg
Copy link
Author

You could encode the messages wrapped in a self describing CBOR tag:

func marshal(tagNum uint64, v any) ([]byte, error) {
	return cbor.Marshal(cbor.Tag{
		Number: tagSelfDescribe,
		Content: cbor.Tag{
			Number:  tagNum,
			Content: v,
		},
	})
}

Where tagSelfDescribe = 55799

https://www.rfc-editor.org/rfc/rfc8949.html#name-self-described-cbor

@peterhellberg
Copy link
Author

package main

import (
	"fmt"
	"log"

	"github.com/fxamacker/cbor/v2"
)

const (
	tagChatMessage  uint64 = 10001
	tagChatPresence        = 10002
	tagSelfDescribe        = 55799
)

type ChatEvent interface {
	ChatEventType() string
}

type chatMessage struct {
	User    string `cbor:"user"`
	Message string `cbor:"message"`
}

func (c chatMessage) ChatEventType() string {
	return "chatMessage"
}

type chatPresence struct {
	User   string `cbor:"user"`
	Status string `cbor:"status"`
}

func (c chatPresence) ChatEventType() string {
	return "chatPresence"
}

func marshal(number uint64, content any) ([]byte, error) {
	return cbor.Marshal(cbor.Tag{
		Number: tagSelfDescribe,
		Content: cbor.Tag{
			Number:  number,
			Content: content,
		},
	})
}

func unmarshal(data []byte) (ChatEvent, error) {
	var raw cbor.RawTag

	if err := cbor.Unmarshal(data, &raw); err != nil {
		return nil, fmt.Errorf("failed to unmarshal tag: %w", err)
	}

	switch raw.Number {
	case tagChatMessage:
		var msg chatMessage
		return msg, cbor.Unmarshal(raw.Content, &msg)
	case tagChatPresence:
		var pres chatPresence
		return pres, cbor.Unmarshal(raw.Content, &pres)
	default:
		return nil, fmt.Errorf("unknown tag: %d", raw.Number)
	}
}

func main() {
	var (
		msg  = chatMessage{User: "alice", Message: "hello!"}
		pres = chatPresence{User: "bob", Status: "away"}
	)

	msgBytes, err := marshal(tagChatMessage, msg)
	if err != nil {
		log.Fatal(err)
	}

	presBytes, err := marshal(tagChatPresence, pres)
	if err != nil {
		log.Fatal(err)
	}

	for _, data := range [][]byte{
		msgBytes,
		presBytes,
	} {
		event, err := unmarshal(data)
		if err != nil {
			log.Fatal(err)
		} else {
			fmt.Printf("Decoded data: %#v\n", event)
		}

		switch e := event.(type) {
		case chatMessage:
			fmt.Println("Message from:", e.User)
		case chatPresence:
			fmt.Println("Presence update:", e.User, e.Status)
		}
	}
}

@peterhellberg
Copy link
Author

peterhellberg commented Sep 5, 2025

TinyCBOR version in C (compiled using zig cc)

Download and unpack tinycbor v0.6.1 into tinycbor-0.6.1/

curl -s -L https://github.com/intel/tinycbor/archive/refs/tags/v0.6.1.tar.gz | tar xvz

c-cbor-tagged-messages.c

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include "cbor.h"

#define TAG_SELF_DESCRIBE 55799
#define TAG_CHAT_MESSAGE 10001
#define TAG_CHAT_PRESENCE 10002

typedef struct {
    char user[64];
    char message[256];
    bool priority;
} ChatMessage;

typedef struct {
    char user[64];
    char status[64];
    int  level;
} ChatPresence;

// ---------------- ENCODING ----------------

typedef void (*EncodeContentFn)(CborEncoder *map, void *ctx);

size_t encode_tagged_message(uint8_t *buffer, size_t buf_size, CborTag outer_tag,
                             CborTag inner_tag, EncodeContentFn encode_fn, void *ctx) {
    CborEncoder enc, map;
    cbor_encoder_init(&enc, buffer, buf_size, 0);
    cbor_encode_tag(&enc, outer_tag);
    cbor_encode_tag(&enc, inner_tag);

    cbor_encoder_create_map(&enc, &map, CborIndefiniteLength);
    encode_fn(&map, ctx);
    cbor_encoder_close_container(&enc, &map);

    return cbor_encoder_get_buffer_size(&enc, buffer);
}

void encode_chat_message_content(CborEncoder *map, void *ctx) {
    ChatMessage *msg = (ChatMessage *)ctx;
    cbor_encode_text_stringz(map, "user");
    cbor_encode_text_stringz(map, msg->user);
    cbor_encode_text_stringz(map, "message");
    cbor_encode_text_stringz(map, msg->message);
    cbor_encode_text_stringz(map, "priority");
    cbor_encode_boolean(map, msg->priority);
}

void encode_chat_presence_content(CborEncoder *map, void *ctx) {
    ChatPresence *pres = (ChatPresence *)ctx;
    cbor_encode_text_stringz(map, "user");
    cbor_encode_text_stringz(map, pres->user);
    cbor_encode_text_stringz(map, "status");
    cbor_encode_text_stringz(map, pres->status);
    cbor_encode_text_stringz(map, "level");
    cbor_encode_int(map, pres->level);
}

size_t encode_chat_message(uint8_t *buffer, size_t buf_size, ChatMessage *msg) {
    return encode_tagged_message(buffer, buf_size, TAG_SELF_DESCRIBE, TAG_CHAT_MESSAGE,
                                 encode_chat_message_content, msg);
}

size_t encode_chat_presence(uint8_t *buffer, size_t buf_size, ChatPresence *pres) {
    return encode_tagged_message(buffer, buf_size, TAG_SELF_DESCRIBE, TAG_CHAT_PRESENCE,
                                 encode_chat_presence_content, pres);
}

// ---------------- DECODING ----------------

typedef void (*MapEntryHandler)(const char *key, CborValue *val, void *ctx);

void decode_map_values(CborValue *map, MapEntryHandler handle_entry, void *ctx) {
    for (CborValue item = *map; !cbor_value_at_end(&item);) {
        char key[64];
        size_t key_len = sizeof(key);

        if (cbor_value_copy_text_string(&item, key, &key_len, NULL) != CborNoError) {
            printf("Error reading key\n");
            return;
        }
        cbor_value_advance(&item);

        handle_entry(key, &item, ctx);
        cbor_value_advance(&item);
    }
}

void handle_chat_message_entry(const char *key, CborValue *val, void *ctx) {
    ChatMessage *msg = (ChatMessage *)ctx;

    if (strcmp(key, "user") == 0 && cbor_value_is_text_string(val)) {
        size_t len = sizeof(msg->user);
        cbor_value_copy_text_string(val, msg->user, &len, NULL);
    } else if (strcmp(key, "message") == 0 && cbor_value_is_text_string(val)) {
        size_t len = sizeof(msg->message);
        cbor_value_copy_text_string(val, msg->message, &len, NULL);
    } else if (strcmp(key, "priority") == 0 && cbor_value_is_boolean(val)) {
        bool b;
        cbor_value_get_boolean(val, &b);
        msg->priority = b;
    }
}

void handle_chat_presence_entry(const char *key, CborValue *val, void *ctx) {
    ChatPresence *pres = (ChatPresence *)ctx;

    if (strcmp(key, "user") == 0 && cbor_value_is_text_string(val)) {
        size_t len = sizeof(pres->user);
        cbor_value_copy_text_string(val, pres->user, &len, NULL);
    } else if (strcmp(key, "status") == 0 && cbor_value_is_text_string(val)) {
        size_t len = sizeof(pres->status);
        cbor_value_copy_text_string(val, pres->status, &len, NULL);
    } else if (strcmp(key, "level") == 0 && cbor_value_is_integer(val)) {
        int64_t level;
        cbor_value_get_int64(val, &level);
        pres->level = (int)level;
    }
}


void decode_message(uint8_t *data, size_t len) {
    CborParser parser;
    CborValue value, inner;
    CborTag tag;
    CborError err;

    err = cbor_parser_init(data, len, 0, &parser, &value);
    if (err != CborNoError) { printf("Parser init error\n"); return; }

    if (!cbor_value_is_tag(&value)) { printf("Expected self-describe tag\n"); return; }
    cbor_value_get_tag(&value, &tag);
    if (tag != TAG_SELF_DESCRIBE) { printf("Expected self-describe tag, got %lu\n", tag); return; }
    cbor_value_advance(&value);

    if (!cbor_value_is_tag(&value)) { printf("Expected inner type tag\n"); return; }
    cbor_value_get_tag(&value, &tag);
    cbor_value_advance(&value);

    cbor_value_enter_container(&value, &inner);

    if (tag == TAG_CHAT_MESSAGE) {
        ChatMessage msg = {0};
        decode_map_values(&inner, handle_chat_message_entry, &msg);
        printf("Decoded ChatMessage: user=%s, message=%s, priority=%s\n",
               msg.user, msg.message, msg.priority?"true":"false");

    } else if (tag == TAG_CHAT_PRESENCE) {
        ChatPresence pres = {0};
        decode_map_values(&inner, handle_chat_presence_entry, &pres);
        printf("Decoded ChatPresence: user=%s, status=%s, level=%d\n",
               pres.user, pres.status, pres.level);

    } else {
        printf("Unknown tag: %lu\n", tag);
    }
}

void print_and_decode(const char *label, uint8_t *buffer, size_t len) {
    printf("%s (%zu bytes): ", label, len);
    for (size_t i = 0; i < len; i++) printf("%02x", buffer[i]);
    printf("\n");
    decode_message(buffer, len);
}

// ---------------- MAIN ----------------

int main() {
    uint8_t buffer[512];
    size_t len;

    ChatMessage msg = {.user="alice", .message="hello!", .priority=true};
    len = encode_chat_message(buffer, sizeof(buffer), &msg);
    print_and_decode("Encoded ChatMessage", buffer, len);

    ChatPresence pres = {.user="bob", .status="away", .level=2};
    len = encode_chat_presence(buffer, sizeof(buffer), &pres);
    print_and_decode("Encoded ChatPresence", buffer, len);

    return 0;
}

Makefile

CC = zig cc
CFLAGS = -Wall -O2 -I./tinycbor-0.6.1/src

SRCS = c-cbor-tagged-messages.c \
      	tinycbor-0.6.1/src/cborencoder.c \
       	tinycbor-0.6.1/src/cborparser.c

OBJS = c-cbor-tagged-messages.o \
    		tinycbor-0.6.1/src/cborencoder.o \
    		tinycbor-0.6.1/src/cborparser.o

TARGET = c-cbor-tagged-messages

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

main.o: main.c
	$(CC) $(CFLAGS) -c $< -o $@

tinycbor-0.6.1/src/cborencoder.o: tinycbor-0.6.1/src/cborencoder.c
	$(CC) $(CFLAGS) -c $< -o $@

tinycbor-0.6.1/src/cborparser.o: tinycbor-0.6.1/src/cborparser.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: download
download:
	curl -s -L https://github.com/intel/tinycbor/archive/refs/tags/v0.6.1.tar.gz | tar xvz

Result

$ make && ./c-cbor-tagged-messages 
zig cc -Wall -O2 -I./tinycbor-0.6.1/src   -c -o c-cbor-tagged-messages.o c-cbor-tagged-messages.c
zig cc -Wall -O2 -I./tinycbor-0.6.1/src -c tinycbor-0.6.1/src/cborencoder.c -o tinycbor-0.6.1/src/cborencoder.o
zig cc -Wall -O2 -I./tinycbor-0.6.1/src -c tinycbor-0.6.1/src/cborparser.c -o tinycbor-0.6.1/src/cborparser.o
zig cc -Wall -O2 -I./tinycbor-0.6.1/src -o c-cbor-tagged-messages c-cbor-tagged-messages.o tinycbor-0.6.1/src/cborencoder.o tinycbor-0.6.1/src/cborparser.o
Encoded ChatMessage (44 bytes): d9d9f7d92711bf647573657265616c696365676d6573736167656668656c6c6f21687072696f72697479f5ff
Decoded ChatMessage: user=alice, message=hello!
Encoded ChatPresence (36 bytes): d9d9f7d92712bf647573657263626f62667374617475736461776179656c6576656c02ff
Decoded ChatPresence: user=bob, status=away

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment