mirror of
https://github.com/zap-proto/zip.git
synced 2026-11-02 16:37:53 +00:00
The op-call plane marshalled its body with encoding/json. That put a text format inside the binary protocol: two services on one host, holding the same struct in memory, serialized it to JSON and parsed it back to cross a socket. JSON is the BOUNDARY encoding — it belongs on the REST routes a browser reaches and in the MCP envelope an agent reads — and it has no business between our own processes. internal/zapenc derives a wire layout from the In/Out type itself: fields take slots in declaration order, each aligned to its own width, which is the rule zap's own schema builder applies. Nothing is named on the wire — a field IS its offset — so the bytes on the socket are the bytes in memory, and the type declaration is the schema both ends read. bool, every sized int/uint, float, string, bytes, nested structs, pointers and slices cross; a map or an interface is refused at encode rather than dropped, because a field that silently does not cross is how a callee decides on a value nobody sent. Refusals cross as ZAP too, status intact, so errors.As still recovers the *HTTPError the callee returned. op.invoke takes its decoder as a PARAMETER, so there is still exactly ONE handler core under REST, MCP, CLI and the plane. The encoding belongs to the transport; the contract is the same op either way. The layout is the type, so the compatibility rule is: append fields at the end, and only at the end. Reordering, inserting or retyping changes the wire. Co-authored-by: Hanzo Dev <[email protected]>
568 lines
14 KiB
Go
568 lines
14 KiB
Go
// Package zapenc encodes a Go value as a ZAP message and reads one back.
|
|
//
|
|
// It is what makes the op-call plane ZAP end to end. An op's In and Out are
|
|
// ordinary Go structs; this derives their wire layout from the type itself, so
|
|
// a service-to-service call carries the same bytes in memory that it puts on
|
|
// the socket, with no second encoding in the middle. JSON stays where it
|
|
// belongs — at the boundary, for browsers — and never inside the binary
|
|
// protocol.
|
|
//
|
|
// # The layout is the type
|
|
//
|
|
// Fields take slots in declaration order, each aligned to its own width, which
|
|
// is the rule zap's own schema builder applies. Nothing is named on the wire:
|
|
// a field IS its offset, exactly as a hand-written ZAP codec would spell it,
|
|
// and the type declaration is the schema both ends read. That is also the whole
|
|
// compatibility rule — REORDERING, INSERTING OR RETYPING A FIELD CHANGES THE
|
|
// WIRE. Append at the end, and only at the end.
|
|
//
|
|
// # What crosses
|
|
//
|
|
// bool, every sized int/uint, float32/64, string, []byte, nested structs,
|
|
// pointers to any of those, and slices. A slice writes each element
|
|
// length-prefixed, which is the variable-element list layout zap.List.BytesAt
|
|
// and ObjectAt already read; a slice of structs therefore carries one complete
|
|
// ZAP message per element.
|
|
//
|
|
// Anything else — a map, an interface, a channel — is refused at encode rather
|
|
// than dropped. A field that silently does not cross is the failure this
|
|
// package exists to make impossible.
|
|
package zapenc
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sync"
|
|
|
|
zap "github.com/zap-proto/go"
|
|
)
|
|
|
|
// Marshal encodes v as a ZAP message. v must be a struct or a pointer to one:
|
|
// the message's root is an object, and there is nothing for a bare scalar to be
|
|
// the root of.
|
|
func Marshal(v any) ([]byte, error) {
|
|
rv := reflect.ValueOf(v)
|
|
for rv.Kind() == reflect.Pointer {
|
|
if rv.IsNil() {
|
|
return nil, nil // a nil input is an absent body, not an empty object
|
|
}
|
|
rv = rv.Elem()
|
|
}
|
|
if rv.Kind() != reflect.Struct {
|
|
return nil, fmt.Errorf("zapenc: %s is not a struct", rv.Type())
|
|
}
|
|
lay, err := layoutOf(rv.Type())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := zap.NewBuilder(lay.size + 256)
|
|
if err := writeStruct(b, lay, rv, true); err != nil {
|
|
return nil, err
|
|
}
|
|
return b.Finish(), nil
|
|
}
|
|
|
|
// Unmarshal reads a ZAP message into v, which must be a non-nil pointer to a
|
|
// struct. An empty message leaves v untouched — a void reply is an absence, not
|
|
// a zero value that a caller might mistake for an answer.
|
|
func Unmarshal(data []byte, v any) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
|
return fmt.Errorf("zapenc: Unmarshal needs a non-nil pointer, got %T", v)
|
|
}
|
|
rv = rv.Elem()
|
|
if rv.Kind() != reflect.Struct {
|
|
return fmt.Errorf("zapenc: %s is not a struct", rv.Type())
|
|
}
|
|
msg, err := zap.Parse(data)
|
|
if err != nil {
|
|
return fmt.Errorf("zapenc: %w", err)
|
|
}
|
|
lay, err := layoutOf(rv.Type())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return readStruct(msg.Root(), lay, rv)
|
|
}
|
|
|
|
// ---- layout ---------------------------------------------------------------
|
|
|
|
type kind uint8
|
|
|
|
const (
|
|
kBool kind = iota
|
|
kInt8
|
|
kInt16
|
|
kInt32
|
|
kInt64
|
|
kUint8
|
|
kUint16
|
|
kUint32
|
|
kUint64
|
|
kFloat32
|
|
kFloat64
|
|
kText
|
|
kBytes
|
|
kStruct
|
|
kSlice
|
|
)
|
|
|
|
type field struct {
|
|
index int
|
|
offset int
|
|
kind kind
|
|
ptr bool // the Go field is a pointer to the encoded type
|
|
elem *layout // kStruct: the nested layout
|
|
slice *sliceElem // kSlice: how one element is encoded
|
|
typ reflect.Type // the Go type at this field, minus any pointer
|
|
}
|
|
|
|
type sliceElem struct {
|
|
kind kind
|
|
elem *layout // when the element is a struct
|
|
typ reflect.Type // the element type, minus any pointer
|
|
ptr bool
|
|
}
|
|
|
|
type layout struct {
|
|
fields []field
|
|
size int
|
|
}
|
|
|
|
var layouts sync.Map // reflect.Type -> *layout
|
|
|
|
func layoutOf(t reflect.Type) (*layout, error) {
|
|
if v, ok := layouts.Load(t); ok {
|
|
return v.(*layout), nil
|
|
}
|
|
lay, err := buildLayout(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layouts.Store(t, lay)
|
|
return lay, nil
|
|
}
|
|
|
|
func buildLayout(t reflect.Type) (*layout, error) {
|
|
lay := &layout{}
|
|
off := 0
|
|
for i := 0; i < t.NumField(); i++ {
|
|
sf := t.Field(i)
|
|
if !sf.IsExported() {
|
|
// An unexported field cannot be read or written by reflection, so it
|
|
// cannot cross. Skipping it silently is right — it was never part of
|
|
// the contract — but it must not consume a slot either.
|
|
continue
|
|
}
|
|
f, err := fieldOf(sf.Type)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s.%s: %w", t.Name(), sf.Name, err)
|
|
}
|
|
f.index = i
|
|
w := width(f.kind)
|
|
off = align(off, w)
|
|
f.offset = off
|
|
off += w
|
|
lay.fields = append(lay.fields, f)
|
|
}
|
|
lay.size = align(off, 8)
|
|
return lay, nil
|
|
}
|
|
|
|
func fieldOf(t reflect.Type) (field, error) {
|
|
f := field{}
|
|
if t.Kind() == reflect.Pointer {
|
|
f.ptr = true
|
|
t = t.Elem()
|
|
}
|
|
f.typ = t
|
|
switch t.Kind() {
|
|
case reflect.Bool:
|
|
f.kind = kBool
|
|
case reflect.Int8:
|
|
f.kind = kInt8
|
|
case reflect.Int16:
|
|
f.kind = kInt16
|
|
case reflect.Int32:
|
|
f.kind = kInt32
|
|
case reflect.Int, reflect.Int64:
|
|
f.kind = kInt64
|
|
case reflect.Uint8:
|
|
f.kind = kUint8
|
|
case reflect.Uint16:
|
|
f.kind = kUint16
|
|
case reflect.Uint32:
|
|
f.kind = kUint32
|
|
case reflect.Uint, reflect.Uint64:
|
|
f.kind = kUint64
|
|
case reflect.Float32:
|
|
f.kind = kFloat32
|
|
case reflect.Float64:
|
|
f.kind = kFloat64
|
|
case reflect.String:
|
|
f.kind = kText
|
|
case reflect.Struct:
|
|
f.kind = kStruct
|
|
lay, err := layoutOf(t)
|
|
if err != nil {
|
|
return f, err
|
|
}
|
|
f.elem = lay
|
|
case reflect.Slice:
|
|
if t.Elem().Kind() == reflect.Uint8 {
|
|
f.kind = kBytes
|
|
break
|
|
}
|
|
f.kind = kSlice
|
|
se, err := sliceElemOf(t.Elem())
|
|
if err != nil {
|
|
return f, err
|
|
}
|
|
f.slice = se
|
|
default:
|
|
return f, fmt.Errorf("zapenc: %s cannot cross the plane; give it a type that can", t.Kind())
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func sliceElemOf(t reflect.Type) (*sliceElem, error) {
|
|
se := &sliceElem{}
|
|
if t.Kind() == reflect.Pointer {
|
|
se.ptr = true
|
|
t = t.Elem()
|
|
}
|
|
se.typ = t
|
|
f, err := fieldOf(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
se.kind = f.kind
|
|
se.elem = f.elem
|
|
if se.kind == kSlice {
|
|
return nil, fmt.Errorf("zapenc: a slice of slices has no wire form; wrap the inner one in a struct")
|
|
}
|
|
return se, nil
|
|
}
|
|
|
|
// width is the fixed-area size of one field. Everything variable — text, bytes,
|
|
// slices and nested structs — carries an offset and a length.
|
|
func width(k kind) int {
|
|
switch k {
|
|
case kBool, kInt8, kUint8:
|
|
return 1
|
|
case kInt16, kUint16:
|
|
return 2
|
|
case kInt32, kUint32, kFloat32:
|
|
return 4
|
|
case kInt64, kUint64, kFloat64:
|
|
return 8
|
|
case kText, kBytes, kSlice, kStruct:
|
|
return 8
|
|
}
|
|
return 8
|
|
}
|
|
|
|
func align(off, n int) int { return (off + n - 1) &^ (n - 1) }
|
|
|
|
// ---- encode ---------------------------------------------------------------
|
|
|
|
func writeStruct(b *zap.Builder, lay *layout, rv reflect.Value, asRoot bool) error {
|
|
// Nested values and list elements must be written BEFORE the object that
|
|
// points at them, because SetObject/SetList take an offset that already
|
|
// exists. Collect them first, then fill the fixed area.
|
|
type pending struct {
|
|
f field
|
|
list int
|
|
length int
|
|
}
|
|
var pends []pending
|
|
for _, f := range lay.fields {
|
|
fv := rv.Field(f.index)
|
|
if f.ptr {
|
|
if fv.IsNil() {
|
|
continue
|
|
}
|
|
fv = fv.Elem()
|
|
}
|
|
switch f.kind {
|
|
case kSlice:
|
|
if fv.Len() == 0 {
|
|
continue
|
|
}
|
|
off, n, err := writeList(b, f.slice, fv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pends = append(pends, pending{f: f, list: off, length: n})
|
|
}
|
|
}
|
|
|
|
ob := b.StartObject(lay.size)
|
|
for _, f := range lay.fields {
|
|
fv := rv.Field(f.index)
|
|
if f.ptr {
|
|
if fv.IsNil() {
|
|
continue // an absent pointer writes nothing: a null, not a zero
|
|
}
|
|
fv = fv.Elem()
|
|
}
|
|
switch f.kind {
|
|
case kBool:
|
|
ob.SetBool(f.offset, fv.Bool())
|
|
case kInt8:
|
|
ob.SetInt8(f.offset, int8(fv.Int()))
|
|
case kInt16:
|
|
ob.SetInt16(f.offset, int16(fv.Int()))
|
|
case kInt32:
|
|
ob.SetInt32(f.offset, int32(fv.Int()))
|
|
case kInt64:
|
|
ob.SetInt64(f.offset, fv.Int())
|
|
case kUint8:
|
|
ob.SetUint8(f.offset, uint8(fv.Uint()))
|
|
case kUint16:
|
|
ob.SetUint16(f.offset, uint16(fv.Uint()))
|
|
case kUint32:
|
|
ob.SetUint32(f.offset, uint32(fv.Uint()))
|
|
case kUint64:
|
|
ob.SetUint64(f.offset, fv.Uint())
|
|
case kFloat32:
|
|
ob.SetFloat32(f.offset, float32(fv.Float()))
|
|
case kFloat64:
|
|
ob.SetFloat64(f.offset, fv.Float())
|
|
case kText:
|
|
ob.SetText(f.offset, fv.String())
|
|
case kBytes:
|
|
ob.SetBytes(f.offset, fv.Bytes())
|
|
case kStruct:
|
|
// A nested value is a complete ZAP message stored as bytes, so a reader
|
|
// Parses it on its own — the same rule list elements follow, rather
|
|
// than a second one for the singular case.
|
|
inner, err := marshalStruct(f.elem, fv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ob.SetBytes(f.offset, inner)
|
|
}
|
|
}
|
|
for _, p := range pends {
|
|
ob.SetList(p.f.offset, p.list, p.length)
|
|
}
|
|
if asRoot {
|
|
ob.FinishAsRoot()
|
|
} else {
|
|
ob.Finish()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeList lays out a variable-element list: each element length-prefixed with
|
|
// a little-endian uint32, which is the layout zap.List.BytesAt and ObjectAt
|
|
// read. The returned length is the ELEMENT COUNT, never a byte count.
|
|
func writeList(b *zap.Builder, se *sliceElem, rv reflect.Value) (int, int, error) {
|
|
var blob []byte
|
|
for i := 0; i < rv.Len(); i++ {
|
|
ev := rv.Index(i)
|
|
if se.ptr {
|
|
if ev.IsNil() {
|
|
ev = reflect.Zero(se.typ)
|
|
} else {
|
|
ev = ev.Elem()
|
|
}
|
|
}
|
|
enc, err := marshalElem(se, ev)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
var n [4]byte
|
|
binary.LittleEndian.PutUint32(n[:], uint32(len(enc)))
|
|
blob = append(blob, n[:]...)
|
|
blob = append(blob, enc...)
|
|
}
|
|
return b.WriteBytes(blob), rv.Len(), nil
|
|
}
|
|
|
|
// marshalElem encodes one list element. A struct element is a complete ZAP
|
|
// message; a scalar element is its own little-endian bytes, and text is its
|
|
// UTF-8 — so an element is always self-describing given the field's type.
|
|
func marshalElem(se *sliceElem, ev reflect.Value) ([]byte, error) {
|
|
switch se.kind {
|
|
case kStruct:
|
|
return marshalStruct(se.elem, ev)
|
|
case kText:
|
|
return []byte(ev.String()), nil
|
|
case kBytes:
|
|
return ev.Bytes(), nil
|
|
case kBool:
|
|
if ev.Bool() {
|
|
return []byte{1}, nil
|
|
}
|
|
return []byte{0}, nil
|
|
case kInt8, kInt16, kInt32, kInt64:
|
|
return leInt(uint64(ev.Int()), width(se.kind)), nil
|
|
case kUint8, kUint16, kUint32, kUint64:
|
|
return leInt(ev.Uint(), width(se.kind)), nil
|
|
case kFloat32:
|
|
return leInt(uint64(f32bits(float32(ev.Float()))), 4), nil
|
|
case kFloat64:
|
|
return leInt(f64bits(ev.Float()), 8), nil
|
|
}
|
|
return nil, fmt.Errorf("zapenc: list element of kind %d has no wire form", se.kind)
|
|
}
|
|
|
|
func marshalStruct(lay *layout, rv reflect.Value) ([]byte, error) {
|
|
b := zap.NewBuilder(lay.size + 128)
|
|
if err := writeStruct(b, lay, rv, true); err != nil {
|
|
return nil, err
|
|
}
|
|
return b.Finish(), nil
|
|
}
|
|
|
|
func leInt(v uint64, n int) []byte {
|
|
var buf [8]byte
|
|
binary.LittleEndian.PutUint64(buf[:], v)
|
|
return append([]byte(nil), buf[:n]...)
|
|
}
|
|
|
|
// ---- decode ---------------------------------------------------------------
|
|
|
|
func readStruct(o zap.Object, lay *layout, rv reflect.Value) error {
|
|
for _, f := range lay.fields {
|
|
fv := rv.Field(f.index)
|
|
target := fv
|
|
if f.ptr {
|
|
// Allocate only once something is actually there, so an absent field
|
|
// reads back as nil rather than as a pointer to a zero value.
|
|
nv := reflect.New(f.typ)
|
|
target = nv.Elem()
|
|
if err := readField(o, f, target); err != nil {
|
|
return err
|
|
}
|
|
if !target.IsZero() {
|
|
fv.Set(nv)
|
|
}
|
|
continue
|
|
}
|
|
if err := readField(o, f, target); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readField(o zap.Object, f field, target reflect.Value) error {
|
|
switch f.kind {
|
|
case kBool:
|
|
target.SetBool(o.Bool(f.offset))
|
|
case kInt8:
|
|
target.SetInt(int64(o.Int8(f.offset)))
|
|
case kInt16:
|
|
target.SetInt(int64(o.Int16(f.offset)))
|
|
case kInt32:
|
|
target.SetInt(int64(o.Int32(f.offset)))
|
|
case kInt64:
|
|
target.SetInt(o.Int64(f.offset))
|
|
case kUint8:
|
|
target.SetUint(uint64(o.Uint8(f.offset)))
|
|
case kUint16:
|
|
target.SetUint(uint64(o.Uint16(f.offset)))
|
|
case kUint32:
|
|
target.SetUint(uint64(o.Uint32(f.offset)))
|
|
case kUint64:
|
|
target.SetUint(o.Uint64(f.offset))
|
|
case kFloat32:
|
|
target.SetFloat(float64(o.Float32(f.offset)))
|
|
case kFloat64:
|
|
target.SetFloat(o.Float64(f.offset))
|
|
case kText:
|
|
target.SetString(o.Text(f.offset))
|
|
case kBytes:
|
|
if b := o.Bytes(f.offset); len(b) > 0 {
|
|
target.SetBytes(append([]byte(nil), b...))
|
|
}
|
|
case kStruct:
|
|
raw := o.Bytes(f.offset)
|
|
if len(raw) == 0 {
|
|
return nil // absent nested value: leave the zero struct
|
|
}
|
|
msg, err := zap.Parse(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("zapenc: nested: %w", err)
|
|
}
|
|
return readStruct(msg.Root(), f.elem, target)
|
|
case kSlice:
|
|
return readList(o, f, target)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readList(o zap.Object, f field, target reflect.Value) error {
|
|
l := o.List(f.offset)
|
|
n := l.Len()
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
out := reflect.MakeSlice(target.Type(), n, n)
|
|
for i := 0; i < n; i++ {
|
|
ev := out.Index(i)
|
|
if f.slice.ptr {
|
|
nv := reflect.New(f.slice.typ)
|
|
if err := readElem(l, i, f.slice, nv.Elem()); err != nil {
|
|
return err
|
|
}
|
|
ev.Set(nv)
|
|
continue
|
|
}
|
|
if err := readElem(l, i, f.slice, ev); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
target.Set(out)
|
|
return nil
|
|
}
|
|
|
|
func readElem(l zap.List, i int, se *sliceElem, target reflect.Value) error {
|
|
if se.kind == kStruct {
|
|
return readStruct(l.ObjectAt(i), se.elem, target)
|
|
}
|
|
raw := l.BytesAt(i)
|
|
switch se.kind {
|
|
case kText:
|
|
target.SetString(string(raw))
|
|
case kBytes:
|
|
target.SetBytes(append([]byte(nil), raw...))
|
|
case kBool:
|
|
target.SetBool(len(raw) > 0 && raw[0] != 0)
|
|
case kInt8, kInt16, kInt32, kInt64:
|
|
target.SetInt(signed(raw, width(se.kind)))
|
|
case kUint8, kUint16, kUint32, kUint64:
|
|
target.SetUint(unsigned(raw))
|
|
case kFloat32:
|
|
target.SetFloat(float64(f32from(uint32(unsigned(raw)))))
|
|
case kFloat64:
|
|
target.SetFloat(f64from(unsigned(raw)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func unsigned(b []byte) uint64 {
|
|
var buf [8]byte
|
|
copy(buf[:], b)
|
|
return binary.LittleEndian.Uint64(buf[:])
|
|
}
|
|
|
|
func signed(b []byte, n int) int64 {
|
|
v := unsigned(b)
|
|
shift := uint(64 - 8*n)
|
|
return int64(v<<shift) >> shift // sign-extend from the field's own width
|
|
}
|
|
|
|
func f32bits(f float32) uint32 { return math.Float32bits(f) }
|
|
func f64bits(f float64) uint64 { return math.Float64bits(f) }
|
|
func f32from(u uint32) float32 { return math.Float32frombits(u) }
|
|
func f64from(u uint64) float64 { return math.Float64frombits(u) }
|