VIIPER Client Generator Documentation
Overview
The VIIPER client generator scans Go source code to extract API routes, device wire formats, and constants; then emits type-safe client SDKs for multiple languages.
What it extracts:
- API routes and DTOs from management API handlers
- Device wire formats from
viiper:wirecomment tags - All exported constants from device packages (automatic)
Output: Type-safe client SDKs for multiple target languages
License
All generated client SDKs are licensed under the MIT License, providing maximum flexibility for integration into your projects. The core VIIPER server remains under its original license.
Running the Generator
cd viiper
go run ./cmd/viiper codegen --lang=all # Generate all SDKs
go run ./cmd/viiper codegen --lang=c # Generate C SDK only
go run ./cmd/viiper codegen --lang=csharp # Generate C# SDK only
go run ./cmd/viiper codegen --lang=typescript # Generate TypeScript SDK only
Output directory: clients/ (relative to repository root)
Comment Tag System
The generator uses lightweight comment tags placed next to device types and constants.
viiper:wire: Device Stream Formats
Syntax:
Directions:
c2s: Client to server (input)s2c: Server to client (output, e.g., rumble, LEDs)
Field types:
- Fixed:
u8,i8,u16,i16,u32,i32 - Variable:
u8*countField(pointer to count field)
Example:
Constant and Map Export
The generator automatically exports all constants and map literals from pkg/device/*/const.go for each device type.
No special tags are required. Exported Go constants and maps are emitted with language-appropriate representations:
- Constants: Grouped into enums (C#/TS) or
#definemacros (C) based on common prefixes - Maps: Converted to Dictionary/Map/lookup functions with helper methods
Code Generation Flow
Scan Phase:
- Parse API routes from
internal/server/api/*.go - Reflect response DTOs from
pkg/apitypes/*.go - Find device types via
RegisterDevice()calls - Parse
viiper:wirecomments for packet layouts - Extract all exported constants and map literals from
pkg/device/*/const.go(automatic)
Emit Phase:
For each language, generate management client, DTO types, device streams, constants, and build configs.
Post-Process:
Optional formatting with clang-format, dotnet format, or prettier.
Wire Format Mapping Rules
Fixed-Size Fields
Fixed-size fields are mapped to native integer types in each target language:
u8/i8: 8-bit unsigned/signed integersu16/i16: 16-bit unsigned/signed integersu32/i32: 32-bit unsigned/signed integers
Variable-Length Fields
Variable-length arrays use a pointer + count pattern. The field syntax u8*count references a count field that determines the array length.
Wire tag example:
Each target language emits appropriate types for dynamic arrays (pointers with counts, managed arrays, or typed arrays depending on the language).
Struct Packing
For wire compatibility, all device I/O structs are tightly packed (no padding).
- C:
#pragma pack(push, 1)/#pragma pack(pop) - C#:
[StructLayout(LayoutKind.Sequential, Pack = 1)] - TypeScript: Manual byte-level encoding/decoding
Example: Keyboard Input (Variable-Length)
Go source with wire tag:
// viiper:wire keyboard c2s modifiers:u8 count:u8 keys:u8*count
type InputState struct {
Modifiers uint8
KeyBitmap [32]uint8 // Internal: 256-bit NKR bitmap
}
Emitted C struct:
#pragma pack(push, 1)
typedef struct {
uint8_t modifiers;
uint8_t count;
uint8_t* keys;
size_t keys_count;
} viiper_keyboard_input_t;
#pragma pack(pop)
Example: Constant and Map Export
Go source (pkg/device/keyboard/const.go):
const (
ModLeftCtrl = 0x01
ModLeftShift = 0x02
KeyA = 0x04
KeyB = 0x05
// ...
)
var CharToKey = map[byte]byte{
'a': KeyA,
'b': KeyB,
'\n': KeyEnter,
// ...
}
Emitted C header (viiper_keyboard.h):
#define VIIPER_KEYBOARD_MODLEFTCTRL 0x1
#define VIIPER_KEYBOARD_MODLEFTSHIFT 0x2
#define VIIPER_KEYBOARD_KEYA 0x4
#define VIIPER_KEYBOARD_KEYB 0x5
// Map lookup function
int viiper_keyboard_char_to_key_lookup(uint8_t key, uint8_t* out_value);
Emitted C# (KeyboardConstants.cs):
public enum Mod : uint
{
LeftCtrl = 0x01,
LeftShift = 0x02,
// ...
}
public enum Key : uint
{
A = 0x04,
B = 0x05,
// ...
}
public static class CharToKey
{
private static readonly Dictionary<byte, Key> _map = new()
{
{ (byte)'a', Key.A },
{ (byte)'b', Key.B },
{ (byte)'\n', Key.Enter },
// ...
};
public static bool TryGetValue(byte key, out Key value)
{
return _map.TryGetValue(key, out value);
}
}
Regeneration Triggers
Run codegen when any of these change:
pkg/apitypes/*.go: API response structurespkg/device/*/inputstate.go: Wire tag annotationspkg/device/*/const.go: Exported constants and map literalsinternal/server/api/*.go: Route registrationsinternal/codegen/generator/**/*.go: Generator templatesinternal/codegen/scanner/**/*.go: Scanner logic (constants, maps, wire tags)
Language-Specific Notes
- C:
#definemacros for constants; switch-based lookup functions for maps; manual memory management for variable-length fields; builds with CMake. - C#: Enums for constant groups;
Dictionary<K,V>with static helper methods for maps;ViiperDeviceclass withOnOutputevent; async/await for management API; struct packing via attributes. - TypeScript: Enums for constant groups;
Record<K, V>objects withGet/Hashelper functions for maps; manual byte encoding viaBinaryWriter/BinaryReader;ViiperDeviceclass with EventEmitter for output;addDeviceAndConnectconvenience method; builds withtsc.
Further Reading
- Design Document: Architectural rationale and detailed generation strategy
- Go Client Documentation: Go reference client usage
- C SDK Documentation: C-specific usage, build, and examples
- C# SDK Documentation: C#-specific usage, async patterns, and map helpers
- TypeScript SDK Documentation: TypeScript-specific usage, EventEmitter patterns, and examples
For questions or contributions, see the main VIIPER repository.