diff --git a/test_error/main.odin b/test_error/main.odin new file mode 100644 index 0000000..0efae95 --- /dev/null +++ b/test_error/main.odin @@ -0,0 +1,28 @@ +package main + +import "core:fmt" +import "core:os" +import "zmq" + +main :: proc() { + ctx := zmq.ctx_new() + defer zmq.ctx_term(ctx) + + rep1, _ := zmq.socket(ctx, .REP) + defer zmq.close(rep1) + zmq.bind(rep1, "tcp://*:5555") + + rep2, _ := zmq.socket(ctx, .REP) + defer zmq.close(rep2) + if err := zmq.bind(rep2, "tcp://*:5555"); err != nil { + fmt.eprintfln("bind failed: %s", zmq.strerror(err)) + switch e in err { + case zmq.ZMQ_Error: + fmt.eprintfln(" variant: ZMQ_Error, member: %v", e) + case os.Platform_Error: + fmt.eprintfln(" variant: Platform_Error, member: %v", e) + case: + fmt.eprintfln(" variant: nil") + } + } +} diff --git a/zmq/err.odin b/zmq/err.odin new file mode 100644 index 0000000..6b7b83b --- /dev/null +++ b/zmq/err.odin @@ -0,0 +1,196 @@ +package zmq + +import "core:c" +import "core:os" + +// ZMQ_HAUSNUMERO — base offset for ZMQ's private error codes. +// +// zmq.h defines two ranges on top of this base: +// +1..18 — fallback POSIX errnos (ENOTSUP, EPROTONOSUPPORT, ... ENETRESET). +// Only used when the platform's C lacks the symbol +// (e.g. MSVC on Windows). On Linux/macOS the system's native +// errno values are used instead, so these never appear from +// zmq_errno() — but we normalize to them for portability (see +// normalize_to_zmq). +// +51..54 — ZMQ-native codes (EFSM, ENOCOMPATPROTO, ETERM, EMTHREAD). +// Always ZMQ-specific, defined unconditionally. +// +// The value is fixed by libzmq's ABI-stability promise (zmq.h): "the value +// of constants must not change, and old values may not be reused". A C +// #define is not a linkable symbol, so hardcoding is the only option. +HAUSNUMERO :: 156384712 + +ZMQ_Error :: enum c.int { + // Fallback POSIX errnos — ZMQ's own values for these errnos, used on + // platforms where the C doesn't define them. We normalize + // all platforms to these values via normalize_to_zmq so that the same + // logical error always has the same value and lands in ZMQ_Error + // regardless of platform. + ENOTSUP = HAUSNUMERO + 1, + EPROTONOSUPPORT = HAUSNUMERO + 2, + ENOBUFS = HAUSNUMERO + 3, + ENETDOWN = HAUSNUMERO + 4, + EADDRINUSE = HAUSNUMERO + 5, + EADDRNOTAVAIL = HAUSNUMERO + 6, + ECONNREFUSED = HAUSNUMERO + 7, + EINPROGRESS = HAUSNUMERO + 8, + ENOTSOCK = HAUSNUMERO + 9, + EMSGSIZE = HAUSNUMERO + 10, + EAFNOSUPPORT = HAUSNUMERO + 11, + ENETUNREACH = HAUSNUMERO + 12, + ECONNABORTED = HAUSNUMERO + 13, + ECONNRESET = HAUSNUMERO + 14, + ENOTCONN = HAUSNUMERO + 15, + ETIMEDOUT = HAUSNUMERO + 16, + EHOSTUNREACH = HAUSNUMERO + 17, + ENETRESET = HAUSNUMERO + 18, + + // ZMQ-native error codes. + EFSM = HAUSNUMERO + 51, + ENOCOMPATPROTO = HAUSNUMERO + 52, + ETERM = HAUSNUMERO + 53, + EMTHREAD = HAUSNUMERO + 54, +} + +// Map platform-native POSIX errnos to ZMQ fallback values. +// +// The 18 errnos below have different numeric values on different platforms +// (e.g. EADDRINUSE is 98 on Linux, 100 on Windows, 48 on macOS). By +// translating them to ZMQ's canonical fallback values (HAUSNUMERO+offset), +// the same logical error always lands in ZMQ_Error with the same value +// on every platform — enabling portable pattern matching. +// +// Errnos not in this list (EAGAIN, EINTR, EACCES, ...) pass through +// unchanged and are classified as Platform_Error. +normalize_to_zmq :: proc(raw: c.int) -> c.int { + #partial switch os.Platform_Error(raw) { + // ENOTSUP shares its value with EOPNOTSUPP on Linux/macOS (both are + // the same constant). On Windows they differ (129 vs 130), but MSVC- + // compiled libzmq uses the ZMQ fallback (HAUSNUMERO+1) anyway, so + // .EOPNOTSUPP here only needs to match the Linux/macOS value. + case .EOPNOTSUPP: + return HAUSNUMERO + 1 + case .EPROTONOSUPPORT: + return HAUSNUMERO + 2 + case .ENOBUFS: + return HAUSNUMERO + 3 + case .ENETDOWN: + return HAUSNUMERO + 4 + case .EADDRINUSE: + return HAUSNUMERO + 5 + case .EADDRNOTAVAIL: + return HAUSNUMERO + 6 + case .ECONNREFUSED: + return HAUSNUMERO + 7 + case .EINPROGRESS: + return HAUSNUMERO + 8 + case .ENOTSOCK: + return HAUSNUMERO + 9 + case .EMSGSIZE: + return HAUSNUMERO + 10 + case .EAFNOSUPPORT: + return HAUSNUMERO + 11 + case .ENETUNREACH: + return HAUSNUMERO + 12 + case .ECONNABORTED: + return HAUSNUMERO + 13 + case .ECONNRESET: + return HAUSNUMERO + 14 + case .ENOTCONN: + return HAUSNUMERO + 15 + case .ETIMEDOUT: + return HAUSNUMERO + 16 + case .EHOSTUNREACH: + return HAUSNUMERO + 17 + case .ENETRESET: + return HAUSNUMERO + 18 + case: + return raw + } +} + +// Reverse of normalize_to_zmq: maps ZMQ fallback values back to +// platform-native errno values. +// +// zmq_strerror only has string entries for ZMQ-native codes (HAUSNUMERO+51..54) +// and delegates everything else to C's strerror, which only knows platform-native +// errno values. The ZMQ fallback values (HAUSNUMERO+1..18) are in a gap — neither +// ZMQ-native nor platform-native — so zmq_strerror returns "Unknown error" for them. +// This function reverses the normalization so strerror produces the correct string. +denormalize_from_zmq :: proc(zmq_val: c.int) -> c.int { + #partial switch ZMQ_Error(zmq_val) { + case .ENOTSUP: + return c.int(os.Platform_Error.EOPNOTSUPP) + case .EPROTONOSUPPORT: + return c.int(os.Platform_Error.EPROTONOSUPPORT) + case .ENOBUFS: + return c.int(os.Platform_Error.ENOBUFS) + case .ENETDOWN: + return c.int(os.Platform_Error.ENETDOWN) + case .EADDRINUSE: + return c.int(os.Platform_Error.EADDRINUSE) + case .EADDRNOTAVAIL: + return c.int(os.Platform_Error.EADDRNOTAVAIL) + case .ECONNREFUSED: + return c.int(os.Platform_Error.ECONNREFUSED) + case .EINPROGRESS: + return c.int(os.Platform_Error.EINPROGRESS) + case .ENOTSOCK: + return c.int(os.Platform_Error.ENOTSOCK) + case .EMSGSIZE: + return c.int(os.Platform_Error.EMSGSIZE) + case .EAFNOSUPPORT: + return c.int(os.Platform_Error.EAFNOSUPPORT) + case .ENETUNREACH: + return c.int(os.Platform_Error.ENETUNREACH) + case .ECONNABORTED: + return c.int(os.Platform_Error.ECONNABORTED) + case .ECONNRESET: + return c.int(os.Platform_Error.ECONNRESET) + case .ENOTCONN: + return c.int(os.Platform_Error.ENOTCONN) + case .ETIMEDOUT: + return c.int(os.Platform_Error.ETIMEDOUT) + case .EHOSTUNREACH: + return c.int(os.Platform_Error.EHOSTUNREACH) + case .ENETRESET: + return c.int(os.Platform_Error.ENETRESET) + case: + return zmq_val + } +} + + +last_error :: proc() -> Error { + raw := errno() + normalized := normalize_to_zmq(raw) + + #partial switch ZMQ_Error(normalized) { + case .ENOTSUP, + .EPROTONOSUPPORT, + .ENOBUFS, + .ENETDOWN, + .EADDRINUSE, + .EADDRNOTAVAIL, + .ECONNREFUSED, + .EINPROGRESS, + .ENOTSOCK, + .EMSGSIZE, + .EAFNOSUPPORT, + .ENETUNREACH, + .ECONNABORTED, + .ECONNRESET, + .ENOTCONN, + .ETIMEDOUT, + .EHOSTUNREACH, + .ENETRESET, + .EFSM, + .ENOCOMPATPROTO, + .ETERM, + .EMTHREAD: + return ZMQ_Error(normalized) + case: + return os.Platform_Error(raw) + } +} + diff --git a/zmq/zmq.odin b/zmq/zmq.odin index 3a669be..14024d7 100644 --- a/zmq/zmq.odin +++ b/zmq/zmq.odin @@ -59,35 +59,17 @@ Flags :: bit_set[Flag;c.int] NoBlock :: Flags{.DONTWAIT} -HAUSNUMERO :: 156384712 - -ZMQ_Error :: enum c.int { - EFSM = HAUSNUMERO + 51, - ENOCOMPATPROTO = HAUSNUMERO + 52, - ETERM = HAUSNUMERO + 53, - EMTHREAD = HAUSNUMERO + 54, -} - Error :: union #shared_nil { os.Platform_Error, ZMQ_Error, } -last_error :: proc() -> Error { - raw := errno() - - if raw >= HAUSNUMERO { - return ZMQ_Error(raw) - } - return os.Platform_Error(raw) -} - strerror :: proc(e: Error) -> cstring { switch v in e { case os.Platform_Error: return _strerror(c.int(v)) case ZMQ_Error: - return _strerror(c.int(v)) + return _strerror(denormalize_from_zmq(c.int(v))) case: return _strerror(0) }