Cyclone

EN/VI

Wire format specification · v1.0.0 · Draft

Cyclone Protocol

Một binary wire format xác định.
Không hơn. Không kém.

Cyclone định nghĩa dữ liệu logic được biến thành byte như thế nào, và byte được biến ngược lại thành dữ liệu logic như thế nào.

Thứ tự

Specification First

Specification là nguồn sự thật duy nhất. Không bao giờ sửa spec để chiều một implementation. Mọi thứ khác đều nằm phía sau nó.

Thứ tự Cyclone đi theo

  1. RFC
  2. Compiler
  3. SDK
  4. Application

Không phải thế này

  1. SDK
  2. Protocol

Một protocol bị nắn theo bất kỳ thứ gì thư viện đầu tiên của nó tình cờ làm.

Phép thử áp cho từng câu của specification: một người viết encoder bằng assembly trên vi điều khiển 8-bit có làm theo được không? Nếu không, câu đó đang mô tả một implementation, và nó không thuộc về spec.

Định nghĩa

Cyclone là gì?

Cyclone là một protocol.

Cyclone không phải

  • × RPC
  • × Networking
  • × Runtime
  • × Serialization Library

Cyclone chỉ quy định

  1. Model
  2. Bytes
  3. Model

Transport, retransmission, thứ tự gói, matchmaking, mã hoá, nén, checksum — tất cả đều xây phía trên Cyclone. Specification dừng lại đúng ở chỗ byte được hình thành.

Triết lý

Mười nguyên tắc, hai tầng.

Bảy nguyên tắc là điều kiện tuân thủ. Ba nguyên tắc là lựa chọn của Reference Implementation. Trộn lẫn hai thứ đó chính là cách một protocol âm thầm biến thành một framework.

Nguyên tắc của Specification

Vi phạm một trong số này thì không phải Cyclone.

01 Specification First Specification là nguồn sự thật duy nhất. Không bao giờ sửa spec để chiều một implementation.
02 Protocol over Framework Cyclone định nghĩa dữ liệu hiện ra trên wire như thế nào — không định nghĩa API, không định nghĩa runtime.
03 Minimal Wire Format Gửi dữ liệu, không bao giờ gửi mô tả dữ liệu. Không tên field, không type info, không tag, không padding.
04 Deterministic by Design Một giá trị, đúng một chuỗi byte. Không được tồn tại hai cách encode hợp lệ cho cùng một dữ liệu.
05 Language Neutral Không ngôn ngữ nào là ngôn ngữ chính. Không chi tiết nào trong format ưu ái một type system.
06 Implementation Independent Không quy định parser, macro hay generator nào. Spec mô tả byte rồi dừng ở đó.
07 Implementation Replaceable Reference Implementation không phải chuẩn. Thay toàn bộ nó, wire format vẫn không đổi.

Nguyên tắc của Reference Implementation

Là lựa chọn thiết kế, không phải điều kiện tuân thủ. Một implementation khác được phép chọn khác, miễn byte sinh ra không đổi.

08 User Owns the Model Không bắt buộc tầng DTO, không base class — RI đọc những field bạn đánh dấu và bỏ qua phần còn lại.
09 Compile-time over Runtime Thứ gì xác định được lúc build thì xong lúc build.
10 Zero Runtime Reflection Wire format cho phép điều đó; spec không bắt buộc. AOT và bare-metal vẫn khả thi.

Quy tắc quyết định, gồm hai câu hỏi. Một: phát biểu này có quan sát được từ byte stream không? Không → nó thuộc về Reference Implementation và không được đưa vào spec. Hai: nó có vi phạm một trong bảy nguyên tắc trên không? Có → không thuộc Cyclone.

Đánh đổi

Những gì Cyclone cố ý không làm

Mỗi điều dưới đây đều là cái giá trực tiếp của một đảm bảo ở trên. Không điều nào nằm trong roadmap.

Không có schema evolution

