A channel moves opaque bytes. What those bytes mean is the application’s choice, and idl/ is one answer: microser, a tagged wire format with a small IDL compiler.

A channel’s content type is a topic, in the sense MQTT uses the word. It is set when the channel opens and read back from the peer’s CHANNEL_OPEN, so a program routes a channel by its topic before it reads a byte of payload. A topic can carry one message type, or many.

Describe a message in an .idl file and the compiler emits a C struct and an encode and decode function for it. A field is a type, a name, and a number from 1 to 31.

message PlayerInput
    uint32 seq   = 1
    uint8  input = 2
end

The wire format is tagged, so a reader steps over a field it does not know: a newer sender may add fields and an older reader still parses the ones it understands. A bytes or string field decodes zero-copy, as a pointer into the caller’s buffer and a length, so a large payload does not enlarge the struct. Keeping message structs small matters, because a message that exceeds the path limit is fragmented, and fragmentation is the first thing to cost you throughput.

When several message types share one topic, a dispatch block multiplexes them on a leading tag byte. Each datagram becomes <tag><body>.

dispatch Chat
    1 Join
    2 Say
    3 Leave
end

From that block the compiler generates a tag constant per message, an encode helper that prefixes the tag, and a decode entry point that reads the tag into a tagged union. The receiver reads a datagram, decodes, and switches on the type, which mirrors how it already polls netchan_event.

struct chat_msg m;
chat_decode(buf, len, &m);
switch (m.type) {
case CHAT_JOIN:  /* m.u.join  */  break;
case CHAT_SAY:   /* m.u.say   */  break;
case CHAT_LEAVE: /* m.u.leave */  break;
default:         /* CHAT_NONE: a type this build predates; skipped */ break;
}

An unknown tag is not an error. The body carries its own length, so the decoder steps over a message type it does not recognize and reports the type as CHAT_NONE, the same forward compatibility the field level gives you. Tag numbers can follow a hierarchy, for instance one range for control and another for data, since a tag is a full byte. examples/typed/ shows the whole path end to end. Generation runs at build time and needs a POSIX awk; a project that would rather not run it can copy idl/microser.h and write the structs by hand.