feat(ffi)!: AimuxError out-param transport + language error models - #91
Conversation
|
ci error |
|
There is something wrong with my mac, wait a minute. |
6af8ffa to
b407fdb
Compare
Replace JSON error envelopes with return-value sentinels plus an optional trailing AimuxError *err out-param (GError-style transport): - success: return the result (non-zero handle / non-NULL string / non-zero stream rc); *err is never touched. failure: sentinel + *err filled when non-NULL. The return value is the only success signal - AimuxError is 24 bytes: code (0-19, append-only; 2..19 mirror the 18 core AiMuxError variants in declaration order), HTTP status or -1, retry_ms or -1, and a callee-allocated message freed via aimux_free_string - classification: JSON parse failures -> AIMUX_E_JSON; null args and invalid handles -> AIMUX_E_INVALID_ARGUMENT - streaming: no on_error callback; terminal failures return 0 + fill err; StreamPart::Error stays data on on_part; per-call stream_ctx replaces TLS - error_value: lossless externally-tagged AiMuxError JSON alongside the Display message (null for boundary-synthesized failures); one reserved pointer slot keeps future ABI room in the frozen 40-byte struct - tests: success-leaves-err-untouched, error_value round-trip, full 18-variant mapping, interior-NUL sanitization, arg/handle classification - docs/api/c.md documents the contract; README updated
One conversion at the language boundary, typed values everywhere inside: Node (napi): errors cross as structured fields, never encoded strings. The tokio stream channel carries Result<String, MappedError>; the async generator yields a StreamItem enum whose ToNapiValue impl (which has an env) builds the throwable via create_throwable. TS owns the class tree: AimuxError base + 18 table-driven subclasses with .code/.status/.retryMs, captureStackTrace, cause chaining; raw re-exports wrapped so instanceof holds; AimuxResult<T> declared via napi dtsHeader. Both surfaces expose errorValue / error_value: the raw externally-tagged AiMuxError JSON (machine-readable companion to the message). Contract tests cover sync/async/stream failures and instanceof narrowing. Python (pyo3): base AimuxError + 18 create_exception! subclasses (RateLimitError, AuthenticationError, APITimeoutError, ...) raised by one to_py_err match; .status/.retry_ms are int | None. The stream channel carries Result<String, AiMuxError> and converts once in __next__.
Each language projects the same 18 variants onto its native dispatch
construct, reading the 40-byte AimuxError out-param through one fromC mapping,
freeing message and error_value exactly once, and exposing errorValue as
the optional raw core-error JSON on each error surface:
- c: AimuxErrorCode switch; examples show the free discipline
- go: single *aimux.Error{Code, Message, Status, RetryMs} + errors.As
(openai-go style); shared ffiString call helper; ctx cancellation wins
over CodeAborted
- java: unchecked AimuxException + 18 subclasses; typed stream callbacks;
jna.library.path wired into tests; arity smoke tests for every symbol
- kotlin: sealed AimuxException hierarchy (when-exhaustive); single
checkError path; fixed put(null) end-of-stream NPE in sequences
- swift: enum AimuxError with associated values, Sendable, optional
status/retryMs; one withCError lifecycle helper
- flutter: sealed-style exception tree via super-parameters;
AimuxUnsupportedError/AimuxTimeoutError avoid shadowing dart:core;
singleton FFI; leak-safe message handling
b407fdb to
a9eac52
Compare
设计脉络(对 #21 的订正)#21 是当时和 AI 一起快速想出来的简单方案,没有太多设计:它正确地杀掉了 后来研究了 GLib 的 GError 设计,确定改用错误指针传递:返回值是唯一的成败信号(0 / NULL / 流 0 = 失败),详情走可选出参
流式路径的同类 TLS( 传输之上,八个语言完成统一错误模型:同一份 18 个 core 变体,投影到各语言的原生分发构造——Python / Node / Java / Kotlin / Flutter 用类树(catch 按类型分发)、Go 单 |
Summary
Breaking change to the C ABI error transport, plus idiomatic error surfaces for every binding language.
Transport (Rust → C)
0/NULL/ stream0) for success vs failure.AimuxError *errcarriescode(0–19),status,retry_ms, andmessage(seeaimux-error.h).*err.on_errorcallback; terminal failures return0+ fillerr.StreamPart::Errorremains data onon_part.Language surfaces (aligned to the same 18 core variants)
AimuxError/AimuxErrorCode*aimux.Error+errors.AsType(Go 1.26; openai-go style)fromCenum AimuxError+ associated values viafromCFields: message, HTTP
status(or −1),retryMs/retry_ms(or −1; 0 = retry now).Commits (review in order)
feat(ffi)!— C transportlist_sessionssignature fixfix:multi-agent review follow-ups (docs/tests stream polarity, channel encode, KotlinfromC, SwiftonError: AimuxError, …)Migration (C consumers / JNA / cgo / dart:ffi)
Before: constructors returned JSON
{"handle"}/{"error"}; failures often sniffed as envelopes.After:
Bindings that still documented envelope returns have been rewired (Java/Kotlin/Swift/Flutter/Go).
Test plan
cargo test -p aimux-ffi— error_detail / native_constructors / provider_handle (rewired for new ABI)cargo checknode + python bindingscompileTestJavaswift buildagainst locallibaimux_ffiOut of scope / follow-ups
errorFromC/ streamAsTypesizeOf == 66064assert in CI with Flutter SDKOthervsInvalidArgument)Docs
docs/api/c.md— transport contractdocs/api/{go,node,python,java,kotlin,swift,flutter}.md— Errors sections