Vị trí là định danh field duy nhất, nên thêm, bớt hay đổi thứ tự một field đều là breaking change. v1 không có thương lượng phiên bản, không có migration cho field.

Không backward / forward compatibility

Không có unknown-field skipping, không có default, nên client cũ không đọc được message của server mới. Hai đầu deploy cùng nhau, hoặc không deploy.

Không metadata, không tự mô tả

Stream không mang tên field, kiểu hay tag — bên nhận bắt buộc phải biết trước thứ tự và kiểu của từng field. Hệ quả nguy hiểm: decode sai định nghĩa thường không báo lỗi gì cả. Nó sinh ra giá trị sai, im lặng.

Không có IDL bắt buộc

Nguồn sự thật là Schema, nhưng spec không quy định bạn phải viết nó ra bằng cách nào — không IDL bắt buộc, không file .cyclone bắt buộc. Reference Implementation diễn đạt Schema bằng annotation ngay trên type có sẵn của bạn, chỉ trích ra ba thứ: tên type, các field, và Cyclone type của từng field. Đổi lại: không có một tài liệu schema độc lập để đưa cho bên thứ ba — hai phía tự lo việc rút ra cùng một Schema.

Không Optional, không nullable, không default

Mọi field đều mang giá trị. v1 không có cách encode field vắng mặt — và chính vì vậy không có chỗ nào mơ hồ về việc sự vắng mặt sẽ được ghi ra sao.

Không transport, không framing, không crypto

Cyclone sinh ra một chuỗi byte rồi dừng. Ranh giới message, retransmission, thứ tự gói, mã hoá và nén đều là phần bạn tự xây phía trên.

Nếu có điều nào không chấp nhận được với bài toán của bạn, một format có tag ID và optional field sẽ phục vụ tốt hơn. Đó là lựa chọn đúng, không phải thất bại của Cyclone.

Specification

RFC-0002

Wire format

Little endian, luôn luôn. Kích thước cố định, không varint. Không tag, không header, không padding, không terminator. Vị trí là định danh duy nhất.

Primitive

Kích thước cố định, little endian. Một u32 là bốn byte kể cả khi giá trị bằng không.

u32   100        → [64 00 00 00]
u32   0x12345678 → [78 56 34 12]
i32   -1         → [FF FF FF FF]
u16   300        → [2C 01]
bool  true       → [01]
bool  0x02không hợp lệ
f32   3.14       → [C3 F5 48 40]
f32   -0.0       → [00 00 00 80]

Float giữ nguyên bit pattern IEEE 754. Không normalize, không canonical NaN.

String

Độ dài UInt32, rồi tới byte UTF-8. Độ dài đếm byte, không bao giờ đếm ký tự.

"abc"      → [03 00 00 00] [61 62 63]
"中"        → [03 00 00 00] [E4 B8 AD]
"Xin chào" → [09 00 00 00] [58 69 6E 20 63 68 C3 A0 6F]
""         → [00 00 00 00]

“中” là một ký tự và ba byte. Độ dài MUST là 3. Vùng byte phải là UTF-8 hợp lệ.

Bytes

Độ dài UInt32, rồi tới byte thô. Giống hệt String, trừ ràng buộc UTF-8.

[]         → [00 00 00 00]
[FF FE]    → [02 00 00 00] [FF FE]
[00 00]    → [02 00 00 00] [00 00]

Đúng những byte mà String phải từ chối. Kiểu đến từ schema — không bao giờ suy ra từ nội dung.

Model

Các field nối liền nhau theo thứ tự khai báo. Không metadata, không field ID, không padding.

model Item { id: u32; name: String }
Item{ 42, "Sword" }
  → [2A 00 00 00] [05 00 00 00] [53 77 6F 72 64]

model Mixed { a: u8; b: u32; c: u8 }
Mixed{ 1, 2, 3 } → [01 02 00 00 00 03]   6 byte
                   12 byte = đang chép layout bộ nhớ

