Skip to content

Commit 7ac829e

Browse files
hyperpolymathclaude
andcommitted
feat(v-ecosystem): 6 new API protocol implementations in V-lang
New protocol modules in v-api-interfaces/: - v-websocket: Room-based WebSocket broker (217 lines) - v-jsonrpc: JSON-RPC 2.0 server with batch support (232 lines) - v-mqtt: MQTT 3.1.1 client over raw TCP, QoS 0 (272 lines) - v-trpc: Type-safe RPC router with middleware (254 lines) - v-soap: SOAP 1.1/1.2 envelope builder + parser (221 lines) - v-capnproto: Zero-copy flat serialisation + RPC (288 lines) Joins existing v-rest, v-grpc, v-graphql — now 9 protocol implementations. All have SPDX headers, V error handling, unit tests, README.adoc. Targeting awesome-v submission after ABI/FFI, contractiles, and full test/benchmark suite are added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 802a2d3 commit 7ac829e

File tree

12 files changed

+1569
-0
lines changed

12 files changed

+1569
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
= v-capnproto
2+
// SPDX-License-Identifier: PMPL-1.0-or-later
3+
4+
Cap'n Proto-style zero-copy message builder for the V-Ecosystem API interfaces layer.
5+
Implements flat serialisation into contiguous word-aligned byte segments where deserialisation is a bounds check rather than a copy, following the core principle of the Cap'n Proto wire format.
6+
Provides a message builder with typed field writers/readers (u8 through u64, byte slices, length-prefixed text), struct allocation with data/pointer sections, and a simple RPC header format for request/response framing.
7+
8+
== Author
9+
10+
Jonathan D.A. Jewell
11+
12+
== Source
13+
14+
`src/capnproto.v`
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// V-Ecosystem Cap'n Proto-style Zero-Copy Message Builder
3+
// Author: Jonathan D.A. Jewell
4+
//
5+
// Implements Cap'n Proto-inspired flat serialisation: messages are
6+
// written directly into a contiguous byte buffer with pointer-based
7+
// field access, so deserialisation is a bounds check rather than a
8+
// copy. Includes a simple RPC request/response pattern over the same
9+
// wire format.
10+
//
11+
// This is NOT a full Cap'n Proto implementation — it is a V-native
12+
// approximation of the zero-copy principle using fixed-size segments
13+
// and inline struct layouts.
14+
15+
module capnproto
16+
17+
// --- Constants ---
18+
19+
// Segment alignment: all fields are 8-byte aligned to match Cap'n
20+
// Proto's word size.
21+
const word_size = 8
22+
23+
// Default segment capacity in bytes.
24+
const default_segment_cap = 4096
25+
26+
// --- Segment (contiguous byte buffer) ---
27+
28+
// Segment is a flat byte buffer that structs and lists are written
29+
// into. Pointers are offsets within this buffer.
30+
pub struct Segment {
31+
mut:
32+
data []u8
33+
pos int // Write cursor
34+
}
35+
36+
// new_segment allocates a segment with the given capacity.
37+
pub fn new_segment(capacity int) &Segment {
38+
cap := if capacity < default_segment_cap { default_segment_cap } else { capacity }
39+
return &Segment{
40+
data: []u8{len: cap}
41+
}
42+
}
43+
44+
// alloc reserves `size` bytes (rounded up to word alignment) and
45+
// returns the offset where they start.
46+
pub fn (mut s Segment) alloc(size int) !int {
47+
aligned := align(size)
48+
if s.pos + aligned > s.data.len {
49+
return error('segment overflow: need ${aligned} bytes at offset ${s.pos}, capacity ${s.data.len}')
50+
}
51+
offset := s.pos
52+
s.pos += aligned
53+
return offset
54+
}
55+
56+
// used returns the number of bytes written so far.
57+
pub fn (s &Segment) used() int {
58+
return s.pos
59+
}
60+
61+
// bytes returns the used portion of the segment as a slice.
62+
pub fn (s &Segment) bytes() []u8 {
63+
return s.data[..s.pos]
64+
}
65+
66+
// --- Field writers ---
67+
68+
// write_u8 stores a single byte at the given offset.
69+
pub fn (mut s Segment) write_u8(offset int, val u8) {
70+
s.data[offset] = val
71+
}
72+
73+
// write_u16 stores a 16-bit little-endian integer.
74+
pub fn (mut s Segment) write_u16(offset int, val u16) {
75+
s.data[offset] = u8(val & 0xFF)
76+
s.data[offset + 1] = u8(val >> 8)
77+
}
78+
79+
// write_u32 stores a 32-bit little-endian integer.
80+
pub fn (mut s Segment) write_u32(offset int, val u32) {
81+
s.data[offset] = u8(val & 0xFF)
82+
s.data[offset + 1] = u8((val >> 8) & 0xFF)
83+
s.data[offset + 2] = u8((val >> 16) & 0xFF)
84+
s.data[offset + 3] = u8(val >> 24)
85+
}
86+
87+
// write_u64 stores a 64-bit little-endian integer.
88+
pub fn (mut s Segment) write_u64(offset int, val u64) {
89+
for i in 0 .. 8 {
90+
s.data[offset + i] = u8((val >> (u64(i) * 8)) & 0xFF)
91+
}
92+
}
93+
94+
// write_bytes copies a byte slice at the given offset.
95+
pub fn (mut s Segment) write_bytes(offset int, src []u8) {
96+
for i, b in src {
97+
s.data[offset + i] = b
98+
}
99+
}
100+
101+
// --- Field readers (zero-copy — read directly from the buffer) ---
102+
103+
// read_u8 reads a single byte from the given offset.
104+
pub fn (s &Segment) read_u8(offset int) u8 {
105+
return s.data[offset]
106+
}
107+
108+
// read_u16 reads a 16-bit little-endian integer.
109+
pub fn (s &Segment) read_u16(offset int) u16 {
110+
return u16(s.data[offset]) | (u16(s.data[offset + 1]) << 8)
111+
}
112+
113+
// read_u32 reads a 32-bit little-endian integer.
114+
pub fn (s &Segment) read_u32(offset int) u32 {
115+
mut val := u32(0)
116+
for i in 0 .. 4 {
117+
val |= u32(s.data[offset + i]) << (u32(i) * 8)
118+
}
119+
return val
120+
}
121+
122+
// read_u64 reads a 64-bit little-endian integer.
123+
pub fn (s &Segment) read_u64(offset int) u64 {
124+
mut val := u64(0)
125+
for i in 0 .. 8 {
126+
val |= u64(s.data[offset + i]) << (u64(i) * 8)
127+
}
128+
return val
129+
}
130+
131+
// read_bytes reads a byte slice of the given length from the offset.
132+
pub fn (s &Segment) read_bytes(offset int, length int) []u8 {
133+
return s.data[offset..offset + length]
134+
}
135+
136+
// --- Message builder ---
137+
138+
// MessageBuilder wraps a segment and provides high-level struct and
139+
// text writing.
140+
pub struct MessageBuilder {
141+
mut:
142+
seg &Segment
143+
}
144+
145+
// new_message creates a message builder with a default-sized segment.
146+
pub fn new_message() &MessageBuilder {
147+
return &MessageBuilder{
148+
seg: new_segment(default_segment_cap)
149+
}
150+
}
151+
152+
// new_message_with_capacity creates a message builder with a specific
153+
// segment capacity.
154+
pub fn new_message_with_capacity(cap int) &MessageBuilder {
155+
return &MessageBuilder{
156+
seg: new_segment(cap)
157+
}
158+
}
159+
160+
// alloc_struct reserves space for a struct with the given data and
161+
// pointer section sizes (in bytes). Returns the offset.
162+
pub fn (mut m MessageBuilder) alloc_struct(data_size int, pointer_count int) !int {
163+
total := data_size + pointer_count * word_size
164+
return m.seg.alloc(total)
165+
}
166+
167+
// write_text writes a length-prefixed text blob into the segment.
168+
// Returns the offset where the length prefix starts.
169+
pub fn (mut m MessageBuilder) write_text(text string) !int {
170+
bytes := text.bytes()
171+
total := 4 + bytes.len + 1 // 4-byte length + content + null terminator
172+
offset := m.seg.alloc(total)!
173+
m.seg.write_u32(offset, u32(bytes.len))
174+
m.seg.write_bytes(offset + 4, bytes)
175+
m.seg.write_u8(offset + 4 + bytes.len, 0) // null terminator
176+
return offset
177+
}
178+
179+
// read_text reads a length-prefixed text blob from the given offset.
180+
pub fn (m &MessageBuilder) read_text(offset int) string {
181+
length := m.seg.read_u32(offset)
182+
return m.seg.read_bytes(offset + 4, int(length)).bytestr()
183+
}
184+
185+
// finish returns the serialised message bytes.
186+
pub fn (m &MessageBuilder) finish() []u8 {
187+
return m.seg.bytes()
188+
}
189+
190+
// segment returns the underlying segment for direct field access.
191+
pub fn (m &MessageBuilder) segment() &Segment {
192+
return m.seg
193+
}
194+
195+
// --- RPC message format ---
196+
197+
// RPC messages use a simple header: [method_id: u32][payload_len: u32]
198+
// followed by the payload struct.
199+
200+
// RpcHeader is the wire header for an RPC call or response.
201+
pub struct RpcHeader {
202+
pub:
203+
method_id u32
204+
payload_len u32
205+
}
206+
207+
// write_rpc_header writes an RPC header at the current position and
208+
// returns the offset where the payload should be written.
209+
pub fn (mut m MessageBuilder) write_rpc_header(method_id u32, payload_len u32) !int {
210+
offset := m.seg.alloc(word_size)!
211+
m.seg.write_u32(offset, method_id)
212+
m.seg.write_u32(offset + 4, payload_len)
213+
return offset + int(word_size)
214+
}
215+
216+
// read_rpc_header reads an RPC header from the given offset.
217+
pub fn (m &MessageBuilder) read_rpc_header(offset int) RpcHeader {
218+
return RpcHeader{
219+
method_id: m.seg.read_u32(offset)
220+
payload_len: m.seg.read_u32(offset + 4)
221+
}
222+
}
223+
224+
// --- Utilities ---
225+
226+
// align rounds size up to the nearest word boundary.
227+
fn align(size int) int {
228+
return (size + word_size - 1) & ~(word_size - 1)
229+
}
230+
231+
// --- Tests ---
232+
233+
fn test_alloc_and_read_write() {
234+
mut msg := new_message()
235+
offset := msg.alloc_struct(16, 0) or {
236+
assert false, 'alloc failed: ${err}'
237+
return
238+
}
239+
msg.segment().write_u32(offset, 0xDEADBEEF)
240+
msg.segment().write_u64(offset + 8, 0x0102030405060708)
241+
242+
assert msg.segment().read_u32(offset) == 0xDEADBEEF
243+
assert msg.segment().read_u64(offset + 8) == 0x0102030405060708
244+
}
245+
246+
fn test_text_roundtrip() {
247+
mut msg := new_message()
248+
offset := msg.write_text('hello capnproto') or {
249+
assert false, 'write_text failed: ${err}'
250+
return
251+
}
252+
assert msg.read_text(offset) == 'hello capnproto'
253+
}
254+
255+
fn test_rpc_header() {
256+
mut msg := new_message()
257+
msg.write_rpc_header(42, 128) or {
258+
assert false, 'write_rpc_header failed: ${err}'
259+
return
260+
}
261+
hdr := msg.read_rpc_header(0)
262+
assert hdr.method_id == 42
263+
assert hdr.payload_len == 128
264+
}
265+
266+
fn test_alignment() {
267+
assert align(1) == 8
268+
assert align(8) == 8
269+
assert align(9) == 16
270+
assert align(16) == 16
271+
}
272+
273+
fn test_segment_overflow() {
274+
mut seg := new_segment(16)
275+
seg.alloc(8) or {
276+
assert false, 'first alloc should succeed'
277+
return
278+
}
279+
seg.alloc(8) or {
280+
assert false, 'second alloc should succeed'
281+
return
282+
}
283+
seg.alloc(8) or {
284+
assert err.str().contains('overflow')
285+
return
286+
}
287+
assert false, 'third alloc should have failed'
288+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
= v-jsonrpc
2+
// SPDX-License-Identifier: PMPL-1.0-or-later
3+
4+
JSON-RPC 2.0 server for the V-Ecosystem API interfaces layer.
5+
Implements the full JSON-RPC 2.0 specification over HTTP: single and batch request parsing, method dispatch via a pluggable handler registry, and correctly structured response/error objects with standard error codes (-32700 through -32603).
6+
This is the same wire protocol that the Model Context Protocol (MCP) uses, making it the natural choice for AI tool-server integration.
7+
8+
== Author
9+
10+
Jonathan D.A. Jewell
11+
12+
== Source
13+
14+
`src/jsonrpc.v`

0 commit comments

Comments
 (0)