Compiler được phép sắp xếp lại field trong RAM. Việc đó không được đổi một byte nào của output.

Array

Số lượng UInt32, rồi từng phần tử theo hình dạng byte của T. Đếm phần tử, không đếm byte.

Array<u32> [1,2,3]
  → [03 00 00 00] [01 00 00 00] [02 00 00 00] [03 00 00 00]

Array<String> ["a","bc"]
  → [02 00 00 00] [01 00 00 00 61] [02 00 00 00 62 63]

Array<Array<u8>> [[1,2],[]]
  → [02 00 00 00] [02 00 00 00 01 02] [00 00 00 00]

Số lượng của [1,2,3] là 3, không phải 12. Mảng con rỗng là số lượng 0 và không byte nào thêm.

Enum

Luôn là UInt32. Bốn byte, bất kể nó khai báo ít thành viên tới đâu.

enum PlayerState { Idle=0, Walk=1, Run=2, Jump=3 }

Idle → [00 00 00 00]
Walk → [01 00 00 00]
Jump → [03 00 00 00]
99   → không hợp lệ — ngoài tập đã định nghĩa

Đây là quyết định của protocol, không phải tối ưu lưu trữ. Không có gì để hai đầu thương lượng.

Một message đầy đủ, từng byte một

Ví dụ từ RFC-0002 §15 — Model lồng, string, số nguyên, bool — encode ra 31 byte. Rê chuột lên một field để làm sáng những byte nó sở hữu, hoặc rê lên một byte để tìm chủ của nó.

model Vector3 { x: f32; y: f32; z: f32 }

model GameMessage {
  playerId:   u32       = 42
  playerName: String    = "Knight"
  position:   Vector3   = { 10.5, 20.3, -5.1 }
  health:     u32       = 100
  isAlive:    bool      = true
}
Hex dump · 31 byte Vị trí là định danh duy nhất
00000000 2A000000060000004B6E696768740000
00000010 28416666A2413333A3C06400000001
Tổng31 byte · 0 byte metadata

Không có gì ngăn cách các field. Không length prefix cho Model lồng, không dấu phân cách giữa positionhealth, không terminator ở cuối. Decoder biết đang đọc field nào thuần tuý nhờ con trỏ đã đi được bao xa — và đó chính là lý do đổi thứ tự field là breaking change, còn đổi tên field thì miễn phí.

RFC-0003

Test vector

Một test vector là một cặp (giá trị logic, chuỗi byte). Không bao giờ là cặp (lời gọi hàm, giá trị trả về) — một vector không nêu tên method nào, ngôn ngữ nào, API nào.

InputByte mong đợi

Primitive

IDInputByte mong đợi
P-002bool true01
P-011u8 255FF
P-021u16 3002C 01
P-032u32 0x1234567878 56 34 12
P-035i32 −214748364800 00 00 80
P-042u64 2⁶⁴−1FF FF FF FF FF FF FF FF
F-005f32 0x4048F5C3 · 3.14C3 F5 48 40
F-002f32 0x80000000 · −0.000 00 00 80
F-031f32 0x7FC00001 · NaN01 00 C0 7F
S-004String “中”03 00 00 00 E4 B8 AD
S-005String “Xin chào”09 00 00 00 58 69 6E 20 63 68 C3 A0 6F

P-032 bắt lỗi endianness: một implementation big-endian sẽ ghi 12 34 56 78 và trượt ở đây. F-031 bắt lỗi canonical hoá NaN — cho ra 00 00 C0 7F nghĩa là dữ liệu người dùng đã bị sửa.

Model

IDInputByte mong đợi
T-001Item{ id=42, name="Sword" }2A 00 00 00 05 00 00 00 53 77 6F 72 64
T-002Empty{}— 0 byte
T-003Vector3{1.5, 2.5, 3.5}00 00 C0 3F 00 00 20 40 00 00 60 40
T-004Body{ pos=Vector3{1.5,2.5,3.5}, hp=100 }00 00 C0 3F 00 00 20 40 00 00 60 40 64 00 00 00
T-010Mixed{ a=1, b=2, c=3 }01 02 00 00 00 03
T-011Order{ z=1, a=2 }01 02

T-010 bắt lỗi chép layout bộ nhớ (12 byte) và lỗi compiler sắp xếp lại field. T-011 bắt những implementation sắp field theo thứ tự chữ cái “cho ổn định” — 02 01 là bug.

Array

IDInputByte mong đợi
A-001Array<u32> []00 00 00 00
A-002Array<u32> [1,2,3]03 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00
A-003Array<String> ["a","bc"]02 00 00 00 01 00 00 00 61 02 00 00 00 62 63
A-004Array<Array<u8>> [[1,2],[]]02 00 00 00 02 00 00 00 01 02 00 00 00 00
A-005Array<bool> [true,false,true]03 00 00 00 01 00 01

A-002 bắt lỗi nhầm số lượng với độ dài byte: số lượng đúng là 3, không phải 12.

Enum

IDInputByte mong đợi
E-001PlayerState::Idle00 00 00 00
E-002PlayerState::Walk01 00 00 00
E-003PlayerState::Jump03 00 00 00
E-010Kích thước encode của mọi enumluôn 4 byte

E-010 bắt những implementation “tối ưu” một enum bốn thành viên xuống còn một byte.

Stream không hợp lệ

IDByte đầu vàoKết quả mong đợi
N-001bool · 02từ chối · không phải 00/01
N-010u32 · 00 00 00từ chối · EOF bất ngờ
N-020String · 02 00 00 00 FF FEtừ chối · UTF-8 không hợp lệ
N-021String · 03 00 00 00 ED A0 80từ chối · surrogate bị encode
N-023String · 64 00 00 00 41 6C 69 63 65từ chối · độ dài 100, còn 5 byte
N-030Array<u32> · 03 00 00 00 01 00 00 00từ chối · số lượng 3, có một phần tử
N-031Array<u32> · FF FF FF FFtừ chối · trước khi cấp phát
N-040enum{0,1} · 63 00 00 00từ chối · giá trị 99 không định nghĩa

Từ chối là MUST; tên lỗi chỉ là SHOULD. N-023 và N-031 kiểm đúng một thứ: độ dài có được kiểm tra trước khi cấp phát không? Pass N-031 bằng cách crash rồi bắt exception thì không tính là pass.

Bộ đầy đủ — gồm số nguyên 8 byte, f64, kiểm tra round-trip và kiểm tra chéo giữa các implementation — nằm trong RFC-0003.

Conformance

Cyclone Compatible

Một implementation là Cyclone Compatible khi và chỉ khi nó pass 100% test vector.

  • RFC-0002
  • Test Vector
  • Output xác định

Không có “tương thích một phần”. Không có 98%. Một vector trượt nghĩa là tồn tại một dữ liệu mà implementation này và một implementation khác bất đồng — im lặng, trên production.

Một implementation chậm gấp 10 lần nhưng pass 100% vector Cyclone Compatible.
Implementation nhanh nhất thế giới mà trượt một vector thì không.

Mã nguồn

Công cụ

Công cụ chính thức

Compiler chính thức Đọc một schema, sinh ra codec. Một trong nhiều cách hợp lệ để thoả RFC-0002. Đang làm
CLI chính thức Soi một byte stream, chạy bộ vector conformance, xuất báo cáo. Đang làm
SDK tham chiếu Một encoder/decoder mẫu để đọc song song với spec, không phải thứ spec phụ thuộc vào. Đang làm

Những công cụ này là reference implementation của Cyclone Protocol. Chúng không phải là một phần của protocol.

Thay cả ba, wire format không đổi. Không có gì trong Cyclone bắt bạn phải dùng chúng — chúng tồn tại để được đọc, và để bị vượt qua.

Bắt đầu