diff options
| author | gingerBill <gingerBill@users.noreply.github.com> | 2026-01-16 13:25:03 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-16 13:25:03 +0000 |
| commit | d46c547264c2be4ff46887d96354e653dbd6069d (patch) | |
| tree | 245f7cc22efcb64061069321a3671453cbcb78aa | |
| parent | a2fa32a518357aefa11edd0978f48625be7dc9e5 (diff) | |
| parent | 57d02cb14850e7b241f5ec519ff5e44c6129a5fe (diff) | |
Merge pull request #6124 from laytan/nbio
Add `core:nbio`
88 files changed, 11906 insertions, 488 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6219f370..0d2f3f1ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,18 +156,14 @@ jobs: - name: Check benchmarks run: ./odin check tests/benchmark -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -no-entry-point + - name: Odin check examples/all for Linux i386 if: matrix.os == 'ubuntu-latest' run: ./odin check examples/all -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -target:linux_i386 - - name: Odin check examples/all for Linux arm64 - if: matrix.os == 'ubuntu-latest' - run: ./odin check examples/all -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -target:linux_arm64 - - name: Odin check examples/all for FreeBSD amd64 - if: matrix.os == 'ubuntu-latest' - run: ./odin check examples/all -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -target:freebsd_amd64 - name: Odin check examples/all for OpenBSD amd64 if: matrix.os == 'ubuntu-latest' run: ./odin check examples/all -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -target:openbsd_amd64 + - name: Odin check examples/all for js_wasm32 if: matrix.os == 'ubuntu-latest' run: ./odin check examples/all -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -no-entry-point -target:js_wasm32 @@ -178,12 +174,6 @@ jobs: - name: Odin check examples/all/sdl3 for Linux i386 if: matrix.os == 'ubuntu-latest' run: ./odin check examples/all/sdl3 -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -no-entry-point -target:linux_i386 - - name: Odin check examples/all/sdl3 for Linux arm64 - if: matrix.os == 'ubuntu-latest' - run: ./odin check examples/all/sdl3 -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -no-entry-point -target:linux_arm64 - - name: Odin check examples/all/sdl3 for FreeBSD amd64 - if: matrix.os == 'ubuntu-latest' - run: ./odin check examples/all/sdl3 -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -no-entry-point -target:freebsd_amd64 - name: Odin check examples/all/sdl3 for OpenBSD amd64 if: matrix.os == 'ubuntu-latest' run: ./odin check examples/all/sdl3 -vet -vet-tabs -strict-style -vet-style -warnings-as-errors -disallow-do -no-entry-point -target:openbsd_amd64 diff --git a/base/runtime/core_builtin.odin b/base/runtime/core_builtin.odin index 6528fdd1b..02278a356 100644 --- a/base/runtime/core_builtin.odin +++ b/base/runtime/core_builtin.odin @@ -360,7 +360,7 @@ new_aligned :: proc($T: typeid, alignment: int, allocator := context.allocator, @(builtin, require_results) new_clone :: proc(data: $T, allocator := context.allocator, loc := #caller_location) -> (t: ^T, err: Allocator_Error) #optional_allocator_error { - t = (^T)(raw_data(mem_alloc_bytes(size_of(T), align_of(T), allocator, loc) or_return)) + t = (^T)(raw_data(mem_alloc_non_zeroed(size_of(T), align_of(T), allocator, loc) or_return)) if t != nil { t^ = data } diff --git a/core/container/pool/pool.odin b/core/container/pool/pool.odin new file mode 100644 index 000000000..01fb29f2d --- /dev/null +++ b/core/container/pool/pool.odin @@ -0,0 +1,140 @@ +package container_pool + +import "base:intrinsics" +import "base:sanitizer" + +import "core:mem" +import "core:sync" + +_ :: sanitizer + +DEFAULT_BLOCK_SIZE :: _DEFAULT_BLOCK_SIZE + +Pool_Arena :: _Pool_Arena + +/* +A thread-safe (between init and destroy) object pool backed by virtual growing arena returning stable pointers. +The element type requires an intrusive link node. + +Example: + Elem :: struct { + link: ^Elem, + } + + p: pool.Pool(Elem) + pool.init(&p, "link") +*/ +Pool :: struct($T: typeid) { + arena: Pool_Arena, + num_outstanding: int, + num_ready: int, + link_off: uintptr, + free_list: ^T, +} + +@(require_results) +init :: proc(p: ^Pool($T), $link_field: string, block_size: uint = DEFAULT_BLOCK_SIZE) -> (err: mem.Allocator_Error) + where intrinsics.type_has_field(T, link_field), + intrinsics.type_field_type(T, link_field) == ^T { + p.link_off = offset_of_by_string(T, link_field) + return _pool_arena_init(&p.arena, block_size) +} + +destroy :: proc(p: ^Pool($T)) { + elem := sync.atomic_exchange_explicit(&p.free_list, nil, .Acquire) + + sync.atomic_store_explicit(&p.num_ready, 0, .Relaxed) + + when .Address in ODIN_SANITIZER_FLAGS { + for ; elem != nil; elem = _get_next(p, elem) { + _unpoison_elem(p, elem) + } + } else { + _ = elem + } + + _pool_arena_destroy(&p.arena) + p.arena = {} +} + +@(require_results) +get :: proc(p: ^Pool($T)) -> (elem: ^T, err: mem.Allocator_Error) #optional_allocator_error { + defer sync.atomic_add_explicit(&p.num_outstanding, 1, .Relaxed) + + for { + elem = sync.atomic_load_explicit(&p.free_list, .Acquire) + if elem == nil { + // NOTE: pool arena has an internal lock. + return new(T, _pool_arena_allocator(&p.arena)) + } + + if _, ok := sync.atomic_compare_exchange_weak_explicit(&p.free_list, elem, _get_next(p, elem), .Acquire, .Relaxed); ok { + _set_next(p, elem, nil) + _unpoison_elem(p, elem) + sync.atomic_sub_explicit(&p.num_ready, 1, .Relaxed) + return + } + } +} + +put :: proc(p: ^Pool($T), elem: ^T) { + mem.zero_item(elem) + _poison_elem(p, elem) + + defer sync.atomic_sub_explicit(&p.num_outstanding, 1, .Relaxed) + defer sync.atomic_add_explicit(&p.num_ready, 1, .Relaxed) + + for { + head := sync.atomic_load_explicit(&p.free_list, .Relaxed) + _set_next(p, elem, head) + if _, ok := sync.atomic_compare_exchange_weak_explicit(&p.free_list, head, elem, .Release, .Relaxed); ok { + return + } + } +} + +num_outstanding :: proc(p: ^Pool($T)) -> int { + return sync.atomic_load(&p.num_outstanding) +} + +num_ready :: proc(p: ^Pool($T)) -> int { + return sync.atomic_load(&p.num_ready) +} + +cap :: proc(p: ^Pool($T)) -> int { + return sync.atomic_load(&p.num_ready) + sync.atomic_load(&p.num_outstanding) +} + +_get_next :: proc(p: ^Pool($T), elem: ^T) -> ^T { + return (^^T)(uintptr(elem) + p.link_off)^ +} + +_set_next :: proc(p: ^Pool($T), elem: ^T, next: ^T) { + (^^T)(uintptr(elem) + p.link_off)^ = next +} + +@(disabled=.Address not_in ODIN_SANITIZER_FLAGS) +_poison_elem :: proc(p: ^Pool($T), elem: ^T) { + if p.link_off > 0 { + sanitizer.address_poison_rawptr(elem, int(p.link_off)) + } + + len := size_of(T) - p.link_off - size_of(rawptr) + if len > 0 { + ptr := rawptr(uintptr(elem) + p.link_off + size_of(rawptr)) + sanitizer.address_poison_rawptr(ptr, int(len)) + } +} + +@(disabled=.Address not_in ODIN_SANITIZER_FLAGS) +_unpoison_elem :: proc(p: ^Pool($T), elem: ^T) { + if p.link_off > 0 { + sanitizer.address_unpoison_rawptr(elem, int(p.link_off)) + } + + len := size_of(T) - p.link_off - size_of(rawptr) + if len > 0 { + ptr := rawptr(uintptr(elem) + p.link_off + size_of(rawptr)) + sanitizer.address_unpoison_rawptr(ptr, int(len)) + } +} diff --git a/core/container/pool/pool_arena_others.odin b/core/container/pool/pool_arena_others.odin new file mode 100644 index 000000000..3069076f7 --- /dev/null +++ b/core/container/pool/pool_arena_others.odin @@ -0,0 +1,29 @@ +#+build !darwin +#+build !freebsd +#+build !openbsd +#+build !netbsd +#+build !linux +#+build !windows +#+private +package container_pool + +import "base:runtime" + +import "core:mem" + +_Pool_Arena :: runtime.Arena + +_DEFAULT_BLOCK_SIZE :: mem.Megabyte + +_pool_arena_init :: proc(arena: ^Pool_Arena, block_size: uint = DEFAULT_BLOCK_SIZE) -> (err: runtime.Allocator_Error) { + runtime.arena_init(arena, block_size, runtime.default_allocator()) or_return + return +} + +_pool_arena_allocator :: proc(arena: ^Pool_Arena) -> runtime.Allocator { + return runtime.arena_allocator(arena) +} + +_pool_arena_destroy :: proc(arena: ^Pool_Arena) { + runtime.arena_destroy(arena) +} diff --git a/core/container/pool/pool_arena_virtual.odin b/core/container/pool/pool_arena_virtual.odin new file mode 100644 index 000000000..192e60260 --- /dev/null +++ b/core/container/pool/pool_arena_virtual.odin @@ -0,0 +1,24 @@ +#+build darwin, freebsd, openbsd, netbsd, linux, windows +package container_pool + +import "base:runtime" + +import "core:mem" +import "core:mem/virtual" + +_Pool_Arena :: virtual.Arena + +_DEFAULT_BLOCK_SIZE :: mem.Gigabyte + +_pool_arena_init :: proc(arena: ^Pool_Arena, block_size: uint = DEFAULT_BLOCK_SIZE) -> (err: runtime.Allocator_Error) { + virtual.arena_init_growing(arena, block_size) or_return + return +} + +_pool_arena_allocator :: proc(arena: ^Pool_Arena) -> runtime.Allocator { + return virtual.arena_allocator(arena) +} + +_pool_arena_destroy :: proc(arena: ^Pool_Arena) { + virtual.arena_destroy(arena) +} diff --git a/core/container/rbtree/rbtree.odin b/core/container/rbtree/rbtree.odin index 35ce21413..e892188d7 100644 --- a/core/container/rbtree/rbtree.odin +++ b/core/container/rbtree/rbtree.odin @@ -91,7 +91,7 @@ destroy :: proc(t: ^$T/Tree($Key, $Value), call_on_remove: bool = true) { } } -len :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> (node_count: int) { +len :: proc "contextless" (t: $T/Tree($Key, $Value)) -> (node_count: int) { return t._size } @@ -108,7 +108,7 @@ last :: proc "contextless" (t: ^$T/Tree($Key, $Value)) -> ^Node(Key, Value) { } // find finds the key in the tree, and returns the corresponding node, or nil iff the value is not present. -find :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (node: ^Node(Key, Value)) { +find :: proc(t: $T/Tree($Key, $Value), key: Key) -> (node: ^Node(Key, Value)) { node = t._root for node != nil { switch t._cmp_fn(key, node.key) { @@ -121,7 +121,7 @@ find :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (node: ^Node(Key, Value)) { } // find_value finds the key in the tree, and returns the corresponding value, or nil iff the value is not present. -find_value :: proc(t: ^$T/Tree($Key, $Value), key: Key) -> (value: Value, ok: bool) #optional_ok { +find_value :: proc(t: $T/Tree($Key, $Value), key: Key) -> (value: Value, ok: bool) #optional_ok { if n := find(t, key); n != nil { return n.value, true } @@ -166,7 +166,7 @@ remove :: proc { // removal was successful. While the node's key + value will be left intact, // the node itself will be freed via the tree's node allocator. remove_key :: proc(t: ^$T/Tree($Key, $Value), key: Key, call_on_remove := true) -> bool { - n := find(t, key) + n := find(t^, key) if n == nil { return false // Key not found, nothing to do } diff --git a/core/container/xar/xar.odin b/core/container/xar/xar.odin index 8ed5bc3e4..07fdf5a15 100644 --- a/core/container/xar/xar.odin +++ b/core/container/xar/xar.odin @@ -417,9 +417,10 @@ Create an iterator for traversing the exponential array. Example: - import "lib:xar" + import "core:container/xar" + import "core:fmt" - iteration_example :: proc() { + iterator_example :: proc() { x: xar.Array(int, 4) defer xar.destroy(&x) diff --git a/core/flags/constants.odin b/core/flags/constants.odin index dc2663e2a..154ed3cec 100644 --- a/core/flags/constants.odin +++ b/core/flags/constants.odin @@ -11,8 +11,7 @@ NO_CORE_NAMED_TYPES :: #config(ODIN_CORE_FLAGS_NO_CORE_NAMED_TYPES, false) IMPORTING_TIME :: #config(ODIN_CORE_FLAGS_USE_TIME, time.IS_SUPPORTED) // Override support for parsing `net` types. -// TODO: Update this when the BSDs are supported. -IMPORTING_NET :: #config(ODIN_CORE_FLAGS_USE_NET, ODIN_OS == .Windows || ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .FreeBSD) +IMPORTING_NET :: #config(ODIN_CORE_FLAGS_USE_NET, ODIN_OS == .Windows || ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .FreeBSD || ODIN_OS == .NetBSD || ODIN_OS == .OpenBSD) TAG_ARGS :: "args" SUBTAG_NAME :: "name" diff --git a/core/flags/errors.odin b/core/flags/errors.odin index 3d34a95d3..e9b2e18c8 100644 --- a/core/flags/errors.odin +++ b/core/flags/errors.odin @@ -1,5 +1,7 @@ package flags +import "base:runtime" +import "core:net" import "core:os" Parse_Error_Reason :: enum { @@ -24,6 +26,12 @@ Parse_Error :: struct { message: string, } +Unified_Parse_Error_Reason :: union #shared_nil { + Parse_Error_Reason, + runtime.Allocator_Error, + net.Parse_Endpoint_Error, +} + // Raised during parsing. // Provides more granular information than what just a string could hold. Open_File_Error :: struct { diff --git a/core/flags/errors_bsd.odin b/core/flags/errors_bsd.odin deleted file mode 100644 index 4d98d2ee4..000000000 --- a/core/flags/errors_bsd.odin +++ /dev/null @@ -1,9 +0,0 @@ -#+build netbsd, openbsd -package flags - -import "base:runtime" - -Unified_Parse_Error_Reason :: union #shared_nil { - Parse_Error_Reason, - runtime.Allocator_Error, -} diff --git a/core/flags/errors_nonbsd.odin b/core/flags/errors_nonbsd.odin deleted file mode 100644 index 28912b57f..000000000 --- a/core/flags/errors_nonbsd.odin +++ /dev/null @@ -1,12 +0,0 @@ -#+build !netbsd -#+build !openbsd -package flags - -import "base:runtime" -import "core:net" - -Unified_Parse_Error_Reason :: union #shared_nil { - Parse_Error_Reason, - runtime.Allocator_Error, - net.Parse_Endpoint_Error, -} diff --git a/core/flags/internal_rtti.odin b/core/flags/internal_rtti.odin index a1b050597..b3880afa0 100644 --- a/core/flags/internal_rtti.odin +++ b/core/flags/internal_rtti.odin @@ -5,6 +5,7 @@ import "base:intrinsics" import "base:runtime" import "core:fmt" import "core:mem" +import "core:net" import "core:os" import "core:reflect" import "core:strconv" @@ -310,7 +311,18 @@ parse_and_set_pointer_by_named_type :: proc(ptr: rawptr, str: string, data_type: } when IMPORTING_NET { - if try_net_parse_workaround(data_type, str, ptr, out_error) { + if data_type == net.Host_Or_Endpoint { + addr, net_error := net.parse_hostname_or_endpoint(str) + if net_error != nil { + // We pass along `net.Error` here. + out_error^ = Parse_Error { + net_error, + "Invalid Host/Endpoint.", + } + return + } + + (cast(^net.Host_Or_Endpoint)ptr)^ = addr return } } diff --git a/core/flags/internal_rtti_nonbsd.odin b/core/flags/internal_rtti_nonbsd.odin deleted file mode 100644 index e1286186b..000000000 --- a/core/flags/internal_rtti_nonbsd.odin +++ /dev/null @@ -1,32 +0,0 @@ -#+private -#+build !netbsd -#+build !openbsd -package flags - -import "core:net" - -// This proc exists purely as a workaround for import restrictions. -// Returns true if caller should return early. -try_net_parse_workaround :: #force_inline proc ( - data_type: typeid, - str: string, - ptr: rawptr, - out_error: ^Error, -) -> bool { - if data_type == net.Host_Or_Endpoint { - addr, net_error := net.parse_hostname_or_endpoint(str) - if net_error != nil { - // We pass along `net.Error` here. - out_error^ = Parse_Error { - net_error, - "Invalid Host/Endpoint.", - } - return true - } - - (cast(^net.Host_Or_Endpoint)ptr)^ = addr - return true - } - - return false -} diff --git a/core/nbio/doc.odin b/core/nbio/doc.odin new file mode 100644 index 000000000..c43196923 --- /dev/null +++ b/core/nbio/doc.odin @@ -0,0 +1,195 @@ +/* +package nbio implements a non-blocking I/O and event loop abstraction layer +over several platform-specific asynchronous I/O APIs. + +More examples can be found in Odin's examples repository +at [[ examples/nbio ; https://github.com/odin-lang/examples/nbio ]]. + +**Event Loop**: + +Each thread may have at most one event loop associated with it. +This is enforced by the package, as running multiple event loops on a single +thread does not make sense. + +Event loops are reference counted and managed by the package. + +`acquire_thread_event_loop` and `release_thread_event_loop` can be used +to acquire and release a reference. Acquiring must be done before any operation +is done. + +The event loop progresses in ticks. A tick checks if any work is to be done, +and based on the given timeout may block waiting for work. + +Ticks are typically done using the `tick`, `run`, and `run_until` procedures. + +Example: + package main + + import "core:nbio" + import "core:time" + import "core:fmt" + + main :: proc() { + err := nbio.acquire_thread_event_loop() + assert(err == nil) + defer nbio.release_thread_event_loop() + + nbio.timeout(time.Second, proc(_: ^nbio.Operation) { + fmt.println("Hellope after 1 second!") + }) + + err = nbio.run() + assert(err == nil) + } + + +**Time and timeouts**: + +Timeouts are intentionally *slightly inaccurate* by design. + +A timeout is not checked continuously, instead, it is evaluated only when +a tick occurs. This means if a tick took a long time, your timeout may be ready +for a bit of time already before the callback is called. + +The function `now` returns the current time as perceived by the event +loop. This value is cached at least once per tick so it is fast to retrieve. + +Most operations also take an optional timeout when executed. +If the timeout completes before the operation, the operation is cancelled and +called back with a `.Timeout` error. + + +**Threading**: + +The package has a concept of I/O threads (threads that are ticking) and worker +threads (any other thread). + +An I/O thread is mostly self contained, operations are executed on it, and +callbacks run on it. + +If you try to execute an operation on a thread that has no running event loop +a panic will be executed. Instead a worker thread can execute operations onto +a running event loop by taking it's reference and executing operations with +that reference. + +In this case: +- The operation is enqueued from the worker thread +- The I/O thread is optionally woken up from blocking for work with `wake_up` +- The next tick, the operation is executed by the I/O thread +- The callback is invoked on the I/O thread + +Example: + package main + + import "core:nbio" + import "core:net" + import "core:thread" + import "core:time" + + Connection :: struct { + loop: ^nbio.Event_Loop, + socket: net.TCP_Socket, + } + + main :: proc() { + workers: thread.Pool + thread.pool_init(&workers, context.allocator, 2) + thread.pool_start(&workers) + + err := nbio.acquire_thread_event_loop() + defer nbio.release_thread_event_loop() + assert(err == nil) + + server, listen_err := nbio.listen_tcp({nbio.IP4_Any, 1234}) + assert(listen_err == nil) + nbio.accept_poly(server, &workers, on_accept) + + err = nbio.run() + assert(err == nil) + + on_accept :: proc(op: ^nbio.Operation, workers: ^thread.Pool) { + assert(op.accept.err == nil) + + nbio.accept_poly(op.accept.socket, workers, on_accept) + + thread.pool_add_task(workers, context.allocator, do_work, new_clone(Connection{ + loop = op.l, + socket = op.accept.client, + })) + } + + do_work :: proc(t: thread.Task) { + connection := (^Connection)(t.data) + + // Imagine CPU intensive work that's been ofloaded to a worker thread. + time.sleep(time.Second * 1) + + nbio.send_poly(connection.socket, {transmute([]byte)string("Hellope!\n")}, connection, on_sent, l=connection.loop) + } + + on_sent :: proc(op: ^nbio.Operation, connection: ^Connection) { + assert(op.send.err == nil) + // Client got our message, clean up. + nbio.close(connection.socket) + free(connection) + } + } + + +**Handle and socket association**: + +Most platforms require handles (files, sockets, etc.) to be explicitly +associated with an event loop or configured for non-blocking/asynchronous +operation. + +On some platforms (notably Windows), this requires a specific flag at open +time (`.Non_Blocking` for `core:os`) and association may fail if the handle was not created +correctly. + +For this reason, prefer `open` and `create_socket` from this package instead. + +`associate_handle`, `associate_file`, and `associate_socket` can be used for externally opened +files/sockets. + + +**Offsets and positional I/O**: + +Operations do not implicitly use or modify a handle’s internal file +offset. + +Instead, operations such as `read` and `write` are *positional* and require +an explicit offset. + +This avoids ambiguity and subtle bugs when multiple asynchronous operations +are issued concurrently against the same handle. + + +**Contexts and callbacks**: + +The `context` inside a callback is *not* the context that submitted the +operation. + +Instead, the callback receives the context that was active when the event +loop function (`tick`, `run`, etc.) was called. + +This is because otherwise the context would have to be copied and held onto for each operation. + +If the submitting context is required inside the callback, it must be copied +into the operation’s user data explicitly. + +Example: + nbio.timeout_poly(time.Second, new_clone(context), proc(_: ^Operation, ctx: ^runtime.Context) { + context = ctx^ + free(ctx) + }) + + +**Callback scheduling guarantees**: + +Callbacks are guaranteed to be invoked in a later tick, never synchronously. +This means that the operation returned from a procedure is at least valid till the end of the +current tick, because an operation is freed after it's callback is called. +Thus you can set user data after an execution is queued, or call `remove`, removing subtle "race" +conditions and simplifying control flow. +*/ +package nbio diff --git a/core/nbio/errors.odin b/core/nbio/errors.odin new file mode 100644 index 000000000..f3bd381d5 --- /dev/null +++ b/core/nbio/errors.odin @@ -0,0 +1,84 @@ +package nbio + +import "base:intrinsics" + +import "core:reflect" + +Error :: intrinsics.type_merge( + Network_Error, + union #shared_nil { + General_Error, + FS_Error, + }, +) +#assert(size_of(Error) == 8) + +// Errors regarding general usage of the event loop. +General_Error :: enum i32 { + None, + + Allocation_Failed = i32(PLATFORM_ERR_ALLOCATION_FAILED), + Unsupported = i32(PLATFORM_ERR_UNSUPPORTED), +} + +// Errors gotten from file system operations. +FS_Error :: enum i32 { + None, + Unsupported = i32(PLATFORM_ERR_UNSUPPORTED), + Allocation_Failed = i32(PLATFORM_ERR_ALLOCATION_FAILED), + Timeout = i32(PLATFORM_ERR_TIMEOUT), + Invalid_Argument = i32(PLATFORM_ERR_INVALID_ARGUMENT), + Permission_Denied = i32(PLATFORM_ERR_PERMISSION_DENIED), + EOF = i32(PLATFORM_ERR_EOF), + Exists = i32(PLATFORM_ERR_EXISTS), + Not_Found = i32(PLATFORM_ERR_NOT_FOUND), +} + +Platform_Error :: _Platform_Error + +error_string :: proc(err: Error) -> string { + err := err + variant := any{ + id = reflect.union_variant_typeid(err), + data = &err, + } + str := reflect.enum_string(variant) + + if str == "" { + #partial switch uerr in err { + case FS_Error: + str, _ = reflect.enum_name_from_value(Platform_Error(uerr)) + case General_Error: + str, _ = reflect.enum_name_from_value(Platform_Error(uerr)) + } + } + if str == "" { + str = "Unknown" + } + + return str +} + +error_string_recv :: proc(recv_err: Recv_Error) -> string { + switch err in recv_err { + case TCP_Recv_Error: return error_string(err) + case UDP_Recv_Error: return error_string(err) + case: return "Unknown" + } +} + +error_string_send :: proc(send_err: Send_Error) -> string { + switch err in send_err { + case TCP_Send_Error: return error_string(err) + case UDP_Send_Error: return error_string(err) + case: return "Unknown" + } +} + +error_string_sendfile :: proc(send_err: Send_File_Error) -> string { + switch err in send_err { + case TCP_Send_Error: return error_string(err) + case FS_Error: return error_string(err) + case: return "Unknown" + } +} diff --git a/core/nbio/errors_linux.odin b/core/nbio/errors_linux.odin new file mode 100644 index 000000000..5c3472500 --- /dev/null +++ b/core/nbio/errors_linux.odin @@ -0,0 +1,16 @@ +#+private +package nbio + +import "core:sys/linux" + +PLATFORM_ERR_UNSUPPORTED :: linux.Errno.ENOSYS +PLATFORM_ERR_ALLOCATION_FAILED :: linux.Errno.ENOMEM +PLATFORM_ERR_TIMEOUT :: linux.Errno.ECANCELED +PLATFORM_ERR_INVALID_ARGUMENT :: linux.Errno.EINVAL +PLATFORM_ERR_OVERFLOW :: linux.Errno.E2BIG +PLATFORM_ERR_NOT_FOUND :: linux.Errno.ENOENT +PLATFORM_ERR_EXISTS :: linux.Errno.EEXIST +PLATFORM_ERR_PERMISSION_DENIED :: linux.Errno.EPERM +PLATFORM_ERR_EOF :: -100 // There is no EOF errno, we use negative for our own error codes. + +_Platform_Error :: linux.Errno diff --git a/core/nbio/errors_others.odin b/core/nbio/errors_others.odin new file mode 100644 index 000000000..f27c91178 --- /dev/null +++ b/core/nbio/errors_others.odin @@ -0,0 +1,20 @@ +#+build !darwin +#+build !freebsd +#+build !openbsd +#+build !netbsd +#+build !linux +#+build !windows +#+private +package nbio + +PLATFORM_ERR_UNSUPPORTED :: 1 +PLATFORM_ERR_ALLOCATION_FAILED :: 2 +PLATFORM_ERR_TIMEOUT :: 3 +PLATFORM_ERR_INVALID_ARGUMENT :: 4 +PLATFORM_ERR_OVERFLOW :: 5 +PLATFORM_ERR_NOT_FOUND :: 6 +PLATFORM_ERR_EXISTS :: 7 +PLATFORM_ERR_PERMISSION_DENIED :: 8 +PLATFORM_ERR_EOF :: 9 + +_Platform_Error :: enum i32 {} diff --git a/core/nbio/errors_posix.odin b/core/nbio/errors_posix.odin new file mode 100644 index 000000000..3dd8f781d --- /dev/null +++ b/core/nbio/errors_posix.odin @@ -0,0 +1,17 @@ +#+build darwin, freebsd, netbsd, openbsd +#+private +package nbio + +import "core:sys/posix" + +PLATFORM_ERR_UNSUPPORTED :: posix.Errno.ENOSYS +PLATFORM_ERR_ALLOCATION_FAILED :: posix.Errno.ENOMEM +PLATFORM_ERR_TIMEOUT :: posix.Errno.ECANCELED +PLATFORM_ERR_INVALID_ARGUMENT :: posix.Errno.EINVAL +PLATFORM_ERR_OVERFLOW :: posix.Errno.E2BIG +PLATFORM_ERR_NOT_FOUND :: posix.Errno.ENOENT +PLATFORM_ERR_EXISTS :: posix.Errno.EEXIST +PLATFORM_ERR_PERMISSION_DENIED :: posix.Errno.EPERM +PLATFORM_ERR_EOF :: -100 // There is no EOF errno, we use negative for our own error codes. + +_Platform_Error :: posix.Errno diff --git a/core/nbio/errors_windows.odin b/core/nbio/errors_windows.odin new file mode 100644 index 000000000..d9b3b7e4d --- /dev/null +++ b/core/nbio/errors_windows.odin @@ -0,0 +1,17 @@ +#+private +package nbio + +import win "core:sys/windows" + +PLATFORM_ERR_UNSUPPORTED :: win.System_Error.NOT_SUPPORTED + +PLATFORM_ERR_ALLOCATION_FAILED :: win.System_Error.OUTOFMEMORY +PLATFORM_ERR_TIMEOUT :: win.System_Error.WAIT_TIMEOUT +PLATFORM_ERR_INVALID_ARGUMENT :: win.System_Error.BAD_ARGUMENTS +PLATFORM_ERR_OVERFLOW :: win.System_Error.BUFFER_OVERFLOW +PLATFORM_ERR_NOT_FOUND :: win.System_Error.FILE_NOT_FOUND +PLATFORM_ERR_EXISTS :: win.System_Error.FILE_EXISTS +PLATFORM_ERR_PERMISSION_DENIED :: win.System_Error.ACCESS_DENIED +PLATFORM_ERR_EOF :: win.System_Error.HANDLE_EOF + +_Platform_Error :: win.System_Error diff --git a/core/nbio/impl.odin b/core/nbio/impl.odin new file mode 100644 index 000000000..3f5191c5e --- /dev/null +++ b/core/nbio/impl.odin @@ -0,0 +1,256 @@ +#+private +package nbio + +import "base:runtime" +import "base:intrinsics" + +import "core:container/pool" +import "core:container/queue" +import "core:net" +import "core:strings" +import "core:sync" +import "core:time" +import "core:reflect" + +@(init, private) +init_thread_local_cleaner :: proc "contextless" () { + runtime.add_thread_local_cleaner(proc() { + l := &_tls_event_loop + if l.refs > 0 { + l.refs = 1 + _release_thread_event_loop() + } + }) +} + +@(thread_local) +_tls_event_loop: Event_Loop + +_acquire_thread_event_loop :: proc() -> General_Error { + l := &_tls_event_loop + if l.err == nil && l.refs == 0 { + when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 && ODIN_OS != .Orca { + allocator := runtime.default_wasm_allocator() + } else { + allocator := runtime.heap_allocator() + } + + l.queue.data.allocator = allocator + + if pool_err := pool.init(&l.operation_pool, "_pool_link"); pool_err != nil { + l.err = .Allocation_Failed + return l.err + } + defer if l.err != nil { pool.destroy(&l.operation_pool) } + + l.err = _init(l, allocator) + l.now = time.now() + } + + if l.err != nil { + return l.err + } + + l.refs += 1 + return nil +} + +_release_thread_event_loop :: proc() { + l := &_tls_event_loop + if l.err != nil { + assert(l.refs == 0) + return + } + + if l.refs > 0 { + l.refs -= 1 + if l.refs == 0 { + queue.destroy(&l.queue) + pool.destroy(&l.operation_pool) + _destroy(l) + l^ = {} + } + } +} + +_current_thread_event_loop :: #force_inline proc(loc := #caller_location) -> (^Event_Loop) { + l := &_tls_event_loop + + if intrinsics.expect(l.refs == 0, false) { + return nil + } + + return l +} + +_tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) { + // Receive operations queued from other threads first. + { + sync.guard(&l.queue_mu) + for op in queue.pop_front_safe(&l.queue) { + _exec(op) + } + } + + return __tick(l, timeout) +} + +_listen_tcp :: proc( + l: ^Event_Loop, + endpoint: Endpoint, + backlog := 1000, + loc := #caller_location, +) -> ( + socket: TCP_Socket, + err: Network_Error, +) { + family := family_from_endpoint(endpoint) + socket = create_tcp_socket(family, l, loc) or_return + defer if err != nil { close(socket, l=l) } + + net.set_option(socket, .Reuse_Address, true) + + bind(socket, endpoint) or_return + + _listen(socket, backlog) or_return + return +} + +_read_entire_file :: proc(l: ^Event_Loop, path: string, user_data: rawptr, cb: Read_Entire_File_Callback, allocator := context.allocator, dir := CWD) { + open_poly3(path, user_data, cb, allocator, on_open, dir=dir, l=l) + + on_open :: proc(op: ^Operation, user_data: rawptr, cb: Read_Entire_File_Callback, allocator: runtime.Allocator) { + if op.open.err != nil { + cb(user_data, nil, {.Open, op.open.err}) + return + } + + stat_poly3(op.open.handle, user_data, cb, allocator, on_stat) + } + + on_stat :: proc(op: ^Operation, user_data: rawptr, cb: Read_Entire_File_Callback, allocator: runtime.Allocator) { + if op.stat.err != nil { + close(op.stat.handle) + cb(user_data, nil, {.Stat, op.stat.err}) + return + } + + if op.stat.type != .Regular { + close(op.stat.handle) + cb(user_data, nil, {.Stat, .Unsupported}) + return + } + + buf, err := make([]byte, op.stat.size, allocator) + if err != nil { + close(op.stat.handle) + cb(user_data, nil, {.Read, .Allocation_Failed}) + return + } + + read_poly3(op.stat.handle, 0, buf, user_data, cb, allocator, on_read, all=true) + } + + on_read :: proc(op: ^Operation, user_data: rawptr, cb: Read_Entire_File_Callback, allocator: runtime.Allocator) { + close(op.read.handle) + + if op.read.err != nil { + delete(op.read.buf, allocator) + cb(user_data, nil, {.Read, op.read.err}) + return + } + + assert(op.read.read == len(op.read.buf)) + cb(user_data, op.read.buf, {}) + } +} + +NBIO_DEBUG :: #config(NBIO_DEBUG, false) + +Debuggable :: union { + Operation_Type, + string, + int, + time.Time, + time.Duration, +} + +@(disabled=!NBIO_DEBUG) +debug :: proc(contents: ..Debuggable, location := #caller_location) { + if context.logger.procedure == nil || .Debug < context.logger.lowest_level { + return + } + + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + + b: strings.Builder + b.buf.allocator = context.temp_allocator + + strings.write_string(&b, "[nbio] ") + + for content, i in contents { + switch val in content { + case Operation_Type: + name, _ := reflect.enum_name_from_value(val) + strings.write_string(&b, name) + case string: + strings.write_string(&b, val) + case int: + strings.write_int(&b, val) + case time.Duration: + ms := time.duration_milliseconds(val) + strings.write_f64(&b, ms, 'f') + strings.write_string(&b, "ms") + + case time.Time: + buf: [time.MIN_HMS_LEN+1]byte + h, m, s, ns := time.precise_clock_from_time(val) + buf[8] = '.' + buf[7] = '0' + u8(s % 10); s /= 10 + buf[6] = '0' + u8(s) + buf[5] = ':' + buf[4] = '0' + u8(m % 10); m /= 10 + buf[3] = '0' + u8(m) + buf[2] = ':' + buf[1] = '0' + u8(h % 10); h /= 10 + buf[0] = '0' + u8(h) + + strings.write_string(&b, string(buf[:])) + strings.write_int(&b, ns) + } + + if i < len(contents)-1 { + strings.write_byte(&b, ' ') + } + } + + context.logger.procedure(context.logger.data, .Debug, strings.to_string(b), context.logger.options, location) +} + +warn :: proc(text: string, location := #caller_location) { + if context.logger.procedure == nil || .Warning < context.logger.lowest_level { + return + } + + context.logger.procedure(context.logger.data, .Warning, text, context.logger.options, location) +} + +@(require_results) +constraint_bufs_to_max_rw :: proc(bufs: [][]byte) -> (constrained: [][]byte, total: int) { + for buf in bufs { + total += len(buf) + } + + constrained = bufs + for n := total; n > MAX_RW; { + last := &constrained[len(constrained)-1] + take := min(len(last), n-MAX_RW) + last^ = last[:take] + if len(last) == 0 { + constrained = constrained[:len(constrained)-1] + } + n -= take + } + + return +} diff --git a/core/nbio/impl_linux.odin b/core/nbio/impl_linux.odin new file mode 100644 index 000000000..cdcbeedc1 --- /dev/null +++ b/core/nbio/impl_linux.odin @@ -0,0 +1,1421 @@ +#+private file +package nbio + +import "base:intrinsics" + +import "core:container/pool" +import "core:container/queue" +import "core:mem" +import "core:net" +import "core:slice" +import "core:strings" +import "core:sys/linux" +import "core:sys/linux/uring" +import "core:time" + +@(private="package") +_FULLY_SUPPORTED :: true + +@(private="package") +_Event_Loop :: struct { + ring: uring.Ring, + // Ready to be submitted to kernel, if kernel is full. + unqueued: queue.Queue(^Operation), + // Ready to run callbacks, mainly next tick, some other ops that error outside the kernel. + completed: queue.Queue(^Operation), + allocator: mem.Allocator, + wake: ^Operation, +} + +@(private="package") +_Handle :: linux.Fd + +@(private="package") +_CWD :: linux.AT_FDCWD + +@(private="package") +MAX_RW :: mem.Gigabyte + +@(private="package") +_Operation :: struct { + removal: ^Operation, + sqe: ^linux.IO_Uring_SQE, + expires: linux.Time_Spec, +} + +@(private="package") +_Accept :: struct { + sockaddr: linux.Sock_Addr_Any, + sockaddr_len: i32, +} + +@(private="package") +_Close :: struct {} + +@(private="package") +_Dial :: struct { + sockaddr: linux.Sock_Addr_Any, +} + +@(private="package") +_Read :: struct {} + +@(private="package") +_Write :: struct {} + +@(private="package") +_Send :: struct { + endpoint: linux.Sock_Addr_Any, + msghdr: linux.Msg_Hdr, + small_bufs: [1][]byte, +} + +@(private="package") +_Recv :: struct { + addr_out: linux.Sock_Addr_Any, + msghdr: linux.Msg_Hdr, + small_bufs: [1][]byte, +} + +@(private="package") +_Timeout :: struct { + expires: linux.Time_Spec, +} + +@(private="package") +_Poll :: struct {} + +@(private="package") +_Remove :: struct { + target: ^Operation, +} + +@(private="package") +_Link_Timeout :: struct { + target: ^Operation, + expires: linux.Time_Spec, +} + +@(private="package") +_Send_File :: struct { + len: int, + pipe: Handle, + + splice: ^Operation, +} + +@(private="package") +_Splice :: struct { + off: int, + len: int, + file: Handle, + pipe: Handle, + + written: int, + + sendfile: ^Operation, +} + +@(private="package") +_Open :: struct { + cpath: cstring, +} + +@(private="package") +_Stat :: struct { + buf: linux.Statx, +} + +@(private="package") +_init :: proc(l: ^Event_Loop, alloc: mem.Allocator) -> (err: General_Error) { + l.allocator = alloc + + params := uring.DEFAULT_PARAMS + params.flags += {.SUBMIT_ALL, .COOP_TASKRUN, .SINGLE_ISSUER} + + uerr := uring.init(&l.ring, ¶ms, QUEUE_SIZE) + if uerr != nil { + err = General_Error(uerr) + return + } + defer if err != nil { uring.destroy(&l.ring) } + + if perr := queue.init(&l.unqueued, allocator = alloc); perr != nil { + err = .Allocation_Failed + return + } + defer if err != nil { queue.destroy(&l.unqueued) } + + if perr := queue.init(&l.completed, allocator = alloc); perr != nil { + err = .Allocation_Failed + return + } + defer if err != nil { queue.destroy(&l.completed) } + + set_up_wake_up(l) or_return + + return + + set_up_wake_up :: proc(l: ^Event_Loop) -> General_Error { + wakefd, wakefd_err := linux.eventfd(0, {.SEMAPHORE, .CLOEXEC, .NONBLOCK}) + if wakefd_err != nil { + return General_Error(wakefd_err) + } + + op, alloc_err := new(Operation, l.allocator) + if alloc_err != nil { + linux.close(wakefd) + return .Allocation_Failed + } + + l.wake = op + l.wake.detached = true + l.wake.l = l + l.wake.type = .Read + l.wake.cb = wake_up_callback + l.wake.read.handle = wakefd + l.wake.read.buf = ([^]byte)(&l.wake.user_data)[:8] + _exec(l.wake) + + return nil + } + + wake_up_callback :: proc(op: ^Operation) { + assert(op.type == .Read) + assert(op == op.l.wake) + assert(op.read.err == nil) + assert(op.read.read == 8) + value := intrinsics.unaligned_load((^u64)(&op.user_data)) + assert(value > 0) + debug(int(value), "wake_up calls handled") + + op.read.read = 0 + op.user_data = {} + _exec(op) + } +} + +@(private="package") +_destroy :: proc(l: ^Event_Loop) { + linux.close(l.wake.read.handle) + free(l.wake, l.allocator) + + queue.destroy(&l.unqueued) + queue.destroy(&l.completed) + uring.destroy(&l.ring) +} + +@(private="package") +__tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> General_Error { + debug("tick") + + // Execute completed operations, mostly next tick ops, also some other ops that may error before + // adding it to the Uring. + n := queue.len(l.completed) + if n > 0 { + l.now = time.now() + for _ in 0 ..< n { + completed := queue.pop_front(&l.completed) + if completed._impl.removal == nil { + completed.cb(completed) + } else if completed._impl.removal != (^Operation)(REMOVED) { + completed._impl.removal._remove.target = nil + } + if !completed.detached { + pool.put(&l.operation_pool, completed) + } + } + } + + err := _flush_submissions(l, timeout) + if err != nil { return General_Error(err) } + + l.now = time.now() + + err = _flush_completions(l, false) + if err != nil { return General_Error(err) } + + return nil + + _flush_completions :: proc(l: ^Event_Loop, wait: bool) -> linux.Errno { + wait := wait + cqes: [128]linux.IO_Uring_CQE = --- + for { + completed, err := uring.copy_cqes(&l.ring, cqes[:], 1 if wait else 0) + if err == .EINTR { + continue + } else if err != nil { + return err + } + + _flush_unqueued(l) + + if completed > 0 { + debug(int(completed), "operations returned from uring") + } + + for cqe in cqes[:completed] { + assert(cqe.user_data != 0) + op, is_timeout := unpack_operation(cqe.user_data) + if is_timeout { + link_timeout_callback(op, cqe.res) + } else { + handle_completed(op, cqe.res) + } + } + + if completed < len(cqes) { break } + + debug("more events ready than our results buffer handles, getting more") + wait = false + } + + return nil + } + + _flush_submissions :: proc(l: ^Event_Loop, timeout: time.Duration) -> linux.Errno { + for { + ts: linux.Time_Spec + ts.time_nsec = uint(timeout) + _, err := uring.submit(&l.ring, 0 if timeout == 0 else 1, nil if timeout < 0 else &ts) + #partial switch err { + case .NONE, .ETIME: + case .EINTR: + warn("uring interrupted") + continue + case .ENOMEM: + // It's full, wait for at least one operation to complete and try again. + warn("could not flush submissions, ENOMEM, waiting for operations to complete before continuing") + ferr := _flush_completions(l, true) + if ferr != nil { return ferr } + continue + case: + return err + } + + break + } + + return nil + } + + _flush_unqueued :: proc(l: ^Event_Loop) { + n := queue.len(l.unqueued) + for _ in 0..<n { + unqueued := queue.pop_front(&l.unqueued) + + if unqueued._impl.removal != nil { + debug(unqueued.type, "was removed and has not been on the ring yet") + if unqueued._impl.removal != (^Operation)(REMOVED) { + // Set the removal target to nil to indicate we've already done it. + unqueued._impl.removal._remove.target = nil + } + if !unqueued.detached { + pool.put(&l.operation_pool, unqueued) + } + continue + } else if unqueued.type == ._Remove { + if unqueued._remove.target == nil { + // If the removal was set to nil by the branch above, we don't need to do anything. + debug("removal target was nil, skipping remove") + if !unqueued.detached { + pool.put(&l.operation_pool, unqueued) + } + continue + } else { + debug("removal was added to ring later") + enqueue(unqueued, uring.async_cancel( + &unqueued.l.ring, + u64(uintptr(unqueued._remove.target)), + u64(uintptr(unqueued)), + )) + continue + } + } + _exec(unqueued) + } + } +} + +@(private="package") +_exec :: proc(op: ^Operation) { + assert(op.l == &_tls_event_loop) + switch op.type { + case .Accept: accept_exec(op) + case .Dial: dial_exec(op) + case .Read: read_exec(op) + case .Write: write_exec(op) + case .Recv: recv_exec(op) + case .Send: send_exec(op) + case .Poll: poll_exec(op) + case .Close: close_exec(op) + case .Timeout: timeout_exec(op) + case .Send_File: sendfile_exec(op) + case .Open: open_exec(op) + case .Stat: stat_exec(op) + case ._Splice: + // This is only reachable when the queue was full the last tick. + // And if that's the case, it would still be full for the real sendfile (splice B) and will be enqueued there. + // So it is safe to do nothing here. + case ._Remove: unreachable() + case ._Link_Timeout: unreachable() + case .None: unreachable() + } +} + +@(private="package") +_open_sync :: proc(l: ^Event_Loop, path: string, dir: Handle, mode: File_Flags, perm: Permissions) -> (handle: Handle, err: FS_Error) { + if path == "" { + err = .Invalid_Argument + return + } + + cpath, cerr := strings.clone_to_cstring(path, l.allocator) + if cerr != nil { + err = .Allocation_Failed + return + } + defer delete(cpath, l.allocator) + + sys_flags := linux.Open_Flags{.NOCTTY, .CLOEXEC, .NONBLOCK} + + if .Write in mode { + if .Read in mode { + sys_flags += {.RDWR} + } else { + sys_flags += {.WRONLY} + } + } + + if .Append in mode { sys_flags += {.APPEND} } + if .Create in mode { sys_flags += {.CREAT} } + if .Excl in mode { sys_flags += {.EXCL} } + if .Sync in mode { sys_flags += {.DSYNC} } + if .Trunc in mode { sys_flags += {.TRUNC} } + // if .Inheritable in mode { sys_flags -= {.CLOEXEC} } + + errno: linux.Errno + handle, errno = linux.openat(dir, cpath, sys_flags, transmute(linux.Mode)perm) + if errno != nil { + err = FS_Error(errno) + } + + return +} + +@(private="package") +_create_socket :: proc( + _: ^Event_Loop, + family: Address_Family, + protocol: Socket_Protocol, +) -> ( + socket: Any_Socket, + err: Create_Socket_Error, +) { + socket = net.create_socket(family, protocol) or_return + // NOTE: this doesn't seem needed with io uring. + // defer if err != nil { net.close(socket) } + // net.set_blocking(socket, false) or_return + return +} + +@(private="package") +_listen :: proc(socket: TCP_Socket, backlog := 1000) -> Listen_Error { + err := linux.listen(linux.Fd(socket), i32(backlog)) + if err != nil { + return net._listen_error(err) + } + return nil +} + +@(private="package") +_remove :: proc(target: ^Operation) { + target := target + assert(target != nil) + + if target._impl.removal != nil { + return + } + + op := _prep(target.l, proc(_: ^Operation) {}, ._Remove) + op._remove.target = target + + target._impl.removal = op + + enqueue(op, uring.async_cancel( + &op.l.ring, + u64(uintptr(target)), + u64(uintptr(op)), + )) +} + +@(private="package") +_associate_handle :: proc(handle: uintptr, l: ^Event_Loop) -> (Handle, Association_Error) { + // Works by default. + return Handle(handle), nil +} + +@(private="package") +_associate_socket :: proc(socket: Any_Socket, l: ^Event_Loop) -> Association_Error { + // Works by default. + return nil +} + +@(private="package") +_wake_up :: proc(l: ^Event_Loop) { + assert(l != &_tls_event_loop) + one: u64 = 1 + // Called from another thread, in which we can't use the uring. + n, err := linux.write(l.wake.read.handle, ([^]byte)(&one)[:size_of(one)]) + // Shouldn't fail. + assert(err == nil) + assert(n == 8) +} + +// Start file private. + +// The size of the IO Uring queues. +QUEUE_SIZE :: #config(ODIN_NBIO_QUEUE_SIZE, 2048) +#assert(QUEUE_SIZE <= uring.MAX_ENTRIES) + +#assert(size_of(Operation) <= 384) // Just so we see when we make it bigger. +#assert(size_of(Specifics) <= 288) // Just so we see when we make it bigger. + +REMOVED :: rawptr(max(uintptr)-1) + +handle_completed :: proc(op: ^Operation, res: i32) { + debug("handling", op.type, "result", int(res)) + + switch op.type { + case .Accept: + accept_callback(op, res) + case .Dial: + dial_callback(op, res) + case .Timeout: + timeout_callback(op, res) + case .Write: + if !write_callback(op, res) { return } + case .Read: + if !read_callback(op, res) { return } + case .Close: + close_callback(op, res) + case .Poll: + poll_callback(op, res) + case .Send: + if !send_callback(op, res) { return } + maybe_callback(op) + if len(op.send.bufs) > 1 { delete(op.send.bufs, op.l.allocator) } + cleanup(op) + return + case .Recv: + if !recv_callback(op, res) { return } + maybe_callback(op) + if len(op.recv.bufs) > 1 { delete(op.recv.bufs, op.l.allocator) } + cleanup(op) + return + case .Open: + open_callback(op, res) + case .Stat: + stat_callback(op, res) + case .Send_File: + if !sendfile_callback(op, res) { return } + case ._Splice: + if !splice_callback(op, res) { return } + case ._Remove: + if !remove_callback(op, res) { return } + case ._Link_Timeout: + unreachable() + case .None: + fallthrough + case: + panic("corrupted operation") + } + + maybe_callback(op) + cleanup(op) + + maybe_callback :: proc(op: ^Operation) { + if op._impl.removal == nil { + debug("done, calling back", op.type) + op.cb(op) + } else if op._impl.removal == (^Operation)(REMOVED) { + debug("done but was cancelled by remove", op.type) + } else { + debug("done but has removal pending", op.type) + // If the remove callback sees their target is nil, they know it is done already. + op._impl.removal._remove.target = nil + } + } + + cleanup :: proc(op: ^Operation) { + if !op.detached { + pool.put(&op.l.operation_pool, op) + } + } +} + +enqueue :: proc(op: ^Operation, sqe: ^linux.IO_Uring_SQE, ok: bool) { + assert(uintptr(op) & LINK_TIMEOUT_MASK == 0) + debug("enqueue", op.type) + if !ok { + warn("queueing for next tick because the ring is full, queue size may need increasing") + pok, _ := queue.push_back(&op.l.unqueued, op) + ensure(pok, "unqueued queue allocation failure") + return + } + + op._impl.sqe = sqe +} + +LINK_TIMEOUT_MASK :: 1 + +link_timeout :: proc(target: ^Operation, expires: time.Time) { + if expires == {} { + return + } + + // If the last op was queued because kernel is full, return. + if target._impl.sqe == nil { + assert(queue.len(target.l.unqueued) > 0 && queue.back_ptr(&target.l.unqueued)^ == target) + return + } + + target._impl.sqe.flags += {.IO_LINK} + target._impl.expires = ns_to_time_spec(expires._nsec) + + // Tag the pointer as a timeout. + p := uintptr(target) + assert(p & LINK_TIMEOUT_MASK == 0) + p |= LINK_TIMEOUT_MASK + + _, ok := uring.link_timeout( + &target.l.ring, + u64(p), + &target._impl.expires, + {.ABS, .REALTIME}, + ) + // If the target wasn't queued, the link timeout should not need to be queued, because uring + // leaves one spot specifically for a link. + assert(ok) +} + +link_timeout_callback :: proc(op: ^Operation, res: i32) { + err := linux.Errno(-res) + if err != nil && err != .ETIME && err != .ECANCELED { + panic("unexpected nbio.link_timeout() error") + } +} + +unpack_operation :: #force_inline proc(user_data: u64) -> (op: ^Operation, timed_out: bool) { + p := uintptr(user_data) + return (^Operation)(p &~ LINK_TIMEOUT_MASK), bool(p & LINK_TIMEOUT_MASK) +} + +@(require_results) +remove_callback :: proc(op: ^Operation, res: i32) -> bool { + assert(op.type == ._Remove) + err := linux.Errno(-res) + + target := op._remove.target + if target == nil { + debug("remove target nil, already handled") + return true + } + + assert(target.type != .None) + assert(target._impl.removal == op) + + if err == .ENOENT { + debug("remove ENOENT, trying again") + + enqueue(op, uring.async_cancel( + &op.l.ring, + u64(uintptr(target)), + u64(uintptr(op)), + )) + + return false + } else if err == .EALREADY { + debug("remove is accepted and will be tried") + } else if err != nil { + assert(false, "unexpected nbio.remove() error") + } + + // Set to sentinel so nothing references the operation that will be reused. + target._impl.removal = (^Operation)(REMOVED) + return true +} + +accept_exec :: proc(op: ^Operation) { + assert(op.type == .Accept) + op.accept._impl.sockaddr_len = size_of(op.accept._impl.sockaddr) + enqueue(op, uring.accept( + &op.l.ring, + u64(uintptr(op)), + linux.Fd(op.accept.socket), + &op.accept._impl.sockaddr, + &op.accept._impl.sockaddr_len, + {}, + )) + link_timeout(op, op.accept.expires) +} + +accept_callback :: proc(op: ^Operation, res: i32) { + assert(op.type == .Accept) + if res < 0 { + errno := linux.Errno(-res) + #partial switch errno { + case .ECANCELED: + op.accept.err = .Timeout + case: + op.accept.err = net._accept_error(errno) + } + + return + } + + op.accept.client = TCP_Socket(res) + // net.set_blocking(net.TCP_Socket(op.accept.client), false) + op.accept.client_endpoint = sockaddr_storage_to_endpoint(&op.accept._impl.sockaddr) +} + +dial_exec :: proc(op: ^Operation) { + assert(op.type == .Dial) + if op.dial.socket == {} { + if op.dial.endpoint.port == 0 { + op.dial.err = .Port_Required + queue.push_back(&op.l.completed, op) + return + } + + sock, err := create_socket(net.family_from_endpoint(op.dial.endpoint), .TCP) + if err != nil { + op.dial.err = err + queue.push_back(&op.l.completed, op) + return + } + + op.dial.socket = sock.(TCP_Socket) + op.dial._impl.sockaddr = endpoint_to_sockaddr(op.dial.endpoint) + } + + enqueue(op, uring.connect( + &op.l.ring, + u64(uintptr(op)), + linux.Fd(op.dial.socket), + &op.dial._impl.sockaddr, + )) + link_timeout(op, op.dial.expires) +} + +dial_callback :: proc(op: ^Operation, res: i32) { + assert(op.type == .Dial) + errno := linux.Errno(-res) + if errno != nil { + #partial switch errno { + case .ECANCELED: + op.dial.err = Dial_Error.Timeout + case: + op.dial.err = net._dial_error(errno) + } + close(op.dial.socket) + } +} + +timeout_exec :: proc(op: ^Operation) { + assert(op.type == .Timeout) + if op.timeout.duration <= 0 { + queue.push_back(&op.l.completed, op) + return + } + + expires := time.time_add(op.l.now, op.timeout.duration) + op.timeout._impl.expires = ns_to_time_spec(expires._nsec) + + enqueue(op, uring.timeout( + &op.l.ring, + u64(uintptr(op)), + &op.timeout._impl.expires, + 0, + {.ABS, .REALTIME}, + )) +} + +timeout_callback :: proc(op: ^Operation, res: i32) { + if res < 0 { + errno := linux.Errno(-res) + #partial switch errno { + case .ETIME, .ECANCELED: // OK. + case: + debug("unexpected timeout error:", int(errno)) + panic("unexpected timeout error") + } + } +} + +close_exec :: proc(op: ^Operation) { + assert(op.type == .Close) + + fd: linux.Fd + switch closable in op.close.subject { + case Handle: fd = linux.Fd(closable) + case TCP_Socket: fd = linux.Fd(closable) + case UDP_Socket: fd = linux.Fd(closable) + case: op.close.err = .Invalid_Argument; return + } + + enqueue(op, uring.close( + &op.l.ring, + u64(uintptr(op)), + fd, + )) +} + +close_callback :: proc(op: ^Operation, res: i32) { + assert(op.type == .Close) + op.close.err = FS_Error(linux.Errno(-res)) +} + +recv_exec :: proc(op: ^Operation) { + assert(op.type == .Recv) + + if op.recv.err != nil { + queue.push_back(&op.l.completed, op) + return + } + + bufs := slice.advance_slices(op.recv.bufs, op.recv.received) + bufs, _ = constraint_bufs_to_max_rw(bufs) + op.recv._impl.msghdr.iov = transmute([]linux.IO_Vec)bufs + + sock: linux.Fd + switch socket in op.recv.socket { + case TCP_Socket: + sock = linux.Fd(socket) + case UDP_Socket: + sock = linux.Fd(socket) + op.recv._impl.msghdr.name = &op.recv._impl.addr_out + op.recv._impl.msghdr.namelen = size_of(op.recv._impl.addr_out) + } + + enqueue(op, uring.recvmsg( + &op.l.ring, + u64(uintptr(op)), + linux.Fd(sock), + &op.recv._impl.msghdr, + {.NOSIGNAL}, + )) + link_timeout(op, op.recv.expires) +} + +@(require_results) +recv_callback :: proc(op: ^Operation, res: i32) -> bool { + assert(op.type == .Recv) + + if res < 0 { + errno := linux.Errno(-res) + switch sock in op.recv.socket { + case TCP_Socket: + #partial switch errno { + case .ECANCELED: + op.recv.err = TCP_Recv_Error.Timeout + case: + op.recv.err = net._tcp_recv_error(errno) + } + case UDP_Socket: + #partial switch errno { + case .ECANCELED: + op.recv.err = UDP_Recv_Error.Timeout + case: + op.recv.err = net._udp_recv_error(errno) + } + } + + return true + } + + op.recv.received += int(res) + + switch sock in op.recv.socket { + case TCP_Socket: + if res == 0 { + // Connection closed. + return true + } + + if op.recv.all { + total: int + for buf in op.recv.bufs { + total += len(buf) + } + + if op.recv.received < total { + recv_exec(op) + return false + } + } + + case UDP_Socket: + op.recv.source = sockaddr_storage_to_endpoint(&op.recv._impl.addr_out) + } + + return true +} + +send_exec :: proc(op: ^Operation) { + assert(op.type == .Send) + + if op.send.err != nil { + queue.push_back(&op.l.completed, op) + return + } + + bufs := slice.advance_slices(op.send.bufs, op.send.sent) + bufs, _ = constraint_bufs_to_max_rw(bufs) + op.send._impl.msghdr.iov = transmute([]linux.IO_Vec)bufs + + sock: linux.Fd + switch socket in op.send.socket { + case TCP_Socket: + sock = linux.Fd(socket) + case UDP_Socket: + sock = linux.Fd(socket) + op.send._impl.endpoint = endpoint_to_sockaddr(op.send.endpoint) + op.send._impl.msghdr.name = &op.send._impl.endpoint + op.send._impl.msghdr.namelen = size_of(op.send._impl.endpoint) + } + + enqueue(op, uring.sendmsg( + &op.l.ring, + u64(uintptr(op)), + sock, + &op.send._impl.msghdr, + {.NOSIGNAL}, + )) + link_timeout(op, op.send.expires) +} + +@(require_results) +send_callback :: proc(op: ^Operation, res: i32) -> bool { + assert(op.type == .Send) + if res < 0 { + errno := linux.Errno(-res) + switch sock in op.send.socket { + case TCP_Socket: + #partial switch errno { + case .ECANCELED: + op.send.err = TCP_Send_Error.Timeout + case: + op.send.err = net._tcp_send_error(errno) + } + case UDP_Socket: + #partial switch errno { + case .ECANCELED: + op.send.err = UDP_Send_Error.Timeout + case: + op.send.err = net._udp_send_error(errno) + } + case: panic("corrupted socket") + } + + return true + } + + op.send.sent += int(res) + + if op.send.all { + total: int + for buf in op.send.bufs { + total += len(buf) + } + + if op.send.sent < total { + assert(res > 0) + send_exec(op) + return false + } + } + + return true +} + +write_exec :: proc(op: ^Operation) { + assert(op.type == .Write) + + buf := op.write.buf[op.write.written:] + buf = buf[:min(MAX_RW, len(buf))] + + enqueue(op, uring.write( + &op.l.ring, + u64(uintptr(op)), + op.write.handle, + buf, + u64(op.write.offset) + u64(op.write.written), + )) + link_timeout(op, op.write.expires) +} + +@(require_results) +write_callback :: proc(op: ^Operation, res: i32) -> bool { + if res < 0 { + errno := linux.Errno(-res) + op.write.err = FS_Error(errno) + return true + } + + op.write.written += int(res) + + if op.write.all && op.write.written < len(op.write.buf) { + write_exec(op) + return false + } + + return true +} + +read_exec :: proc(op: ^Operation) { + assert(op.type == .Read) + + buf := op.read.buf[op.read.read:] + buf = buf[:min(MAX_RW, len(buf))] + + enqueue(op, uring.read( + &op.l.ring, + u64(uintptr(op)), + op.read.handle, + buf, + u64(op.read.offset) + u64(op.read.read), + )) + link_timeout(op, op.read.expires) +} + +@(require_results) +read_callback :: proc(op: ^Operation, res: i32) -> bool { + if res < 0 { + errno := linux.Errno(-res) + op.read.err = FS_Error(errno) + return true + } else if res == 0 { + if op.read.read == 0 { + op.read.err = .EOF + } + return true + } + + op.read.read += int(res) + + if op.read.all && op.read.read < len(op.read.buf) { + read_exec(op) + return false + } + + return true +} + +poll_exec :: proc(op: ^Operation) { + assert(op.type == .Poll) + + events: linux.Fd_Poll_Events + switch op.poll.event { + case .Receive: events = { .IN } + case .Send: events = { .OUT } + } + + fd: linux.Fd + switch sock in op.poll.socket { + case TCP_Socket: fd = linux.Fd(sock) + case UDP_Socket: fd = linux.Fd(sock) + } + + enqueue(op, uring.poll_add( + &op.l.ring, + u64(uintptr(op)), + fd, + events, + {}, + )) + link_timeout(op, op.poll.expires) +} + +poll_callback :: proc(op: ^Operation, res: i32) { + if res < 0 { + errno := linux.Errno(-res) + #partial switch errno { + case .NONE: // no-op + case .ECANCELED: + op.poll.result = .Timeout + case .EINVAL, .EFAULT, .EBADF: + op.poll.result = .Invalid_Argument + case: + op.poll.result = .Error + } + + return + } + + op.poll.result = .Ready +} + +/* +`sendfile` is implemented with 2 splices over a pipe. + +Splice A: from file to pipe +Splice B: from pipe to socket (optionally linked to a timeout) + +The splices are hard-linked which means A completes before B. +B could get an `EWOULDBLOCK`, which is when the remote end has not read enough of the socket data yet. +In that case we enqueue a poll on the socket and continue when that completes. +A shouldn't get `EWOULDBLOCK`, but as a cautionary measure we handle it. + +The timeout is either linked to the splice B op, or the poll op, either of these is also always in progress in the kernel. +*/ +sendfile_exec :: proc(op: ^Operation, splice := true) { + assert(op.type == .Send_File) + + splice_done := !splice + if splice_op := op.sendfile._impl.splice; splice && splice_op != nil { + splice_done = splice_op._splice.written == splice_op._splice.len + } + + debug("sendfile_exec") + + if op.sendfile._impl.splice == nil { + // First stat for the file size. + if op.sendfile.nbytes == SEND_ENTIRE_FILE { + debug("sendfile SEND_ENTIRE_FILE, doing stat") + + stat_poly(op.sendfile.file, op, proc(stat_op: ^Operation, sendfile_op: ^Operation) { + if stat_op.stat.err != nil { + sendfile_op.sendfile.err = stat_op.stat.err + } else if stat_op.stat.type != .Regular { + sendfile_op.sendfile.err = FS_Error.Invalid_Argument + } else { + sendfile_op.sendfile.nbytes = int(i64(stat_op.stat.size) - i64(sendfile_op.sendfile.offset)) + if sendfile_op.sendfile.nbytes <= 0 { + sendfile_op.sendfile.err = FS_Error.Invalid_Argument + } + } + + if sendfile_op.sendfile.err != nil { + handle_completed(sendfile_op, 0) + return + } + + sendfile_exec(sendfile_op) + }) + return + } + + debug("sendfile setting up") + + rw: [2]linux.Fd + err := linux.pipe2(&rw, {.NONBLOCK, .CLOEXEC}) + if err != nil { + op.sendfile.err = FS_Error(err) + queue.push_back(&op.l.completed, op) + return + } + + splice_op := _prep(op.l, proc(_: ^Operation) { debug("sendfile splice helper callback") }, ._Splice) + splice_op._splice.sendfile = op + splice_op._splice.file = op.sendfile.file + splice_op._splice.pipe = rw[1] + splice_op._splice.off = op.sendfile.offset + splice_op._splice.len = op.sendfile.nbytes + + op.sendfile._impl.splice = splice_op + op.sendfile._impl.pipe = rw[0] + op.sendfile._impl.len = op.sendfile.nbytes + } + + splice_op: ^Operation + if !splice_done { + splice_op = op.sendfile._impl.splice + enqueue(splice_op, uring.splice( + &splice_op.l.ring, + u64(uintptr(splice_op)), + splice_op._splice.file, + i64(splice_op._splice.off) + i64(splice_op._splice.written), + splice_op._splice.pipe, + -1, + u32(min(splice_op._splice.len - splice_op._splice.written, MAX_RW)), + {.NONBLOCK}, + )) + } + + b, b_added := uring.splice( + &op.l.ring, + u64(uintptr(op)), + op.sendfile._impl.pipe, + -1, + linux.Fd(op.sendfile.socket), + -1, + u32(min(op.sendfile._impl.len - op.sendfile.sent, MAX_RW)), + {.NONBLOCK}, + ) + if !splice_done && b_added { + assert(splice_op._impl.sqe != nil) // if b was added successfully, a should've been too. + // Makes sure splice A (file to pipe) completes before splice B (pipe to socket). + splice_op._impl.sqe.flags += {.IO_HARDLINK} + } + enqueue(op, b, b_added) + + link_timeout(op, op.sendfile.expires) +} + +@(require_results) +splice_callback :: proc(op: ^Operation, res: i32) -> bool { + assert(op.type == ._Splice) + + if res < 0 { + errno := linux.Errno(-res) + #partial switch errno { + case .EAGAIN: + // Splice A (from file to pipe) would block, this means the buffer is full and it first needs + // to be sent over the socket by splice B (from pipe to socket). + // So we don't do anything here, once a splice B completes a new splice A will be created. + + case: + // Splice A (from file to pipe) error, we need to close the pipes, cancel the pending splice B, + // and call the callback with the error. + + debug("sendfile helper splice error, closing pipe") + + close(op._splice.pipe) + + // This is nil if this is a cancel originating from the sendfile. + // This is not nil if it is an actual error that happened on this splice. + sendfile_op := op._splice.sendfile + if sendfile_op != nil { + debug("sendfile helper splice error, cancelling main sendfile") + assert(sendfile_op.type == .Send_File) + + sendfile_op.sendfile._impl.splice = nil + sendfile_op.sendfile.err = FS_Error(errno) + } + } + + return true + } + + op._splice.written += int(res) + + sendfile_op := op._splice.sendfile + if sendfile_op != nil { + if op._splice.written < sendfile_op.sendfile.nbytes { + return false + } + + sendfile_op.sendfile._impl.splice = nil + } + + assert(op._splice.pipe > 0) + close(op._splice.pipe) + + debug("sendfile helper splice completely done") + return true +} + +@(require_results) +sendfile_callback :: proc(op: ^Operation, res: i32) -> bool { + assert(op.type == .Send_File) + + if op.sendfile.err == nil && res < 0 { + errno := linux.Errno(-res) + #partial switch errno { + case .EAGAIN: + // Splice B (from pipe to socket) would block. We are waiting on the remote to read more + // of our buffer before we can send more to it. + // We use a poll to find out when this is. + + debug("sendfile needs to poll") + + poll_op := poll_poly(op.sendfile.socket, .Send, op, proc(poll_op: ^Operation, sendfile_op: ^Operation) { + #partial switch poll_op.poll.result { + case .Ready: + // Do not enqueue a splice right away, we know there is at least one splice call worth of data in the kernel buffer. + sendfile_exec(sendfile_op, splice=false) + return + + case .Timeout: + sendfile_op.sendfile.err = TCP_Send_Error.Timeout + case: + sendfile_op.sendfile.err = TCP_Send_Error.Unknown + } + + debug("sendfile poll error") + handle_completed(sendfile_op, 0) + }) + + link_timeout(poll_op, op.sendfile.expires) + return false + + case .ECANCELED: + op.sendfile.err = TCP_Send_Error.Timeout + case: + op.sendfile.err = net._tcp_send_error(errno) + } + } + + if op.sendfile.err != nil { + debug("sendfile error") + + if op.sendfile._impl.pipe > 0 { + close(op.sendfile._impl.pipe) + } + + splice_op := op.sendfile._impl.splice + if splice_op != nil { + assert(splice_op.type == ._Splice) + splice_op._splice.sendfile = nil + _remove(splice_op) + } + + return true + } + + op.sendfile.sent += int(res) + if op.sendfile.sent < op.sendfile._impl.len { + debug("sendfile not completely done yet") + sendfile_exec(op) + if op.sendfile.progress_updates { op.cb(op) } + return false + } + + debug("sendfile completely done") + return true +} + +open_exec :: proc(op: ^Operation) { + assert(op.type == .Open) + + sys_flags := linux.Open_Flags{.NOCTTY, .CLOEXEC, .NONBLOCK} + + if .Write in op.open.mode { + if .Read in op.open.mode { + sys_flags += {.RDWR} + } else { + sys_flags += {.WRONLY} + } + } + + if .Append in op.open.mode { sys_flags += {.APPEND} } + if .Create in op.open.mode { sys_flags += {.CREAT} } + if .Excl in op.open.mode { sys_flags += {.EXCL} } + if .Sync in op.open.mode { sys_flags += {.DSYNC} } + if .Trunc in op.open.mode { sys_flags += {.TRUNC} } + // if .Inheritable in op.open.mode { sys_flags -= {.CLOEXEC} } + + cpath, err := strings.clone_to_cstring(op.open.path, op.l.allocator) + if err != nil { + op.open.err = .Allocation_Failed + queue.push_back(&op.l.completed, op) + return + } + op.open._impl.cpath = cpath + + enqueue(op, uring.openat( + &op.l.ring, + u64(uintptr(op)), + linux.Fd(op.open.dir), + op.open._impl.cpath, + transmute(linux.Mode)op.open.perm, + sys_flags, + )) +} + +open_callback :: proc(op: ^Operation, res: i32) { + assert(op.type == .Open) + + delete(op.open._impl.cpath, op.l.allocator) + + if res < 0 { + errno := linux.Errno(-res) + op.open.err = FS_Error(errno) + return + } + + op.open.handle = Handle(res) +} + +stat_exec :: proc(op: ^Operation) { + assert(op.type == .Stat) + + enqueue(op, uring.statx( + &op.l.ring, + u64(uintptr(op)), + op.stat.handle, + "", + {.EMPTY_PATH}, + {.TYPE, .SIZE}, + &op.stat._impl.buf, + )) +} + +stat_callback :: proc(op: ^Operation, res: i32) { + assert(op.type == .Stat) + + if res < 0 { + errno := linux.Errno(-res) + op.stat.err = FS_Error(errno) + return + } + + type := File_Type.Regular + switch op.stat._impl.buf.mode & linux.S_IFMT { + case linux.S_IFBLK, linux.S_IFCHR: type = .Device + case linux.S_IFDIR: type = .Directory + case linux.S_IFIFO: type = .Pipe_Or_Socket + case linux.S_IFLNK: type = .Symlink + case linux.S_IFREG: type = .Regular + case linux.S_IFSOCK: type = .Pipe_Or_Socket + } + + op.stat.type = type + op.stat.size = i64(op.stat._impl.buf.size) +} + +@(require_results) +sockaddr_storage_to_endpoint :: proc(addr: ^linux.Sock_Addr_Any) -> (ep: Endpoint) { + #partial switch addr.family { + case .INET: + return Endpoint { + address = IP4_Address(addr.sin_addr), + port = int(addr.sin_port), + } + case .INET6: + return Endpoint { + address = IP6_Address(transmute([8]u16be)addr.sin6_addr), + port = int(addr.sin6_port), + } + case: + return {} + } +} + +@(require_results) +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: linux.Sock_Addr_Any) { + switch a in ep.address { + case IP4_Address: + sockaddr.sin_family = .INET + sockaddr.sin_port = u16be(ep.port) + sockaddr.sin_addr = cast([4]u8)a + return + case IP6_Address: + sockaddr.sin6_family = .INET6 + sockaddr.sin6_port = u16be(ep.port) + sockaddr.sin6_addr = transmute([16]u8)a + return + } + + unreachable() +} + +@(require_results) +ns_to_time_spec :: proc(nsec: i64) -> linux.Time_Spec { + NANOSECONDS_PER_SECOND :: 1e9 + return { + time_sec = uint(nsec / NANOSECONDS_PER_SECOND), + time_nsec = uint(nsec % NANOSECONDS_PER_SECOND), + } +} diff --git a/core/nbio/impl_others.odin b/core/nbio/impl_others.odin new file mode 100644 index 000000000..cac1f0c63 --- /dev/null +++ b/core/nbio/impl_others.odin @@ -0,0 +1,217 @@ +#+build !darwin +#+build !freebsd +#+build !openbsd +#+build !netbsd +#+build !linux +#+build !windows +#+private +package nbio + +import "core:container/avl" +import "core:container/pool" +import "core:container/queue" +import "core:mem" +import "core:slice" +import "core:time" + +_FULLY_SUPPORTED :: false + +_Event_Loop :: struct { + completed: queue.Queue(^Operation), + timeouts: avl.Tree(^Operation), + allocator: mem.Allocator, +} + +_Handle :: uintptr + +_CWD :: Handle(-100) + +MAX_RW :: mem.Gigabyte + +_Operation :: struct { + removed: bool, +} + +_Accept :: struct {} + +_Close :: struct {} + +_Dial :: struct {} + +_Recv :: struct { + small_bufs: [1][]byte, +} + +_Send :: struct { + small_bufs: [1][]byte, +} + +_Read :: struct {} + +_Write :: struct {} + +_Timeout :: struct { + expires: time.Time, +} + +_Poll :: struct {} + +_Send_File :: struct {} + +_Open :: struct {} + +_Stat :: struct {} + +_Splice :: struct {} + +_Remove :: struct {} + +_Link_Timeout :: struct {} + +_init :: proc(l: ^Event_Loop, allocator: mem.Allocator) -> (rerr: General_Error) { + l.allocator = allocator + l.completed.data.allocator = allocator + + avl.init_cmp(&l.timeouts, timeouts_cmp, allocator) + + return nil + + timeouts_cmp :: #force_inline proc(a, b: ^Operation) -> slice.Ordering { + switch { + case a.timeout._impl.expires._nsec < b.timeout._impl.expires._nsec: + return .Less + case a.timeout._impl.expires._nsec > b.timeout._impl.expires._nsec: + return .Greater + case uintptr(a) < uintptr(b): + return .Less + case uintptr(a) > uintptr(b): + return .Greater + case: + assert(a == b) + return .Equal + } + } +} + +_destroy :: proc(l: ^Event_Loop) { + queue.destroy(&l.completed) + avl.destroy(&l.timeouts, false) +} + +__tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> General_Error { + l.now = time.now() + + for op in queue.pop_front_safe(&l.completed) { + if !op._impl.removed { + op.cb(op) + } + if !op.detached { + pool.put(&l.operation_pool, op) + } + } + + iter := avl.iterator(&l.timeouts, .Forward) + for node in avl.iterator_next(&iter) { + op := node.value + cexpires := time.diff(l.now, op.timeout._impl.expires) + + done := cexpires <= 0 + if done { + op.cb(op) + avl.remove_node(&l.timeouts, node) + if !op.detached { + pool.put(&l.operation_pool, op) + } + continue + } + + break + } + + return nil +} + +_create_socket :: proc(l: ^Event_Loop, family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { + return nil, .Network_Unreachable +} + +_listen :: proc(socket: TCP_Socket, backlog := 1000) -> Listen_Error { + return .Network_Unreachable +} + +_exec :: proc(op: ^Operation) { + switch op.type { + case .Timeout: + _, _, err := avl.find_or_insert(&op.l.timeouts, op) + if err != nil { + panic("nbio: allocation failure") + } + return + case .Accept: + op.accept.err = .Network_Unreachable + case .Close: + op.close.err = .Unsupported + case .Dial: + op.dial.err = Dial_Error.Network_Unreachable + case .Recv: + switch _ in op.recv.socket { + case TCP_Socket: op.recv.err = TCP_Recv_Error.Network_Unreachable + case UDP_Socket: op.recv.err = UDP_Recv_Error.Network_Unreachable + case: op.recv.err = TCP_Recv_Error.Network_Unreachable + } + case .Send: + switch _ in op.send.socket { + case TCP_Socket: op.send.err = TCP_Send_Error.Network_Unreachable + case UDP_Socket: op.send.err = UDP_Send_Error.Network_Unreachable + case: op.send.err = TCP_Send_Error.Network_Unreachable + } + case .Send_File: + op.sendfile.err = .Network_Unreachable + case .Read: + op.read.err = .Unsupported + case .Write: + op.write.err = .Unsupported + case .Poll: + op.poll.result = .Error + case .Open: + op.open.err = .Unsupported + case .Stat: + op.stat.err = .Unsupported + case .None, ._Link_Timeout, ._Remove, ._Splice: + fallthrough + case: + unreachable() + } + + _, err := queue.push_back(&op.l.completed, op) + if err != nil { + panic("nbio: allocation failure") + } +} + +_remove :: proc(target: ^Operation) { + #partial switch target.type { + case .Timeout: + avl.remove_value(&target.l.timeouts, target) + if !target.detached { + pool.put(&target.l.operation_pool, target) + } + case: + target._impl.removed = true + } +} + +_open_sync :: proc(l: ^Event_Loop, path: string, dir: Handle, mode: File_Flags, perm: Permissions) -> (handle: Handle, err: FS_Error) { + return 0, FS_Error.Unsupported +} + +_associate_handle :: proc(handle: uintptr, l: ^Event_Loop) -> (Handle, Association_Error) { + return Handle(handle), nil +} + +_associate_socket :: proc(socket: Any_Socket, l: ^Event_Loop) -> Association_Error { + return nil +} + +_wake_up :: proc(l: ^Event_Loop) { +} diff --git a/core/nbio/impl_posix.odin b/core/nbio/impl_posix.odin new file mode 100644 index 000000000..e003f6ea3 --- /dev/null +++ b/core/nbio/impl_posix.odin @@ -0,0 +1,1403 @@ +#+build darwin, freebsd, openbsd, netbsd +#+private file +package nbio + +import "core:c" +import "core:container/pool" +import "core:container/queue" +import "core:mem" +import "core:net" +import "core:slice" +import "core:strings" +import "core:sys/posix" +import "core:time" +import kq "core:sys/kqueue" +import sa "core:container/small_array" + +@(private="package") +_FULLY_SUPPORTED :: true + +@(private="package") +_Event_Loop :: struct { + // kqueue does not permit multiple {ident, filter} pairs in the kqueue. + // We have to keep record of what we currently have in the kqueue, and if we get an operation + // that would be the same (ident, filter) pair we need to bundle the operations under one kevent. + submitted: map[Queue_Identifier]^Operation, + allocator: mem.Allocator, + // Holds all events we want to flush. Flushing is done each tick at which point this is emptied. + pending: sa.Small_Array(QUEUE_SIZE, kq.KEvent), + // Holds what should be in `pending` but didn't fit. + // When `pending`is flushed these are moved to `pending`. + overflow: queue.Queue(kq.KEvent), + // Contains all operations that were immediately completed in `exec`. + // These ops did not block so can call back next tick. + completed: queue.Queue(^Operation), + kqueue: kq.KQ, +} + +@(private="package") +_Handle :: posix.FD + +@(private="package") +_CWD :: posix.AT_FDCWD + +@(private="package") +MAX_RW :: mem.Gigabyte + +@(private="package") +_Operation :: struct { + // Linked list of operations that are bundled (same {ident, filter} pair) with this one. + next: ^Operation, + prev: ^Operation, + + flags: Operation_Flags, + result: i64, +} + +@(private="package") +_Accept :: struct {} + +@(private="package") +_Close :: struct {} + +@(private="package") +_Dial :: struct {} + +@(private="package") +_Recv :: struct { + small_bufs: [1][]byte, +} + +@(private="package") +_Send :: struct { + small_bufs: [1][]byte, +} + +@(private="package") +_Read :: struct {} + +@(private="package") +_Write :: struct {} + +@(private="package") +_Timeout :: struct {} + +@(private="package") +_Poll :: struct {} + +@(private="package") +_Send_File :: struct { + mapping: []byte, // `mmap`'d buffer (if native `sendfile` is not supported). +} + +@(private="package") +_Open :: struct {} + +@(private="package") +_Stat :: struct {} + +@(private="package") +_Splice :: struct {} + +@(private="package") +_Remove :: struct {} + +@(private="package") +_Link_Timeout :: struct {} + +@(private="package") +_init :: proc(l: ^Event_Loop, allocator: mem.Allocator) -> (rerr: General_Error) { + l.allocator = allocator + l.submitted.allocator = allocator + l.overflow.data.allocator = allocator + l.completed.data.allocator = allocator + + kqueue, err := kq.kqueue() + if err != nil { + return General_Error(posix.errno()) + } + + l.kqueue = kqueue + + sa.append(&l.pending, kq.KEvent{ + ident = IDENT_WAKE_UP, + filter = .User, + flags = {.Add, .Enable, .Clear}, + }) + + return nil +} + +@(private="package") +_destroy :: proc(l: ^Event_Loop) { + delete(l.submitted) + queue.destroy(&l.overflow) + queue.destroy(&l.completed) + posix.close(l.kqueue) +} + +@(private="package") +__tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> General_Error { + debug("tick") + + if n := queue.len(l.completed); n > 0 { + l.now = time.now() + debug("processing", n, "already completed") + + for _ in 0 ..< n { + op := queue.pop_front(&l.completed) + handle_completed(op) + } + + if pool.num_outstanding(&l.operation_pool) == 0 { return nil } + } + + if NBIO_DEBUG { + npending := sa.len(l.pending) + if npending > 0 { + debug("queueing", npending, "new events, there are", int(len(l.submitted)), "events pending") + } else { + debug("there are", int(len(l.submitted)), "events pending") + } + } + + ts_backing: posix.timespec + ts_pointer: ^posix.timespec // nil means forever. + if queue.len(l.completed) == 0 && len(l.submitted) > 0 { + if timeout >= 0 { + debug("timeout", timeout) + ts_backing = {tv_sec=posix.time_t(timeout/time.Second), tv_nsec=c.long(timeout%time.Second)} + ts_pointer = &ts_backing + } else { + debug("timeout forever") + } + } else { + debug("timeout 0, there is completed work pending") + ts_pointer = &ts_backing + } + + for { + results_buf: [128]kq.KEvent + results := kevent(l, results_buf[:], ts_pointer) or_return + + sa.clear(&l.pending) + for overflow in queue.pop_front_safe(&l.overflow) { + sa.append(&l.pending, overflow) or_break + } + + l.now = time.now() + + handle_results(l, results) + + if len(results) < len(results_buf) { + break + } + + debug("more events ready than our results buffer handles, getting more") + + // No timeout for the next call. + ts_backing = {} + ts_pointer = &ts_backing + } + + + return nil + + kevent :: proc(l: ^Event_Loop, buf: []kq.KEvent, ts: ^posix.timespec) -> ([]kq.KEvent, General_Error) { + for { + new_events, err := kq.kevent(l.kqueue, sa.slice(&l.pending), buf, ts) + #partial switch err { + case nil: + assert(new_events >= 0) + return buf[:new_events], nil + case .EINTR: + warn("kevent interrupted") + case: + warn("kevent error") + warn(string(posix.strerror(err))) + return nil, General_Error(err) + } + } + } + + is_internal_timeout :: proc(filter: kq.Filter, op: ^Operation) -> bool { + // A `.Timeout` that `.Has_Timeout` is a `remove()`'d timeout. + return filter == .Timer && (op.type != .Timeout || .Has_Timeout in op._impl.flags) + } + + handle_results :: proc(l: ^Event_Loop, results: []kq.KEvent) { + if len(results) > 0 { + debug(len(results), "events completed") + } + + // Mark all operations that have an event returned as not `.For_Kernel`. + // We have to do this right away, or we may process an operation as if we think the kernel is responsible. + for &event in results { + if ODIN_OS != .Darwin { + // On the BSDs, a `.Delete` that results in an `.Error` does not keep the `.Delete` flag in the result. + // We only have `udata == nil` when we do a delete, so we can add it back here to keep consistent. + if .Error in event.flags && event.udata == nil { + event.flags += {.Delete} + } + } + + if .Delete in event.flags { + continue + } + + if event.filter == .User && event.ident == IDENT_WAKE_UP { + continue + } + + op := cast(^Operation)event.udata + assert(op != nil) + assert(op.type != .None) + + if is_internal_timeout(event.filter, op) { + continue + } + + _, del := delete_key(&l.submitted, Queue_Identifier{ ident = event.ident, filter = event.filter }) + assert(del != nil) + + for next := op; next != nil; next = next._impl.next { + assert(.For_Kernel in next._impl.flags) + next._impl.flags -= {.For_Kernel} + } + } + + // If we get a timeout and an actual result, ignore the timeout. + // We have to do this after the previous loop so we know if the target op of a timeout was also completed. + // We have to do this before the next loop so we handle timeouts before their target ops. Otherwise the target could already be done. + for &event in results { + if .Delete in event.flags { + continue + } + + if event.filter == .User && event.ident == IDENT_WAKE_UP { + continue + } + + op := cast(^Operation)event.udata + if is_internal_timeout(event.filter, op) { + // If the actual event has also been returned this tick, we need to ignore the timeout to not get a uaf. + if .For_Kernel not_in op._impl.flags { + assert(.Has_Timeout in op._impl.flags) + op._impl.flags -= {.Has_Timeout} + + event.filter = kq.Filter(FILTER_IGNORE) + debug(op.type, "timed out but was also completed this tick, ignoring timeout") + } + + } + } + + for event in results { + if event.filter == kq.Filter(FILTER_IGNORE) { + // Previous loop told us to ignore. + continue + } + + if event.filter == .User && event.ident == IDENT_WAKE_UP { + debug("woken up") + continue + } + + if .Delete in event.flags { + assert(.Error in event.flags) + // Seems to happen when you delete at the same time or just after a close. + debug("delete error", int(event.data)) + if err := posix.Errno(event.data); err != .ENOENT && err != .EBADF { + warn("unexpected delete error") + warn(string(posix.strerror(err))) + } + continue + } + + op := cast(^Operation)event.udata + assert(op != nil) + assert(op.type != .None) + + // Timeout result that is a non-timeout op, meaning the operation timed out. + // Because of the previous loop we are sure that the target op is not also in this tick's results. + if is_internal_timeout(event.filter, op) { + debug("got timeout for", op.type) + + assert(.Error not_in event.flags) + + assert(.Has_Timeout in op._impl.flags) + op._impl.flags -= {.Has_Timeout} + + // Remove the actual operation. + timeout_and_delete(op) + handle_completed(op) + continue + } + + // Weird loop, but we need to get the next ptr before handle_completed(curr), curr is freed in handle_completed. + for curr, next := op, op._impl.next; curr != nil; curr, next = next, next == nil ? nil : next._impl.next { + if .Error in event.flags { curr._impl.flags += {.Error} } + if .EOF in event.flags { curr._impl.flags += {.EOF} } + curr._impl.result = event.data + handle_completed(curr) + } + } + } +} + +@(private="package") +_create_socket :: proc(l: ^Event_Loop, family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { + socket = net.create_socket(family, protocol) or_return + + berr := net.set_blocking(socket, false) + // This shouldn't be able to fail. + assert(berr == nil) + + return +} + +@(private="package") +_listen :: proc(socket: TCP_Socket, backlog := 1000) -> Listen_Error { + if res := posix.listen(posix.FD(socket), i32(backlog)); res != .OK { + return posix_listen_error() + } + return nil +} + +@(private="package") +_exec :: proc(op: ^Operation) { + assert(op.l == &_tls_event_loop) + + debug("exec", op.type) + + result: Op_Result + switch op.type { + case .Accept: + result = accept_exec(op) + case .Close: + // no-op + case .Timeout: + result = timeout_exec(op) + case .Dial: + result = dial_exec(op) + case .Recv: + result = recv_exec(op) + case .Send: + result = send_exec(op) + case .Send_File: + result = sendfile_exec(op) + case .Read: + result = read_exec(op) + case .Write: + result = write_exec(op) + case .Poll: + result = poll_exec(op) + assert(result == .Pending) + case .Open: + open_exec(op) + case .Stat: + stat_exec(op) + case .None, ._Link_Timeout, ._Remove, ._Splice: + fallthrough + case: + unreachable() + } + + switch result { + case .Pending: + // no-op, in kernel. + debug(op.type, "pending") + case .Done: + debug(op.type, "done immediately") + op._impl.flags += {.Done} + _, err := queue.push_back(&op.l.completed, op) // Got result, handle it next tick. + ensure(err == nil, "allocation failure") + } +} + +@(private="package") +_remove :: proc(target: ^Operation) { + assert(target != nil) + + debug("remove", target.type) + + if .Removed in target._impl.flags { + debug("already removed") + return + } + + target._impl.flags += {.Removed, .Has_Timeout} + link_timeout(target, target.l.now) +} + +@(private="package") +_open_sync :: proc(l: ^Event_Loop, path: string, dir: Handle, mode: File_Flags, perm: Permissions) -> (handle: Handle, err: FS_Error) { + if path == "" { + err = .Invalid_Argument + return + } + + cpath, cerr := strings.clone_to_cstring(path, l.allocator) + if cerr != nil { + err = .Allocation_Failed + return + } + defer delete(cpath, l.allocator) + + sys_flags := posix.O_Flags{.NOCTTY, .CLOEXEC, .NONBLOCK} + + if .Write in mode { + if .Read in mode { + sys_flags += {.RDWR} + } else { + sys_flags += {.WRONLY} + } + } + + if .Append in mode { sys_flags += {.APPEND} } + if .Create in mode { sys_flags += {.CREAT} } + if .Excl in mode { sys_flags += {.EXCL} } + if .Sync in mode { sys_flags += {.DSYNC} } + if .Trunc in mode { sys_flags += {.TRUNC} } + + handle = posix.openat(dir, cpath, sys_flags, transmute(posix.mode_t)posix._mode_t(transmute(u32)perm)) + if handle < 0 { + err = FS_Error(posix.errno()) + } + + return +} + +@(private="package") +_associate_handle :: proc(handle: uintptr, l: ^Event_Loop) -> (Handle, Association_Error) { + flags_ := posix.fcntl(posix.FD(handle), .GETFL) + if flags_ < 0 { + #partial switch errno := posix.errno(); errno { + case .EBADF: return -1, .Invalid_Handle + case: return -1, Association_Error(errno) + } + } + flags := transmute(posix.O_Flags)(flags_) + + if .NONBLOCK in flags { + return Handle(handle), nil + } + + if posix.fcntl(posix.FD(handle), .SETFL, flags) < 0 { + #partial switch errno := posix.errno(); errno { + case .EBADF: return -1, .Invalid_Handle + case: return -1, Association_Error(errno) + } + } + + return Handle(handle), nil +} + +@(private="package") +_associate_socket :: proc(socket: Any_Socket, l: ^Event_Loop) -> Association_Error { + if err := net.set_blocking(socket, false); err != nil { + switch err { + case .None: unreachable() + case .Network_Unreachable: return .Network_Unreachable + case .Invalid_Argument: return .Invalid_Handle + case .Unknown: fallthrough + case: return Association_Error(net.last_platform_error()) + } + } + + return nil +} + +@(private="package") +_wake_up :: proc(l: ^Event_Loop) { + ev := [1]kq.KEvent{ + { + ident = IDENT_WAKE_UP, + filter = .User, + flags = {}, + fflags = { + user = {.Trigger}, + }, + }, + } + t: posix.timespec + n, err := kq.kevent(l.kqueue, ev[:], nil, &t) + assert(err == nil) + assert(n == 0) +} + +// Start file private. + +// Max operations that can be enqueued per tick. +QUEUE_SIZE :: #config(ODIN_NBIO_QUEUE_SIZE, 256) + +FILTER_IGNORE :: kq.Filter(max(kq._Filter_Backing)) + +IDENT_WAKE_UP :: 69 + +Op_Result :: enum { + Done, + Pending, +} + +Operation_Flag :: enum { + Done, + Removed, + Has_Timeout, + For_Kernel, + EOF, + Error, +} +Operation_Flags :: bit_set[Operation_Flag] + +// Operations in the kqueue are uniquely identified using these 2 fields. You may not have more +// than one operation with the same identity submitted. +// So we need to keep track of the operations we have submitted, and if we add another, link it to a previously +// added operation. +Queue_Identifier :: struct { + filter: kq.Filter, + ident: uintptr, +} + +handle_completed :: proc(op: ^Operation) { + debug("handling", op.type) + + result: Op_Result + #partial switch op.type { + case .Accept: + result = accept_exec(op) + case .Dial: + result = dial_exec(op) + case .Send: + if send_exec(op) == .Done { + maybe_callback(op) + bufs_destroy(op.send.bufs, op.l.allocator) + cleanup(op) + } + return + case .Recv: + if recv_exec(op) == .Done { + maybe_callback(op) + bufs_destroy(op.recv.bufs, op.l.allocator) + cleanup(op) + } + return + case .Send_File: + result = sendfile_exec(op) + case .Read: + result = read_exec(op) + case .Write: + result = write_exec(op) + case .Poll: + result = poll_exec(op) + case .Open: + open_exec(op) + case .Close: + close_exec(op) + case .Timeout, .Stat: + // no-op + case: + unimplemented() + } + + if result == .Done { + maybe_callback(op) + cleanup(op) + } + + maybe_callback :: proc(op: ^Operation) { + if .Removed not_in op._impl.flags { + debug("done", op.type, "calling back") + op.cb(op) + } else { + debug("done but removed", op.type) + } + } + + bufs_destroy :: proc(bufs: [][]byte, allocator: mem.Allocator) { + if len(bufs) > 1 { + delete(bufs, allocator) + } + } + + cleanup :: proc(op: ^Operation) { + if .Has_Timeout in op._impl.flags { + remove_link_timeout(op) + } + if !op.detached { + pool.put(&op.l.operation_pool, op) + } + } +} + +@(require_results) +accept_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Accept) + + defer if op.accept.err != nil && op.accept.client > 0 { + posix.close(posix.FD(op.accept.client)) + } + + if op.accept.err != nil || .Done in op._impl.flags { + return .Done + } + + op.accept.client, op.accept.client_endpoint, op.accept.err = net.accept_tcp(op.accept.socket) + if op.accept.err != nil { + if op.accept.err == .Would_Block { + op.accept.err = nil + add_pending(op, .Read, uintptr(op.accept.socket)) + link_timeout(op, op.accept.expires) + return .Pending + } + + return .Done + } + + if err := net.set_blocking(op.accept.client, false); err != nil { + op.accept.err = posix_accept_error() + } + + return .Done +} + +@(require_results) +dial_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Dial) + + defer if op.dial.err != nil && op.dial.socket > 0 { + posix.close(posix.FD(op.dial.socket)) + } + + if op.dial.err != nil || .Done in op._impl.flags { + return .Done + } + + if op.dial.socket > 0 { + // We have already called connect, retrieve potential error number only. + err: posix.Errno + size := posix.socklen_t(size_of(err)) + posix.getsockopt(posix.FD(op.dial.socket), posix.SOL_SOCKET, .ERROR, &err, &size) + if err != nil { + posix.errno(err) + op.dial.err = posix_dial_error() + } + return .Done + } + + if op.dial.endpoint.port == 0 { + op.dial.err = .Port_Required + return .Done + } + + family := family_from_endpoint(op.dial.endpoint) + osocket, socket_err := _create_socket(op.l, family, .TCP) + if socket_err != nil { + op.dial.err = socket_err + return .Done + } + + op.dial.socket = osocket.(TCP_Socket) + + sockaddr := endpoint_to_sockaddr(op.dial.endpoint) + if posix.connect(posix.FD(op.dial.socket), (^posix.sockaddr)(&sockaddr), posix.socklen_t(sockaddr.ss_len)) != .OK { + if posix.errno() == .EINPROGRESS { + add_pending(op, .Write, uintptr(op.dial.socket)) + link_timeout(op, op.dial.expires) + return .Pending + } + + op.dial.err = posix_dial_error() + return .Done + } + + return .Done +} + +@(require_results) +poll_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Poll) + + if .Error in op._impl.flags { + #partial switch posix.Errno(op._impl.result) { + case .EBADF: op.poll.result = .Invalid_Argument + case: op.poll.result = .Error + } + return .Done + } + + if op._impl.result != 0 { + op.poll.result = .Ready + return .Done + } + + if op.poll.result != .Ready { + return .Done + } + + filter: kq.Filter + switch op.poll.event { + case .Receive: filter = .Read + case .Send: filter = .Write + } + + add_pending(op, filter, uintptr(net.any_socket_to_socket(op.poll.socket))) + link_timeout(op, op.poll.expires) + return .Pending +} + +close_exec :: proc(op: ^Operation) { + assert(op.type == .Close) + + if op.close.err != nil || op.close.subject == nil { + return + } + + fd: posix.FD + switch subject in op.close.subject { + case TCP_Socket: fd = posix.FD(subject) + case UDP_Socket: fd = posix.FD(subject) + case Handle: fd = posix.FD(subject) + case: op.close.err = .Invalid_Argument; return + } + + if posix.close(fd) != .OK { + op.close.err = FS_Error(posix.errno()) + } +} + +@(require_results) +send_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Send) + + if op.send.err != nil || .Done in op._impl.flags { + return .Done + } + + total: int + bufs := slice.advance_slices(op.send.bufs, op.send.sent) + bufs, total = constraint_bufs_to_max_rw(op.send.bufs) + + sock, n := sendv(op.send.socket, bufs, op.send.endpoint) + if n < 0 { + if posix.errno() == .EWOULDBLOCK { + if !op.send.all && op.send.sent > 0 { + return .Done + } + + add_pending(op, .Write, uintptr(sock)) + link_timeout(op, op.send.expires) + return .Pending + } + + switch _ in op.send.socket { + case TCP_Socket: op.send.err = posix_tcp_send_error() + case UDP_Socket: op.send.err = posix_udp_send_error() + } + return .Done + } + + op.send.sent += n + + if op.send.sent < total { + return send_exec(op) + } + + return .Done + + sendv :: proc(socket: Any_Socket, bufs: [][]byte, to: net.Endpoint) -> (posix.FD, int) { + assert(len(bufs) < int(max(i32))) + + msg: posix.msghdr + msg.msg_iov = cast([^]posix.iovec)raw_data(bufs) + msg.msg_iovlen = i32(len(bufs)) + + toaddr: posix.sockaddr_storage + fd: posix.FD + switch sock in socket { + case TCP_Socket: + fd = posix.FD(sock) + case UDP_Socket: + fd = posix.FD(sock) + toaddr = endpoint_to_sockaddr(to) + msg.msg_name = &toaddr + msg.msg_namelen = posix.socklen_t(toaddr.ss_len) + } + + return fd, posix.sendmsg(fd, &msg, {.NOSIGNAL}) + } +} + +@(require_results) +recv_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Recv) + + if op.recv.err != nil || .Done in op._impl.flags { + return .Done + } + + total: int + bufs := slice.advance_slices(op.recv.bufs, op.recv.received) + bufs, total = constraint_bufs_to_max_rw(op.recv.bufs) + + _, is_tcp := op.recv.socket.(net.TCP_Socket) + + sock, n := recvv(op.recv.socket, bufs, &op.recv.source) + if n < 0 { + if posix.errno() == .EWOULDBLOCK { + if is_tcp && !op.recv.all && op.recv.received > 0 { + return .Done + } + + add_pending(op, .Read, uintptr(sock)) + link_timeout(op, op.recv.expires) + return .Pending + } + + if is_tcp { + op.recv.err = posix_tcp_recv_error() + } else { + op.recv.err = posix_udp_recv_error() + } + + return .Done + } + + assert(is_tcp || op.recv.received == 0) + op.recv.received += n + + if is_tcp && n != 0 && op.recv.received < total { + return recv_exec(op) + } + + return .Done + + recvv :: proc(socket: Any_Socket, bufs: [][]byte, from: ^Endpoint) -> (fd: posix.FD, n: int) { + assert(len(bufs) < int(max(i32))) + + msg: posix.msghdr + msg.msg_iov = cast([^]posix.iovec)raw_data(bufs) + msg.msg_iovlen = i32(len(bufs)) + + udp: bool + fromaddr: posix.sockaddr_storage + switch sock in socket { + case TCP_Socket: + fd = posix.FD(sock) + case UDP_Socket: + fd = posix.FD(sock) + udp = true + msg.msg_name = &fromaddr + msg.msg_namelen = posix.socklen_t(size_of(fromaddr)) + } + + n = posix.recvmsg(fd, &msg, {.NOSIGNAL}) + if n >= 0 && udp { + from^ = sockaddr_to_endpoint(&fromaddr) + } + + return + } +} + +@(require_results) +sendfile_exec :: proc(op: ^Operation) -> (result: Op_Result) { + assert(op.type == .Send_File) + + defer if result == .Done && op.sendfile._impl.mapping != nil { + posix.munmap(raw_data(op.sendfile._impl.mapping), len(op.sendfile._impl.mapping)) + } + + if op.sendfile.err != nil || .Done in op._impl.flags { + return .Done + } + + when ODIN_OS == .NetBSD || ODIN_OS == .OpenBSD { + // Doesn't have `sendfile`, emulate it with `mmap` + normal `send`. + return sendfile_exec_emulated(op) + } else { + return sendfile_exec_native(op) + + @(require_results) + sendfile_exec_native :: proc(op: ^Operation) -> Op_Result { + nbytes := op.sendfile.nbytes + assert(nbytes != 0) + if nbytes == SEND_ENTIRE_FILE { + nbytes = 0 // special value for entire file. + + // If we want progress updates we need nbytes to be the actual size, or the user + // won't be able to check `sent < nbytes` to know if it's the final callback. + if op.sendfile.progress_updates { + stat: posix.stat_t + if posix.fstat(op.sendfile.file, &stat) != .OK { + op.sendfile.err = FS_Error(posix.errno()) + return .Done + } + op.sendfile.nbytes = int(stat.st_size - posix.off_t(op.sendfile.offset)) + } + } else { + nbytes -= op.sendfile.sent + } + + n, ok := posix_sendfile(op.sendfile.file, op.sendfile.socket, op.sendfile.offset + op.sendfile.sent, nbytes) + + assert(n >= 0) + op.sendfile.sent += n + + if !ok { + op.sendfile.err = posix_tcp_send_error() + if op.sendfile.err == .Would_Block { + op.sendfile.err = nil + if op.sendfile.progress_updates { op.cb(op) } + add_pending(op, .Write, uintptr(op.sendfile.socket)) + link_timeout(op, op.sendfile.expires) + return .Pending + } + + return .Done + } + + assert(op.sendfile.nbytes == SEND_ENTIRE_FILE || op.sendfile.sent == op.sendfile.nbytes) + return .Done + } + } + + @(require_results) + sendfile_exec_emulated :: proc(op: ^Operation) -> Op_Result { + if op.sendfile.nbytes == SEND_ENTIRE_FILE { + stat: posix.stat_t + if posix.fstat(op.sendfile.file, &stat) != .OK { + op.sendfile.err = FS_Error(posix.errno()) + return .Done + } + op.sendfile.nbytes = int(stat.st_size - posix.off_t(op.sendfile.offset)) + } + + if op.sendfile._impl.mapping == nil { + addr := posix.mmap(nil, uint(op.sendfile.nbytes), {.READ}, {}, op.sendfile.file, posix.off_t(op.sendfile.offset)) + if addr == posix.MAP_FAILED { + op.sendfile.err = FS_Error(posix.errno()) + return .Done + } + op.sendfile._impl.mapping = ([^]byte)(addr)[:op.sendfile.nbytes] + } + + n := posix.send( + posix.FD(op.sendfile.socket), + raw_data(op.sendfile._impl.mapping)[op.sendfile.sent:], + uint(min(MAX_RW, op.sendfile.nbytes - op.sendfile.sent)), + {.NOSIGNAL}, + ) + if n < 0 { + op.sendfile.err = posix_tcp_send_error() + if op.sendfile.err == .Would_Block { + op.sendfile.err = nil + add_pending(op, .Write, uintptr(op.sendfile.socket)) + link_timeout(op, op.sendfile.expires) + return .Pending + } + + return .Done + } + + op.sendfile.sent += n + + if op.sendfile.sent < op.sendfile.nbytes { + if op.sendfile.progress_updates { op.cb(op) } + return sendfile_exec_emulated(op) + } + + return .Done + } +} + +@(require_results) +read_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Read) + + if op.read.err != nil || .Done in op._impl.flags { + return .Done + } + + to_read := op.read.buf[op.read.read:] + to_read = to_read[:min(MAX_RW, len(to_read))] + + res := posix.pread(op.read.handle, raw_data(to_read), len(to_read), posix.off_t(op.read.offset) + posix.off_t(op.read.read)) + if res < 0 { + errno := posix.errno() + if errno == .EWOULDBLOCK { + if !op.read.all && op.read.read > 0 { + return .Done + } + + add_pending(op, .Read, uintptr(op.read.handle)) + link_timeout(op, op.read.expires) + return .Pending + } + + op.read.err = FS_Error(errno) + return .Done + } else if res == 0 { + if op.read.read == 0 { + op.read.err = .EOF + } + return .Done + } + + op.read.read += res + + if op.read.read < len(op.read.buf) { + return read_exec(op) + } + + return .Done +} + +@(require_results) +write_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Write) + + if op.write.err != nil || .Done in op._impl.flags { + return .Done + } + + to_write := op.write.buf[op.write.written:] + to_write = to_write[:min(MAX_RW, len(to_write))] + + res := posix.pwrite(op.write.handle, raw_data(to_write), len(to_write), posix.off_t(op.write.offset) + posix.off_t(op.write.written)) + if res < 0 { + errno := posix.errno() + if errno == .EWOULDBLOCK { + if !op.write.all && op.write.written > 0 { + return .Done + } + + add_pending(op, .Write, uintptr(op.write.handle)) + link_timeout(op, op.write.expires) + return .Pending + } + + op.write.err = FS_Error(errno) + return .Done + } + + op.write.written += res + + if op.write.written < len(op.write.buf) { + return write_exec(op) + } + + return .Done +} + +timeout_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Timeout) + + if op.timeout.duration <= 0 { + return .Done + } + + op.l.submitted[Queue_Identifier{ ident = uintptr(op), filter = .Timer }] = op + + op._impl.flags += {.For_Kernel} + + append_pending(op.l, kq.KEvent { + ident = uintptr(op), + filter = .Timer, + flags = {.Add, .Enable, .One_Shot}, + fflags = { + timer = kq.TIMER_FLAGS_NSECONDS + {.Absolute}, + }, + data = op.l.now._nsec + i64(op.timeout.duration), + udata = op, + }) + return .Pending +} + +open_exec :: proc(op: ^Operation) { + assert(op.type == .Open) + + if op.open.err != nil && op.open.handle > 0 { + posix.close(op.open.handle) + return + } + + if .Done in op._impl.flags { + return + } + + op.open.handle, op.open.err = _open_sync(op.l, op.open.path, op.open.dir, op.open.mode, op.open.perm) +} + +stat_exec :: proc(op: ^Operation) { + assert(op.type == .Stat) + + stat: posix.stat_t + if posix.fstat(op.stat.handle, &stat) != .OK { + op.stat.err = FS_Error(posix.errno()) + return + } + + op.stat.type = .Undetermined + switch { + case posix.S_ISBLK(stat.st_mode) || posix.S_ISCHR(stat.st_mode): + op.stat.type = .Device + case posix.S_ISDIR(stat.st_mode): + op.stat.type = .Directory + case posix.S_ISFIFO(stat.st_mode) || posix.S_ISSOCK(stat.st_mode): + op.stat.type = .Pipe_Or_Socket + case posix.S_ISLNK(stat.st_mode): + op.stat.type = .Symlink + case posix.S_ISREG(stat.st_mode): + op.stat.type = .Regular + } + + op.stat.size = i64(stat.st_size) +} + +add_pending :: proc(op: ^Operation, filter: kq.Filter, ident: uintptr) { + debug("adding pending", op.type) + op._impl.flags += {.For_Kernel} + + _, val, just_inserted, err := map_entry(&op.l.submitted, Queue_Identifier{ ident = ident, filter = filter }) + ensure(err == nil, "allocation failure") + if just_inserted { + val^ = op + + append_pending(op.l, kq.KEvent { + filter = filter, + ident = ident, + flags = {.Add, .Enable, .One_Shot}, + udata = op, + }) + } else { + debug("already have this operation on the kqueue, bundling it") + + last := val^ + for last._impl.next != nil { + last = last._impl.next + } + last._impl.next = op + op._impl.prev = last + } +} + +append_pending :: #force_inline proc(l: ^Event_Loop, ev: kq.KEvent) { + if !sa.append(&l.pending, ev) { + warn("queue is full, adding to overflow, should QUEUE_SIZE be increased?") + _, err := queue.append(&l.overflow, ev) + ensure(err == nil, "allocation failure") + } +} + +link_timeout :: proc(op: ^Operation, expires: time.Time) { + if expires == {} { + return + } + + debug(op.type, "times out at", expires) + + op._impl.flags += {.Has_Timeout} + + append_pending(op.l, kq.KEvent { + ident = uintptr(op), + filter = .Timer, + flags = {.Add, .Enable, .One_Shot}, + fflags = { + timer = kq.TIMER_FLAGS_NSECONDS + {.Absolute}, + }, + data = expires._nsec, + udata = op, + }) +} + +remove_link_timeout :: proc(op: ^Operation) { + debug("removing timeout of", op.type) + assert(.Has_Timeout in op._impl.flags) + + append_pending(op.l, kq.KEvent { + ident = uintptr(op), + filter = .Timer, + flags = {.Delete, .Disable, .One_Shot}, + }) +} + +timeout_and_delete :: proc(target: ^Operation) { + filter: kq.Filter + ident: uintptr + switch target.type { + case .Accept: + target.accept.err = .Timeout + filter = .Read + ident = uintptr(target.accept.socket) + case .Dial: + target.dial.err = Dial_Error.Timeout + filter = .Write + ident = uintptr(target.dial.socket) + case .Read: + target.read.err = .Timeout + filter = .Read + ident = uintptr(target.read.handle) + case .Write: + target.write.err = .Timeout + filter = .Write + ident = uintptr(target.write.handle) + case .Recv: + switch sock in target.recv.socket { + case TCP_Socket: + target.recv.err = TCP_Recv_Error.Timeout + ident = uintptr(sock) + case UDP_Socket: + target.recv.err = UDP_Recv_Error.Timeout + ident = uintptr(sock) + } + filter = .Read + case .Send: + switch sock in target.send.socket { + case TCP_Socket: + target.send.err = TCP_Send_Error.Timeout + ident = uintptr(sock) + case UDP_Socket: + target.send.err = UDP_Send_Error.Timeout + ident = uintptr(sock) + } + filter = .Write + case .Send_File: + target.send.err = TCP_Send_Error.Timeout + filter = .Write + ident = uintptr(target.sendfile.socket) + case .Poll: + target.poll.result = .Timeout + ident = uintptr(net.any_socket_to_socket(target.poll.socket)) + + switch target.poll.event { + case .Receive: filter = .Read + case .Send: filter = .Write + } + + case .Timeout: + ident = uintptr(target) + filter = .Timer + + case .Close: + target.close.err = .Timeout + return + + case .Open: + target.open.err = .Timeout + return + + case .Stat: + target.stat.err = .Timeout + return + + case .None, ._Link_Timeout, ._Remove, ._Splice: + return + } + + // If there are other ops linked to this kevent, don't remove it. + if target._impl.next != nil || target._impl.prev != nil { + debug("removing target by pulling it out of the linked list, other ops depend on the kevent") + assert(filter != .Timer) + + if target._impl.next != nil { + target._impl.next._impl.prev = target._impl.prev + } + + if target._impl.prev != nil { + target._impl.prev._impl.next = target._impl.next + } else { + debug("target was the head of the list, updating map to point at new head") + + _, vp, _, err := map_entry(&target.l.submitted, Queue_Identifier{ ident = ident, filter = filter }) + ensure(err == nil, "allocation failure") + assert(vp^ == target) + vp^ = target._impl.next + + ev := kq.KEvent{ + filter = filter, + ident = ident, + flags = {.Add, .Enable, .One_Shot}, + udata = target._impl.next, + } + if !sa.append(&target.l.pending, ev) { + warn("just removed the head operation of a list of multiple, and the queue is full, have to force this update through inefficiently") + // This has to happen the next time we submit or we could have udata pointing wrong. + // Very inefficient but probably never hit. + + // Makes kevent return a receipt for our addition, so we don't take any new events from it. + // This forces .Error to be added and data being 0 means it's added. + ev.flags += {.Receipt} + + timeout: posix.timespec + n, err := kq.kevent(target.l.kqueue, ([^]kq.KEvent)(&ev)[:1], ([^]kq.KEvent)(&ev)[:1], &timeout) + assert(n == 1) + assert(err == nil) + + // The receipt flag makes this occur on the event. + assert(.Error in ev.flags) + assert(ev.data == 0) + } + } + + } else if .For_Kernel in target._impl.flags { + debug("adding delete event") + + _, dval := delete_key(&target.l.submitted, Queue_Identifier{ ident = ident, filter = filter }) + assert(dval != nil) + + append_pending(target.l, kq.KEvent{ + ident = ident, + filter = filter, + flags = {.Delete, .Disable, .One_Shot}, + }) + } else { + debug("remove without delete event, because target is not in kernel") + } +} + +@(require_results) +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: posix.sockaddr_storage) { + switch a in ep.address { + case IP4_Address: + (^posix.sockaddr_in)(&sockaddr)^ = posix.sockaddr_in { + sin_port = u16be(ep.port), + sin_addr = transmute(posix.in_addr)a, + sin_family = .INET, + sin_len = size_of(posix.sockaddr_in), + } + return + case IP6_Address: + (^posix.sockaddr_in6)(&sockaddr)^ = posix.sockaddr_in6 { + sin6_port = u16be(ep.port), + sin6_addr = transmute(posix.in6_addr)a, + sin6_family = .INET6, + sin6_len = size_of(posix.sockaddr_in6), + } + return + } + unreachable() +} + +@(require_results) +sockaddr_to_endpoint :: proc(native_addr: ^posix.sockaddr_storage) -> (ep: Endpoint) { + #partial switch native_addr.ss_family { + case .INET: + addr := cast(^posix.sockaddr_in)native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte)addr.sin_addr), + port = port, + } + case .INET6: + addr := cast(^posix.sockaddr_in6)native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be)addr.sin6_addr), + port = port, + } + case: + panic("native_addr is neither IP4 or IP6 address") + } + return +} diff --git a/core/nbio/impl_posix_darwin.odin b/core/nbio/impl_posix_darwin.odin new file mode 100644 index 000000000..7ec64c2df --- /dev/null +++ b/core/nbio/impl_posix_darwin.odin @@ -0,0 +1,29 @@ +#+private +package nbio + +import "core:net" +import "core:sys/posix" + +foreign import lib "system:System" + +posix_sendfile :: proc(fd: Handle, s: TCP_Socket, offset, nbytes: int) -> (sent: int, ok := true) { + foreign lib { + @(link_name="sendfile") + _posix_sendfile :: proc (fd, s: posix.FD, offset: posix.off_t, len: ^posix.off_t, hdtr: rawptr, flags: i32) -> posix.result --- + } + + len := posix.off_t(nbytes) + if _posix_sendfile(posix.FD(fd), posix.FD(s), posix.off_t(offset), &len, nil, 0) != .OK { + ok = false + } + sent = int(len) + return +} + +posix_listen_error :: net._listen_error +posix_accept_error :: net._accept_error +posix_dial_error :: net._dial_error +posix_tcp_send_error :: net._tcp_send_error +posix_udp_send_error :: net._udp_send_error +posix_tcp_recv_error :: net._tcp_recv_error +posix_udp_recv_error :: net._udp_recv_error diff --git a/core/nbio/impl_posix_freebsd.odin b/core/nbio/impl_posix_freebsd.odin new file mode 100644 index 000000000..50089510b --- /dev/null +++ b/core/nbio/impl_posix_freebsd.odin @@ -0,0 +1,52 @@ +#+private +package nbio + +import "core:net" +import "core:sys/posix" +import "core:sys/freebsd" + +foreign import lib "system:c" + +// TODO: rewrite freebsd implementation to use `sys/freebsd` instead of `sys/posix`. + +posix_sendfile :: proc(fd: Handle, s: TCP_Socket, offset, nbytes: int) -> (sent: int, ok := true) { + foreign lib { + @(link_name="sendfile") + _posix_sendfile :: proc (fd, s: posix.FD, offset: posix.off_t, nbytes: uint, hdtr: rawptr, sbytes: ^posix.off_t, flags: i32) -> posix.result --- + } + + len: posix.off_t + if _posix_sendfile(posix.FD(fd), posix.FD(s), posix.off_t(offset), uint(nbytes), nil, &len, 0) != .OK { + ok = false + } + sent = int(len) + return +} + +posix_listen_error :: proc() -> Listen_Error { + return net._listen_error(freebsd.Errno(posix.errno())) +} + +posix_accept_error :: proc() -> Accept_Error { + return net._accept_error(freebsd.Errno(posix.errno())) +} + +posix_dial_error :: proc() -> Dial_Error { + return net._dial_error(freebsd.Errno(posix.errno())) +} + +posix_tcp_send_error :: proc() -> TCP_Send_Error { + return net._tcp_send_error(freebsd.Errno(posix.errno())) +} + +posix_udp_send_error :: proc() -> UDP_Send_Error { + return net._udp_send_error(freebsd.Errno(posix.errno())) +} + +posix_tcp_recv_error :: proc() -> TCP_Recv_Error { + return net._tcp_recv_error(freebsd.Errno(posix.errno())) +} + +posix_udp_recv_error :: proc() -> UDP_Recv_Error { + return net._udp_recv_error(freebsd.Errno(posix.errno())) +} diff --git a/core/nbio/impl_posix_netbsd.odin b/core/nbio/impl_posix_netbsd.odin new file mode 100644 index 000000000..a0e2652f4 --- /dev/null +++ b/core/nbio/impl_posix_netbsd.odin @@ -0,0 +1,12 @@ +#+private +package nbio + +import "core:net" + +posix_listen_error :: net._listen_error +posix_accept_error :: net._accept_error +posix_dial_error :: net._dial_error +posix_tcp_send_error :: net._tcp_send_error +posix_udp_send_error :: net._udp_send_error +posix_tcp_recv_error :: net._tcp_recv_error +posix_udp_recv_error :: net._udp_recv_error diff --git a/core/nbio/impl_posix_openbsd.odin b/core/nbio/impl_posix_openbsd.odin new file mode 100644 index 000000000..a0e2652f4 --- /dev/null +++ b/core/nbio/impl_posix_openbsd.odin @@ -0,0 +1,12 @@ +#+private +package nbio + +import "core:net" + +posix_listen_error :: net._listen_error +posix_accept_error :: net._accept_error +posix_dial_error :: net._dial_error +posix_tcp_send_error :: net._tcp_send_error +posix_udp_send_error :: net._udp_send_error +posix_tcp_recv_error :: net._tcp_recv_error +posix_udp_recv_error :: net._udp_recv_error diff --git a/core/nbio/impl_windows.odin b/core/nbio/impl_windows.odin new file mode 100644 index 000000000..4610d4754 --- /dev/null +++ b/core/nbio/impl_windows.odin @@ -0,0 +1,1733 @@ +#+private file +package nbio + +import "base:intrinsics" + +import "core:container/avl" +import "core:container/pool" +import "core:container/queue" +import "core:mem" +import "core:net" +import "core:slice" +import "core:strings" +import "core:time" +import "core:path/filepath" + +import win "core:sys/windows" + +@(private="package") +_FULLY_SUPPORTED :: true + +@(private="package") +_Event_Loop :: struct { + iocp: win.HANDLE, + allocator: mem.Allocator, + timeouts: avl.Tree(^Operation), + completed: queue.Queue(^Operation), +} + +@(private="package") +_Handle :: distinct uintptr + +@(private="package") +_CWD :: ~_Handle(99) + +@(private="package") +MAX_RW :: mem.Gigabyte + +@(private="package") +_Operation :: struct { + over: win.OVERLAPPED, + timeout: ^Operation, +} + +@(private="package") +_Accept :: struct { + // Space that gets the local and remote address written into it. + addrs: [(size_of(win.sockaddr_in6)+16)*2]byte, +} + +@(private="package") +_Close :: struct {} + +@(private="package") +_Dial :: struct { + addr: win.SOCKADDR_STORAGE_LH, +} + +@(private="package") +_Read :: struct {} + +@(private="package") +_Write :: struct {} + +@(private="package") +_Send :: struct { + small_bufs: [1][]byte, +} + +@(private="package") +_Recv :: struct { + source: win.SOCKADDR_STORAGE_LH, + source_len: win.INT, + small_bufs: [1][]byte, + flags: win.DWORD, +} + +@(private="package") +_Timeout :: struct { + expires: time.Time, + target: ^Operation, +} + +@(private="package") +_Poll :: struct { + wait_handle: win.HANDLE, +} + +@(private="package") +_Send_File :: struct {} + +@(private="package") +_Remove :: struct {} + +@(private="package") +_Link_Timeout :: struct {} + +@(private="package") +_Splice :: struct {} + +@(private="package") +_Open :: struct {} + +@(private="package") +_Stat :: struct {} + +@(private="package") +_init :: proc(l: ^Event_Loop, alloc: mem.Allocator) -> (err: General_Error) { + l.allocator = alloc + + mem_err: mem.Allocator_Error + if mem_err = queue.init(&l.completed, allocator = alloc); mem_err != nil { + err = .Allocation_Failed + return + } + defer if err != nil { queue.destroy(&l.completed) } + + avl.init(&l.timeouts, timeouts_cmp, alloc) + + win.ensure_winsock_initialized() + + l.iocp = win.CreateIoCompletionPort(win.INVALID_HANDLE_VALUE, nil, 0, 1) + if l.iocp == nil { + err = General_Error(win.GetLastError()) + return + } + + return + + timeouts_cmp :: #force_inline proc(a, b: ^Operation) -> slice.Ordering { + switch { + case a.timeout._impl.expires._nsec < b.timeout._impl.expires._nsec: + return .Less + case a.timeout._impl.expires._nsec > b.timeout._impl.expires._nsec: + return .Greater + case uintptr(a) < uintptr(b): + return .Less + case uintptr(a) > uintptr(b): + return .Greater + case: + assert(a == b) + return .Equal + } + } +} + +@(private="package") +_destroy :: proc(l: ^Event_Loop) { + queue.destroy(&l.completed) + avl.destroy(&l.timeouts) + win.CloseHandle(l.iocp) +} + +@(private="package") +__tick :: proc(l: ^Event_Loop, timeout: time.Duration) -> (err: General_Error) { + debug("tick") + + l.now = time.now() + next_timeout := check_timeouts(l) + + // Prevent infinite loop when callback adds to completed by storing length. + n := queue.len(l.completed) + if n > 0 { + for _ in 0 ..< n { + op := queue.pop_front(&l.completed) + handle_completed(op) + } + } + + if pool.num_outstanding(&l.operation_pool) == 0 { return nil } + + actual_timeout := win.INFINITE + if queue.len(l.completed) > 0 { + actual_timeout = 0 + } else if timeout >= 0 { + actual_timeout = win.DWORD(timeout / time.Millisecond) + } + if nt, ok := next_timeout.?; ok { + actual_timeout = min(actual_timeout, win.DWORD(nt / time.Millisecond)) + } + + for { + QUEUE_SIZE :: 256 + events: [QUEUE_SIZE]win.OVERLAPPED_ENTRY + entries_removed: win.ULONG + if !win.GetQueuedCompletionStatusEx(l.iocp, &events[0], len(events), &entries_removed, actual_timeout, false) { + if terr := win.GetLastError(); terr != win.WAIT_TIMEOUT { + err = General_Error(terr) + return + } + } + + if actual_timeout > 0 { + // We may have just waited some time, lets update the current time. + l.now = time.now() + } + + if entries_removed > 0 { + debug(int(entries_removed), "operations were completed") + } + + for event in events[:entries_removed] { + if event.lpCompletionKey == COMPLETION_KEY_WAKE_UP { continue } + assert(event.lpOverlapped != nil) + op := container_of(container_of(event.lpOverlapped, _Operation, "over"), Operation, "_impl") + handle_completed(op) + } + + if entries_removed < QUEUE_SIZE { + break + } + + // `events` was filled up, get more. + debug("GetQueuedCompletionStatusEx filled entire events buffer, getting more") + actual_timeout = 0 + } + + return nil + + check_timeouts :: proc(l: ^Event_Loop) -> (expires: Maybe(time.Duration)) { + curr := l.now + + if avl.len(&l.timeouts) == 0 { + return + } + + debug(avl.len(&l.timeouts), "timeouts", "threshold", curr) + + iter := avl.iterator(&l.timeouts, .Forward) + for node in avl.iterator_next(&iter) { + op := node.value + cexpires := time.diff(curr, op.timeout._impl.expires) + + debug("expires after", cexpires) + + removed := op._impl.timeout == (^Operation)(REMOVED) + done := cexpires <= 0 + if removed { + debug("timeout removed!") + } else if done { + debug("timeout done!") + handle_completed(op) + } + if removed || done { + avl.remove_node(&l.timeouts, node) + continue + } + + expires = cexpires + debug("first timeout in the future is at", op.timeout._impl.expires, "after", cexpires) + return + } + + return + } + + handle_completed :: proc(op: ^Operation) { + debug("handling", op.type) + + if op._impl.timeout == (^Operation)(REMOVED) { + debug(op.type, "was removed") + + // Set an error, and call the internal callback. + // This way resources are cleaned up properly, for example the result socket for dial. + // If we just do nothing it will be leaked. + + if op._impl.over.Internal == nil { + // There is no error from the kernel, set one ourselves. + // This needs to be an NTSTATUS code, not a win32 error number. + STATUS_REQUEST_ABORTED :: 0xC023000C + op._impl.over.Internal = (^win.c_ulong)(uintptr(STATUS_REQUEST_ABORTED)) + } + } + + result := Op_Result.Done + switch op.type { + case .Read: + result = read_callback(op) + case .Recv: + result = recv_callback(op) + if result == .Done { + maybe_callback(op) + if len(op.recv.bufs) > 1 { + delete(op.recv.bufs, op.l.allocator) + } + cleanup(op) + return + } + case .Write: + result = write_callback(op) + case .Send: + result = send_callback(op) + if result == .Done { + maybe_callback(op) + if len(op.send.bufs) > 1 { + delete(op.send.bufs, op.l.allocator) + } + cleanup(op) + return + } + case .Send_File: + result = sendfile_callback(op) + case .Accept: + accept_callback(op) + case .Dial: + dial_callback(op) + case .Poll: + poll_callback(op) + case .Timeout, .Open, .Stat, .Close: + // no-op. + case .None, ._Link_Timeout, ._Remove, ._Splice: + fallthrough + case: + unreachable() + } + + if result == .Pending { + assert(op._impl.timeout != (^Operation)(REMOVED)) + debug(op.type, "pending") + return + } + + maybe_callback(op) + cleanup(op) + + maybe_callback :: proc(op: ^Operation) { + if op._impl.timeout == (^Operation)(REMOVED) { + debug(op.type, "done but removed") + } else { + debug(op.type, "done") + op.cb(op) + + if op._impl.timeout != nil { + debug("cancelling timeout of", op.type) + op._impl.timeout.timeout._impl.target = nil + _remove(op._impl.timeout) + } + } + } + + cleanup :: proc(op: ^Operation) { + if !op.detached { + pool.put(&op.l.operation_pool, op) + } + } + } + +} + +@(private="package") +_exec :: proc(op: ^Operation) { + assert(op.l == &_tls_event_loop) + + result: Op_Result + switch op.type { + case .Accept: result = accept_exec(op) + case .Close: close_exec(op); result = .Done + case .Dial: result = dial_exec(op) + case .Recv: result = recv_exec(op) + case .Send: result = send_exec(op) + case .Send_File: result = sendfile_exec(op) + case .Read: result = read_exec(op) + case .Write: result = write_exec(op) + case .Timeout: result = timeout_exec(op) + case .Poll: result = poll_exec(op) + case .Open: open_exec(op); result = .Done + case .Stat: stat_exec(op); result = .Done + case .None, ._Link_Timeout, ._Remove, ._Splice: + unreachable() + } + + switch result { + case .Pending: + // no-op, in kernel. + debug("exec", op.type, "pending") + case .Done: + debug("exec", op.type, "done immediately") + _, err := queue.append(&op.l.completed, op) // Got result, handle it next tick. + ensure(err == nil, "allocation failure") + } +} + +@(private="package") +_open_sync :: proc(l: ^Event_Loop, name: string, dir: Handle, mode: File_Flags, perm: Permissions) -> (handle: Handle, err: FS_Error) { + if len(name) == 0 { + err = .Invalid_Argument + return + } + + dir := dir + + is_abs := filepath.is_abs(name) + is_cwd: bool + cwd_path: win.wstring + if !is_abs && dir == CWD { + is_cwd = true + + cwd_len := win.GetCurrentDirectoryW(0, nil) + assert(cwd_len > 0) + cwd_buf, cwd_err := make([]u16, cwd_len, l.allocator) + if cwd_err != nil { return INVALID_HANDLE, .Allocation_Failed } + cwd_len = win.GetCurrentDirectoryW(cwd_len, raw_data(cwd_buf)) + assert(int(cwd_len) == len(cwd_buf)-1) + cwd_path = win.wstring(raw_data(cwd_buf)) + + dir = Handle(win.CreateFileW( + cwd_path, + win.GENERIC_READ, + win.FILE_SHARE_READ|win.FILE_SHARE_WRITE, + nil, + win.OPEN_EXISTING, + win.FILE_FLAG_BACKUP_SEMANTICS, + nil, + )) + if dir == INVALID_HANDLE { + err = FS_Error(win.GetLastError()) + return + } + } + defer if is_cwd { + delete(cwd_path, l.allocator) + win.CloseHandle(win.HANDLE(dir)) + } + + path, was_alloc := _normalize_path(name, l.allocator) + defer if was_alloc { delete(path, l.allocator) } + + wpath := win.utf8_to_utf16(path, l.allocator) + defer delete(wpath, l.allocator) + + if path == "" || wpath == nil { + return INVALID_HANDLE, .Allocation_Failed + } + + path_len := len(wpath) * 2 + if path_len > int(max(u16)) { + err = .Invalid_Argument + return + } + + access: u32 + switch mode & {.Read, .Write} { + case {.Read}: access = win.FILE_GENERIC_READ + case {.Write}: access = win.FILE_GENERIC_WRITE + case {.Read, .Write}: access = win.FILE_GENERIC_READ | win.FILE_GENERIC_WRITE + } + + if .Create in mode { + access |= win.FILE_GENERIC_WRITE + } + if .Append in mode { + access &~= win.FILE_GENERIC_WRITE + access |= win.FILE_APPEND_DATA + } + share_mode := u32(win.FILE_SHARE_READ | win.FILE_SHARE_WRITE) + + create_mode: u32 = win.FILE_OPEN + switch { + case mode & {.Create, .Excl} == {.Create, .Excl}: + create_mode = win.FILE_CREATE + case mode & {.Create, .Trunc} == {.Create, .Trunc}: + create_mode = win.FILE_OVERWRITE_IF + case mode & {.Create} == {.Create}: + create_mode = win.FILE_OPEN_IF + case mode & {.Trunc} == {.Trunc}: + create_mode = win.FILE_OVERWRITE + } + + attrs: u32 = win.FILE_ATTRIBUTE_NORMAL + + if .Write_User not_in perm { + attrs = win.FILE_ATTRIBUTE_READONLY + if create_mode == win.FILE_OVERWRITE_IF { + // NOTE(bill): Open has just asked to create a file in read-only mode. + // If the file already exists, to make it akin to a *nix open call, + // the call preserves the existing permissions. + + h: win.HANDLE + io_status: win.IO_STATUS_BLOCK + status := win.NtCreateFile( + &h, + access, + &{ + Length = size_of(win.OBJECT_ATTRIBUTES), + RootDirectory = is_abs ? nil : win.HANDLE(dir), + ObjectName = &{ + Length = u16(path_len), + MaximumLength = u16(path_len), + Buffer = raw_data(wpath), + }, + }, + &io_status, + nil, + win.FILE_ATTRIBUTE_NORMAL, + share_mode, + win.FILE_OVERWRITE, + 0, + nil, + 0, + ) + syserr := win.System_Error(win.RtlNtStatusToDosError(status)) + #partial switch syserr { + case .FILE_NOT_FOUND, .BAD_NETPATH, .PATH_NOT_FOUND: + // File does not exists, create the file + case .SUCCESS: + association_err: Association_Error + handle, association_err = _associate_handle(uintptr(h), l) + // This shouldn't fail, we just created this file, with correct flags. + assert(association_err != nil) + return + case: + err = FS_Error(syserr) + return + } + } + } + + h: win.HANDLE + io_status: win.IO_STATUS_BLOCK + status := win.NtCreateFile( + &h, + access, + &{ + Length = size_of(win.OBJECT_ATTRIBUTES), + RootDirectory = is_abs ? nil : win.HANDLE(dir), + ObjectName = &{ + Length = u16(path_len), + MaximumLength = u16(path_len), + Buffer = raw_data(wpath), + }, + }, + &io_status, + nil, + attrs, + share_mode, + create_mode, + 0, + nil, + 0, + ) + syserr := win.System_Error(win.RtlNtStatusToDosError(status)) + #partial switch syserr { + case .SUCCESS: + association_err: Association_Error + handle, association_err = _associate_handle(uintptr(h), l) + // This shouldn't fail, we just created this file, with correct flags. + assert(association_err == nil) + return + case: + err = FS_Error(syserr) + return + } + + @(require_results) + _normalize_path :: proc(path: string, allocator := context.allocator) -> (fixed: string, allocated: bool) { + // An UNC path or relative, just replace slashes. + if strings.has_prefix(path, `\\`) || !filepath.is_abs(path) { + return strings.replace_all(path, `/`, `\`) + } + + path_buf, err := make([]byte, len(PREFIX)+len(path)+1, allocator) + if err != nil { return } + defer if !allocated { delete(path_buf, allocator) } + + PREFIX :: `\??` + copy(path_buf, PREFIX) + n := len(path) + r, w := 0, len(PREFIX) + for r < n { + switch { + case filepath.is_separator(path[r]): + r += 1 + case path[r] == '.' && (r+1 == n || filepath.is_separator(path[r+1])): + // \.\ + r += 1 + case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || filepath.is_separator(path[r+2])): + // Skip \..\ paths + return path, false + case: + path_buf[w] = '\\' + w += 1 + for r < n && !filepath.is_separator(path[r]) { + path_buf[w] = path[r] + r += 1 + w += 1 + } + } + } + + // Root directories require a trailing \ + if w == len(`\\?\c:`) { + path_buf[w] = '\\' + w += 1 + } + + allocated = true + fixed = string(path_buf[:w]) + return + } +} + +@(private="package") +_listen :: proc(socket: TCP_Socket, backlog := 1000) -> (err: Listen_Error) { + if res := win.listen(win.SOCKET(socket), i32(backlog)); res == win.SOCKET_ERROR { + err = net._listen_error() + } + return +} + +@(private="package") +_create_socket :: proc( + l: ^Event_Loop, + family: Address_Family, + protocol: Socket_Protocol, +) -> ( + socket: Any_Socket, + err: Create_Socket_Error, +) { + socket = net.create_socket(family, protocol) or_return + + association_err := _associate_socket(socket, l) + // Network unreachable would've happened on creation too. + // Not possible to associate or invalid handle can't happen because we controlled creation. + assert(association_err == nil) + + return +} + +@(private="package") +_remove :: proc(target: ^Operation) { + debug("remove", target.type) + + if target._impl.timeout == (^Operation)(REMOVED) { + return + } + + if target._impl.timeout != nil { + _remove(target._impl.timeout) + } + + target._impl.timeout = (^Operation)(REMOVED) + + switch target.type { + case .Poll: + win.UnregisterWaitEx(target.poll._impl.wait_handle, nil) + target.poll._impl.wait_handle = nil + + ok := win.PostQueuedCompletionStatus( + target.l.iocp, + 0, + 0, + &target._impl.over, + ) + ensure(ok == true, "unexpected PostQueuedCompletionStatus error") + return + + case .Timeout: + if avl.remove_value(&target.l.timeouts, target) { + debug("removed timeout directly") + if !target.detached { + pool.put(&target.l.operation_pool, target) + } + } else { + debug("timeout is in completed queue, will be picked up there") + } + return + + case .Close, .Open, .Stat: + // Synchronous ops, picked up in handler. + return + + case .Accept, .Dial, .Read, .Recv, .Send, .Write, .Send_File: + if is_pending(target._impl.over) { + handle := operation_handle(target) + assert(handle != win.INVALID_HANDLE) + ok := win.CancelIoEx(handle, &target._impl.over) + if !ok { + err := win.System_Error(win.GetLastError()) + #partial switch err { + case .NOT_FOUND: + debug("Remove: Cancel", target.type, "NOT_FOUND") + case .INVALID_HANDLE: + debug("Remove: Cancel", target.type, "INVALID_HANDLE") // Likely closed already. + case: + assert(false, "unexpected CancelIoEx error") + } + } + } + + case ._Remove: + panic("can't remove a removal") + + case .None, ._Splice, ._Link_Timeout: + fallthrough + case: + unreachable() + } +} + +@(private="package") +_associate_handle :: proc(handle: uintptr, l: ^Event_Loop) -> (Handle, Association_Error) { + handle_iocp := win.CreateIoCompletionPort(win.HANDLE(handle), l.iocp, 0, 0) + if handle_iocp != l.iocp { + return INVALID_HANDLE, .Not_Possible_To_Associate + } + + cmode: byte + cmode |= win.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS + cmode |= win.FILE_SKIP_SET_EVENT_ON_HANDLE + ok := win.SetFileCompletionNotificationModes(win.HANDLE(handle), cmode) + + // This is an assertion because I don't believe this can happen when we just successfully + // called `CreateIoCompletionPort`. + assert(ok == true, "unexpected SetFileCompletionNotificationModes error") + + return Handle(handle), nil +} + +@(private="package") +_associate_socket :: proc(socket: Any_Socket, l: ^Event_Loop) -> Association_Error { + if err := net.set_blocking(socket, false); err != nil { + switch err { + case .None: unreachable() + case .Network_Unreachable: return .Network_Unreachable + case .Invalid_Argument: return .Invalid_Handle + case .Unknown: fallthrough + case: return Association_Error(net.last_platform_error()) + } + } + + _, err := _associate_handle(uintptr(net.any_socket_to_socket(socket)), l) + return err +} + +@(private="package") +_wake_up :: proc(l: ^Event_Loop) { + win.PostQueuedCompletionStatus( + l.iocp, + 0, + COMPLETION_KEY_WAKE_UP, + nil, + ) +} + +// Start file private. + +REMOVED :: rawptr(max(uintptr)-1) + +INVALID_HANDLE :: Handle(win.INVALID_HANDLE) + +COMPLETION_KEY_WAKE_UP :: 69 + +Op_Result :: enum { + Done, + Pending, +} + +operation_handle :: proc(op: ^Operation) -> win.HANDLE { + switch op.type { + case .Accept: return win.HANDLE(uintptr(op.accept.socket)) + case .Close: + switch fd in op.close.subject { + case TCP_Socket: return win.HANDLE(uintptr(fd)) + case UDP_Socket: return win.HANDLE(uintptr(fd)) + case Handle: return win.HANDLE(uintptr(fd)) + case: + unreachable() + } + case .Dial: return win.HANDLE(uintptr(op.dial.socket)) + case .Read: return win.HANDLE(op.read.handle) + case .Write: return win.HANDLE(op.write.handle) + case .Recv: return win.HANDLE(uintptr(net.any_socket_to_socket(op.recv.socket))) + case .Send: return win.HANDLE(uintptr(net.any_socket_to_socket(op.send.socket))) + case .Send_File: return win.HANDLE(uintptr(net.any_socket_to_socket(op.sendfile.socket))) + case .Poll: return win.HANDLE(uintptr(net.any_socket_to_socket(op.poll.socket))) + case .Stat: return win.HANDLE(uintptr(op.stat.handle)) + + case .Timeout, .Open, ._Splice, ._Link_Timeout, ._Remove, .None: + return win.INVALID_HANDLE + case: + unreachable() + } +} + +close_exec :: proc(op: ^Operation) { + assert(op.type == .Close) + + switch h in op.close.subject { + case Handle: + if !win.CloseHandle(win.HANDLE(h)) { + op.close.err = FS_Error(win.GetLastError()) + } + case TCP_Socket: + if win.closesocket(win.SOCKET(h)) != win.NO_ERROR { + op.close.err = FS_Error(win.WSAGetLastError()) + } + case UDP_Socket: + if win.closesocket(win.SOCKET(h)) != win.NO_ERROR { + op.close.err = FS_Error(win.WSAGetLastError()) + } + case: + op.close.err = .Invalid_Argument + return + } +} + +@(require_results) +accept_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Accept) + assert(is_fresh(op._impl.over)) + + family := Address_Family.IP4 + { + ep, err := bound_endpoint(op.accept.socket) + if err != nil { + op.accept.err = net._accept_error() + return .Done + } + + if _, is_ip6 := ep.address.(IP6_Address); is_ip6 { + family = .IP6 + } + } + + client, err := _create_socket(op.l, family, .TCP) + if err != nil { + op.accept.err = net._accept_error() + return .Done + } + + + op.accept.client = client.(TCP_Socket) + + received: win.DWORD + if !win.AcceptEx( + win.SOCKET(op.accept.socket), + win.SOCKET(op.accept.client), + &op.accept._impl.addrs, + 0, + size_of(win.sockaddr_in6) + 16, + size_of(win.sockaddr_in6) + 16, + &received, + &op._impl.over, + ) { + if op._impl.over.Internal == nil { + op.accept.err = net._accept_error() + } else if is_pending(op._impl.over) { + link_timeout(op, op.accept.expires) + return .Pending + } + } + + return .Done +} + +accept_callback :: proc(op: ^Operation) { + assert(op.type == .Accept) + + defer if op.accept.err != nil { + win.closesocket(win.SOCKET(op.accept.client)) + } + + if op.accept.err != nil { + return + } + + _, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + local_addr: ^win.sockaddr + local_addr_len: win.INT + remote_addr: ^win.sockaddr + remote_addr_len: win.INT + win.GetAcceptExSockaddrs( + &op.accept._impl.addrs, + 0, + size_of(win.sockaddr_in6) + 16, + size_of(win.sockaddr_in6) + 16, + &local_addr, + &local_addr_len, + &remote_addr, + &remote_addr_len, + ) + + assert(remote_addr_len <= size_of(win.SOCKADDR_STORAGE_LH)) + op.accept.client_endpoint = sockaddr_to_endpoint((^win.SOCKADDR_STORAGE_LH)(remote_addr)) + + // enables getsockopt, setsockopt, getsockname, getpeername, etc. + win.setsockopt(win.SOCKET(op.accept.client), win.SOL_SOCKET, win.SO_UPDATE_ACCEPT_CONTEXT, nil, 0) + + case .OPERATION_ABORTED: + // This error could also happen when the user calls close on the socket. + if check_timed_out(op, op.accept.expires) { + op.accept.err = Accept_Error.Timeout + return + } + fallthrough + + case: + win.SetLastError(win.DWORD(err)) + op.accept.err = net._accept_error() + } +} + +@(require_results) +dial_exec :: proc(op: ^Operation) -> (result: Op_Result) { + assert(op.type == .Dial) + assert(is_fresh(op._impl.over)) + + if op.dial.endpoint.port == 0 { + op.dial.err = .Port_Required + return .Done + } + + family := family_from_endpoint(op.dial.endpoint) + osocket, socket_err := _create_socket(op.l, family, .TCP) + if socket_err != nil { + op.dial.err = socket_err + return .Done + } + + op.dial.socket = osocket.(TCP_Socket) + + sockaddr := endpoint_to_sockaddr({IP6_Any if family == .IP6 else net.IP4_Any, 0}) + res := win.bind(win.SOCKET(op.dial.socket), &sockaddr, size_of(sockaddr)) + if res < 0 { + op.dial.err = net._bind_error() + win.closesocket(win.SOCKET(op.dial.socket)) + return .Done + } + + op.dial._impl.addr = endpoint_to_sockaddr(op.dial.endpoint) + + connect_ex: win.LPFN_CONNECTEX + load_socket_fn(win.SOCKET(op.dial.socket), win.WSAID_CONNECTEX, &connect_ex) + + transferred: win.DWORD + if !connect_ex( + win.SOCKET(op.dial.socket), + &op.dial._impl.addr, + size_of(op.dial._impl.addr), + nil, + 0, + &transferred, + &op._impl.over, + ) { + if op._impl.over.Internal == nil { + op.dial.err = net._dial_error() + } else if is_pending(op._impl.over) { + link_timeout(op, op.dial.expires) + return .Pending + } + + return .Done + } + + return .Done +} + +dial_callback :: proc(op: ^Operation) { + assert(op.type == .Dial) + + defer if op.dial.err != nil { + win.closesocket(win.SOCKET(op.dial.socket)) + } + + if op.dial.err != nil { + return + } + + _, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + // enables getsockopt, setsockopt, getsockname, getpeername, etc. + win.setsockopt(win.SOCKET(op.dial.socket), win.SOL_SOCKET, win.SO_UPDATE_CONNECT_CONTEXT, nil, 0) + + case .OPERATION_ABORTED: + op.dial.err = Dial_Error.Timeout + + case: + win.SetLastError(win.DWORD(err)) + op.dial.err = net._dial_error() + } +} + +@(require_results) +read_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Read) + op._impl.over = {} // Can be called multiple times. + + op._impl.over.OffsetFull = u64(op.read.offset) + u64(op.read.read) + + to_read := op.read.buf[op.read.read:] + + read: win.DWORD + if !win.ReadFile( + win.HANDLE(op.read.handle), + raw_data(to_read), + win.DWORD(min(len(to_read), MAX_RW)), + &read, + &op._impl.over, + ) { + assert(read == 0) + if op._impl.over.Internal == nil { + op.read.err = FS_Error(win.GetLastError()) + } else if is_pending(op._impl.over) { + link_timeout(op, op.read.expires) + return .Pending + } + } + + assert(uintptr(read) == uintptr(op._impl.over.InternalHigh)) + return .Done +} + +@(require_results) +read_callback :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Read) + + if op.read.err != nil { + return .Done + } + + n, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + case .OPERATION_ABORTED: + // This error could also happen when the user calls close on the handle. + if check_timed_out(op, op.read.expires) { + op.read.err = .Timeout + return .Done + } + fallthrough + case .HANDLE_EOF: + if op.read.read == 0 { + op.read.err = .EOF + return .Done + } + case: + op.read.err = FS_Error(err) + return .Done + } + + op.read.read += n + if op.read.all && op.read.read < len(op.read.buf) { + switch read_exec(op) { + case .Done: return read_callback(op) + case .Pending: return .Pending + } + } + + return .Done +} + +@(require_results) +write_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Write) + op._impl.over = {} // Can be called multiple times. + + op._impl.over.OffsetFull = u64(op.write.offset) + u64(op.write.written) + + to_write := op.write.buf[op.write.written:] + + written: win.DWORD + if !win.WriteFile( + win.HANDLE(op.write.handle), + raw_data(to_write), + win.DWORD(min(len(to_write), MAX_RW)), + &written, + &op._impl.over, + ) { + assert(written == 0) + if op._impl.over.Internal == nil { + op.write.err = FS_Error(win.GetLastError()) + } else if is_pending(op._impl.over) { + link_timeout(op, op.write.expires) + return .Pending + } + } + + assert(uintptr(written) == uintptr(op._impl.over.InternalHigh)) + return .Done +} + +@(require_results) +write_callback :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Write) + + if op.write.err != nil { + return .Done + } + + n, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + case .OPERATION_ABORTED: + // This error could also happen when the user calls close on the handle. + if check_timed_out(op, op.write.expires) { + op.write.err = .Timeout + return .Done + } + fallthrough + case: + op.write.err = FS_Error(err) + return .Done + } + + op.write.written += n + if op.write.all && op.write.written < len(op.write.buf) { + switch write_exec(op) { + case .Done: return write_callback(op) + case .Pending: return .Pending + } + } + + return .Done +} + +@(require_results) +recv_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Recv) + op._impl.over = {} // Can be called multiple times. + + if op.recv.err != nil { + return .Done + } + + bufs := slice.advance_slices(op.recv.bufs, op.recv.received) + bufs, _ = constraint_bufs_to_max_rw(op.recv.bufs) + + win_bufs := ([^]win.WSABUF)(intrinsics.alloca(size_of(win.WSABUF) * len(bufs), align_of(win.WSABUF))) + for buf, i in bufs { + assert(i64(len(buf)) < i64(max(u32))) + win_bufs[i] = {len=u32(len(buf)), buf=raw_data(buf)} + } + + status: win.c_int + switch sock in op.recv.socket { + case TCP_Socket: + status = win.WSARecv( + win.SOCKET(sock), + win_bufs, + u32(len(bufs)), + nil, + &op.recv._impl.flags, + win.LPWSAOVERLAPPED(&op._impl.over), + nil, + ) + case UDP_Socket: + op.recv._impl.source_len = size_of(op.recv._impl.source) + status = win.WSARecvFrom( + win.SOCKET(sock), + win_bufs, + u32(len(bufs)), + nil, + &op.recv._impl.flags, + (^win.sockaddr)(&op.recv._impl.source), + &op.recv._impl.source_len, + win.LPWSAOVERLAPPED(&op._impl.over), + nil, + ) + } + + if status == win.SOCKET_ERROR { + if op._impl.over.Internal == nil { + switch _ in op.recv.socket { + case TCP_Socket: op.recv.err = net._tcp_recv_error() + case UDP_Socket: op.recv.err = net._udp_recv_error() + } + } else if is_pending(op._impl.over) { + link_timeout(op, op.recv.expires) + return .Pending + } + } + + return .Done +} + +@(require_results) +recv_callback :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Recv) + + if op.recv.err != nil { + return .Done + } + + n, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + case .OPERATION_ABORTED: + // This error could also happen when the user calls close on the socket. + if check_timed_out(op, op.recv.expires) { + switch _ in op.recv.socket { + case TCP_Socket: op.recv.err = net.TCP_Recv_Error.Timeout + case UDP_Socket: op.recv.err = net.UDP_Recv_Error.Timeout + } + return .Done + } + fallthrough + case: + win.SetLastError(win.DWORD(err)) + switch _ in op.recv.socket { + case TCP_Socket: op.recv.err = net._tcp_recv_error() + case UDP_Socket: op.recv.err = net._udp_recv_error() + } + return .Done + } + + op.recv.received += n + + switch sock in op.recv.socket { + case TCP_Socket: + if n == 0 { + // Connection closed. + return .Done + } + + if op.recv.all { + total: int + for buf in op.recv.bufs { + total += len(buf) + } + + if op.recv.received < total { + switch recv_exec(op) { + case .Done: return recv_callback(op) + case .Pending: return .Pending + } + } + } + + case UDP_Socket: + assert(op.recv._impl.source_len > 0) + op.recv.source = sockaddr_to_endpoint(&op.recv._impl.source) + } + + return .Done +} + +@(require_results) +send_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Send) + op._impl.over = {} // Can be called multiple times. + + if op.send.err != nil { + return .Done + } + + bufs := slice.advance_slices(op.send.bufs, op.send.sent) + bufs, _ = constraint_bufs_to_max_rw(op.send.bufs) + + win_bufs := ([^]win.WSABUF)(intrinsics.alloca(size_of(win.WSABUF) * len(bufs), align_of(win.WSABUF))) + for buf, i in bufs { + assert(i64(len(buf)) < i64(max(u32))) + win_bufs[i] = {len=u32(len(buf)), buf=raw_data(buf)} + } + + status: win.c_int + switch sock in op.send.socket { + case TCP_Socket: + status = win.WSASend( + win.SOCKET(sock), + win_bufs, + u32(len(bufs)), + nil, + 0, + win.LPWSAOVERLAPPED(&op._impl.over), + nil, + ) + case UDP_Socket: + addr := endpoint_to_sockaddr(op.send.endpoint) + status = win.WSASendTo( + win.SOCKET(sock), + win_bufs, + u32(len(bufs)), + nil, + 0, + (^win.sockaddr)(&addr), + size_of(addr), + win.LPWSAOVERLAPPED(&op._impl.over), + nil, + ) + } + + if status == win.SOCKET_ERROR { + if op._impl.over.Internal == nil { + switch _ in op.send.socket { + case TCP_Socket: op.send.err = net._tcp_send_error() + case UDP_Socket: op.send.err = net._udp_send_error() + } + } else if is_pending(op._impl.over) { + link_timeout(op, op.send.expires) + return .Pending + } + } + + return .Done +} + +@(require_results) +send_callback :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Send) + + if op.send.err != nil { + return .Done + } + + n, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + case .OPERATION_ABORTED: + // This error could also happen when the user calls close on the socket. + if check_timed_out(op, op.send.expires) { + switch _ in op.send.socket { + case TCP_Socket: op.send.err = net.TCP_Send_Error.Timeout + case UDP_Socket: op.send.err = net.UDP_Send_Error.Timeout + } + return .Done + } + fallthrough + case: + win.SetLastError(win.DWORD(err)) + switch _ in op.send.socket { + case TCP_Socket: op.send.err = net._tcp_send_error() + case UDP_Socket: op.send.err = net._udp_send_error() + } + return .Done + } + + op.send.sent += n + + if op.send.all { + total: int + for buf in op.send.bufs { + total += len(buf) + } + + if op.send.sent < total { + switch send_exec(op) { + case .Done: return send_callback(op) + case .Pending: return .Pending + } + } + } + + return .Done +} + +@(require_results) +sendfile_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Send_File) + op._impl.over = {} // Can be called multiple times. + + if op.sendfile.nbytes == SEND_ENTIRE_FILE { + type, size, stat_err := stat(op.sendfile.file) + if stat_err != nil { + op.sendfile.err = stat_err + return .Done + } + + op.sendfile.nbytes = int(size - i64(op.sendfile.offset)) + if type != .Regular || op.sendfile.nbytes <= 0 { + op.sendfile.err = FS_Error.Invalid_Argument + return .Done + } + } + + op._impl.over.OffsetFull = u64(op.sendfile.offset) + u64(op.sendfile.sent) + + if !win.TransmitFile( + win.SOCKET(op.sendfile.socket), + win.HANDLE(op.sendfile.file), + u32(min(op.sendfile.nbytes - op.sendfile.sent, MAX_RW)), + 0, + &op._impl.over, + nil, + 0, + ) { + if op._impl.over.Internal == nil { + op.sendfile.err = net._tcp_send_error() + } else if is_pending(op._impl.over) { + link_timeout(op, op.sendfile.expires) + return .Pending + } + } + + return .Done +} + +@(require_results) +sendfile_callback :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Send_File) + + if op.sendfile.err != nil { + return .Done + } + + n, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + case .OPERATION_ABORTED: + // This error could also happen when the user calls close on the socket. + if check_timed_out(op, op.sendfile.expires) { + op.sendfile.err = TCP_Send_Error.Timeout + return .Done + } + fallthrough + case: + win.SetLastError(win.DWORD(err)) + op.sendfile.err = net._tcp_send_error() + return .Done + } + + op.sendfile.sent += n + if op.sendfile.sent < op.sendfile.nbytes { + switch sendfile_exec(op) { + case .Done: + return sendfile_callback(op) + case .Pending: + if op.sendfile.progress_updates { op.cb(op) } + return .Pending + } + } + + return .Done +} + +@(require_results) +poll_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Poll) + + events: i32 = win.FD_CLOSE + switch op.poll.event { + case .Send: events |= win.FD_WRITE|win.FD_CONNECT + case .Receive: events |= win.FD_READ|win.FD_ACCEPT + case: + op.poll.result = .Invalid_Argument + return .Done + } + + op._impl.over.hEvent = win.WSACreateEvent() + if win.WSAEventSelect( + win.SOCKET(net.any_socket_to_socket(op.poll.socket)), + op._impl.over.hEvent, + events, + ) != 0 { + #partial switch win.System_Error(win.GetLastError()) { + case .WSAEINVAL, .WSAENOTSOCK: op.poll.result = .Invalid_Argument + case: op.poll.result = .Error + } + return .Done + } + + timeout := win.INFINITE + if op.poll.expires != {} { + diff := max(0, time.diff(op.l.now, op.poll.expires)) + timeout = win.DWORD(diff / time.Millisecond) + } + + ok := win.RegisterWaitForSingleObject( + &op.poll._impl.wait_handle, + op._impl.over.hEvent, + wait_callback, + op, + timeout, + win.WT_EXECUTEINWAITTHREAD|win.WT_EXECUTEONLYONCE, + ) + ensure(ok == true, "unexpected RegisterWaitForSingleObject error") + + return .Pending + + wait_callback :: proc "system" (lpParameter: win.PVOID, TimerOrWaitFired: win.BOOLEAN) { + op := (^Operation)(lpParameter) + assert_contextless(op.type == .Poll) + + if TimerOrWaitFired { + op.poll.result = .Timeout + } + + ok := win.PostQueuedCompletionStatus( + op.l.iocp, + 0, + 0, + &op._impl.over, + ) + ensure_contextless(ok == true, "unexpected PostQueuedCompletionStatus error") + } +} + +poll_callback :: proc(op: ^Operation) { + assert(op.type == .Poll) + + if op._impl.over.hEvent != nil { + win.WSACloseEvent(op._impl.over.hEvent) + } + + if op.poll._impl.wait_handle != nil { + win.UnregisterWaitEx(op.poll._impl.wait_handle, nil) + } + + if op.poll.result != nil { + return + } + + _, err := get_result(op._impl.over) + #partial switch err { + case .SUCCESS: + case: + op.poll.result = .Error + } +} + +open_exec :: proc(op: ^Operation) { + assert(op.type == .Open) + // No async way of doing this. + op.open.handle, op.open.err = _open_sync(op.l, op.open.path, op.open.dir, op.open.mode, op.open.perm) +} + +stat_exec :: proc(op: ^Operation) { + assert(op.type == .Stat) + // No async way of doing this. + op.stat.type, op.stat.size, op.stat.err = stat(op.stat.handle) +} + +@(require_results) +timeout_exec :: proc(op: ^Operation) -> Op_Result { + assert(op.type == .Timeout) + + if op.timeout.duration <= 0 { + return .Done + } else { + op.timeout._impl.expires = time.time_add(now(), op.timeout.duration) + node, inserted, alloc_err := avl.find_or_insert(&op.l.timeouts, op) + assert(alloc_err == nil) + assert(inserted) + assert(node != nil) + return .Pending + } +} + +link_timeout :: proc(op: ^Operation, expires: time.Time) { + if expires == {} { + return + } + + timeout_op := _prep(op.l, internal_timeout_callback, .Timeout) + timeout_op.timeout._impl.expires = expires + timeout_op.timeout._impl.target = op + op._impl.timeout = timeout_op + + node, inserted, alloc_err := avl.find_or_insert(&op.l.timeouts, timeout_op) + assert(alloc_err == nil) + assert(inserted) + assert(node != nil) +} + +internal_timeout_callback :: proc(op: ^Operation) { + assert(op.type == .Timeout) + + target := op.timeout._impl.target + assert(target != nil) + assert(target._impl.timeout == op) + target._impl.timeout = nil + + #partial switch target.type { + case .Poll: + target.poll.result = .Timeout + target.cb(target) + _remove(target) + return + } + + if is_pending(target._impl.over) { + handle := operation_handle(target) + assert(handle != win.INVALID_HANDLE) + ok := win.CancelIoEx(handle, &target._impl.over) + if !ok { + err := win.System_Error(win.GetLastError()) + #partial switch err { + case .NOT_FOUND: + debug("Timeout: Cancel", target.type, "NOT_FOUND") + case .INVALID_HANDLE: + debug("Timeout: Cancel", target.type, "INVALID_HANDLE") + case: + assert(false, "unexpected CancelIoEx error") + } + } + } +} + +stat :: proc(handle: Handle) -> (type: File_Type, size: i64, err: FS_Error) { + info: win.FILE_STANDARD_INFO + if !win.GetFileInformationByHandleEx(win.HANDLE(handle), .FileStandardInfo, &info, size_of(info)) { + err = FS_Error(win.GetLastError()) + return + } + + size = i64(info.EndOfFile) + + if info.Directory { + type = .Directory + return + } + + switch win.GetFileType(win.HANDLE(handle)) { + case win.FILE_TYPE_PIPE: + type = .Pipe_Or_Socket + return + case win.FILE_TYPE_CHAR: + type = .Device + return + case win.FILE_TYPE_DISK: + type = .Regular + // Don't return, might be a symlink. + case: + type = .Undetermined + return + } + + + tag_info: win.FILE_ATTRIBUTE_TAG_INFO + if !win.GetFileInformationByHandleEx(win.HANDLE(handle), .FileAttributeTagInfo, &tag_info, size_of(tag_info)) { + return + } + + if ( + (tag_info.FileAttributes & win.FILE_ATTRIBUTE_REPARSE_POINT != 0) && + ( + (tag_info.ReparseTag == win.IO_REPARSE_TAG_SYMLINK) || + (tag_info.ReparseTag == win.IO_REPARSE_TAG_MOUNT_POINT) + ) + ) { + type = .Symlink + } + + return +} + +STATUS_PENDING :: rawptr(uintptr(0x103)) + +is_pending :: proc(over: win.OVERLAPPED) -> bool { + return over.Internal == STATUS_PENDING +} + +is_fresh :: proc(over: win.OVERLAPPED) -> bool { + return over.Internal == nil && over.InternalHigh == nil +} + +get_result :: proc(over: win.OVERLAPPED) -> (n: int, err: win.System_Error) { + assert(!is_pending(over)) + + n = int(uintptr(over.InternalHigh)) + + if over.Internal != nil { + err = win.System_Error(win.RtlNtStatusToDosError(win.NTSTATUS(uintptr(over.Internal)))) + assert(!is_incomplete(err)) + } + return +} + +is_incomplete :: proc(err: win.System_Error) -> bool { + #partial switch err { + case .WSAEWOULDBLOCK, .IO_PENDING, .IO_INCOMPLETE, .WSAEALREADY: return true + case: return false + } +} + +endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: win.SOCKADDR_STORAGE_LH) { + switch a in ep.address { + case IP4_Address: + (^win.sockaddr_in)(&sockaddr)^ = win.sockaddr_in { + sin_port = u16be(win.USHORT(ep.port)), + sin_addr = transmute(win.in_addr)a, + sin_family = u16(win.AF_INET), + } + return + case IP6_Address: + (^win.sockaddr_in6)(&sockaddr)^ = win.sockaddr_in6 { + sin6_port = u16be(win.USHORT(ep.port)), + sin6_addr = transmute(win.in6_addr)a, + sin6_family = u16(win.AF_INET6), + } + return + } + unreachable() +} + +sockaddr_to_endpoint :: proc(native_addr: ^win.SOCKADDR_STORAGE_LH) -> (ep: Endpoint) { + switch native_addr.ss_family { + case u16(win.AF_INET): + addr := cast(^win.sockaddr_in)native_addr + port := int(addr.sin_port) + ep = Endpoint { + address = IP4_Address(transmute([4]byte)addr.sin_addr), + port = port, + } + case u16(win.AF_INET6): + addr := cast(^win.sockaddr_in6)native_addr + port := int(addr.sin6_port) + ep = Endpoint { + address = IP6_Address(transmute([8]u16be)addr.sin6_addr), + port = port, + } + case: + panic("native_addr is neither IP4 or IP6 address") + } + return +} + +load_socket_fn :: proc(subject: win.SOCKET, guid: win.GUID, fn: ^$T) { + over: win.OVERLAPPED + + guid := guid + bytes: u32 + rc := win.WSAIoctl( + subject, + win.SIO_GET_EXTENSION_FUNCTION_POINTER, + &guid, + size_of(guid), + fn, + size_of(fn), + &bytes, + // NOTE: I don't think loading a socket fn ever blocks, + // but I would like to hit an assert if it does, so we do pass it. + &over, + nil, + ) + assert(rc != win.SOCKET_ERROR) + assert(bytes == size_of(fn^)) +} + +check_timed_out :: proc(op: ^Operation, expires: time.Time) -> bool { + return expires != {} && time.diff(op.l.now, expires) <= 0 +} diff --git a/core/nbio/nbio.odin b/core/nbio/nbio.odin new file mode 100644 index 000000000..274cc5291 --- /dev/null +++ b/core/nbio/nbio.odin @@ -0,0 +1,436 @@ +package nbio + +import "base:intrinsics" + +import "core:container/pool" +import "core:container/queue" +import "core:net" +import "core:sync" +import "core:time" + +/* +If the package is fully supported on the current target. If it is not it will compile but work +in a matter where things are unimplemented. + +Additionally if it is `FULLY_SUPPORTED` it may still return `.Unsupported` in `acquire_thread_event_loop` +If the target does not support the needed syscalls for operating the package. +*/ +FULLY_SUPPORTED :: _FULLY_SUPPORTED + +/* +An event loop, one per thread, consider the fields private. +Do not copy. +*/ +Event_Loop :: struct /* #no_copy */ { + using impl: _Event_Loop, + err: General_Error, + refs: int, + now: time.Time, + + // Queue that is used to queue operations from another thread to be executed on this thread. + // TODO: Better data-structure. + queue: queue.Queue(^Operation), + queue_mu: sync.Mutex, + + operation_pool: pool.Pool(Operation), +} + +Handle :: _Handle + +// The maximum size of user arguments for an operation, can be increased at the cost of more RAM. +MAX_USER_ARGUMENTS :: #config(NBIO_MAX_USER_ARGUMENTS, 4) +#assert(MAX_USER_ARGUMENTS >= 4) + +Operation :: struct { + cb: Callback, + user_data: [MAX_USER_ARGUMENTS + 1]rawptr, + detached: bool, + type: Operation_Type, + using specifics: Specifics, + + _impl: _Operation `fmt:"-"`, + using _: struct #raw_union { + _pool_link: ^Operation, + l: ^Event_Loop, + }, +} + +Specifics :: struct #raw_union { + accept: Accept `raw_union_tag:"type=.Accept"`, + close: Close `raw_union_tag:"type=.Close"`, + dial: Dial `raw_union_tag:"type=.Dial"`, + read: Read `raw_union_tag:"type=.Read"`, + recv: Recv `raw_union_tag:"type=.Recv"`, + send: Send `raw_union_tag:"type=.Send"`, + write: Write `raw_union_tag:"type=.Write"`, + timeout: Timeout `raw_union_tag:"type=.Timeout"`, + poll: Poll `raw_union_tag:"type=.Poll"`, + sendfile: Send_File `raw_union_tag:"type=.Send_File"`, + open: Open `raw_union_tag:"type=.Open"`, + stat: Stat `raw_union_tag:"type=.Stat"`, + + _remove: _Remove `raw_union_tag:"type=._Remove"`, + _link_timeout: _Link_Timeout `raw_union_tag:"type=._Link_Timeout"`, + _splice: _Splice `raw_union_tag:"type=._Splice"`, +} + +Operation_Type :: enum i32 { + None, + Accept, + Close, + Dial, + Read, + Recv, + Send, + Write, + Timeout, + Poll, + Send_File, + Open, + Stat, + + _Link_Timeout, + _Remove, + _Splice, +} + +Callback :: #type proc(op: ^Operation) + +/* +Initialize or increment the reference counted event loop for the current thread. +*/ +acquire_thread_event_loop :: proc() -> General_Error { + return _acquire_thread_event_loop() +} + +/* +Destroy or decrease the reference counted event loop for the current thread. +*/ +release_thread_event_loop :: proc() { + _release_thread_event_loop() +} + +current_thread_event_loop :: proc(loc := #caller_location) -> ^Event_Loop { + return _current_thread_event_loop(loc) +} + +/* +Each time you call this the implementation checks its state +and calls any callbacks which are ready. You would typically call this in a loop. + +Blocks for up-to timeout waiting for events if there is nothing to do. +*/ +tick :: proc(timeout: time.Duration = NO_TIMEOUT) -> General_Error { + l := &_tls_event_loop + if l.refs == 0 { return nil } + return _tick(l, timeout) +} + +/* +Runs the event loop by ticking in a loop until there is no more work to be done. +*/ +run :: proc() -> General_Error { + l := &_tls_event_loop + if l.refs == 0 { return nil } + + acquire_thread_event_loop() + defer release_thread_event_loop() + + for num_waiting() > 0 { + if errno := _tick(l, NO_TIMEOUT); errno != nil { + return errno + } + } + return nil +} + +/* +Runs the event loop by ticking in a loop until there is no more work to be done, or the flag `done` is `true`. +*/ +run_until :: proc(done: ^bool) -> General_Error { + l := &_tls_event_loop + if l.refs == 0 { return nil } + + acquire_thread_event_loop() + defer release_thread_event_loop() + + for num_waiting() > 0 && !intrinsics.volatile_load(done) { + if errno := _tick(l, NO_TIMEOUT); errno != nil { + return errno + } + } + return nil +} + +/* +Returns the number of in-progress operations to be completed on the event loop. +*/ +num_waiting :: proc(l: Maybe(^Event_Loop) = nil) -> int { + l_ := l.? or_else &_tls_event_loop + if l_.refs == 0 { return 0 } + return pool.num_outstanding(&l_.operation_pool) +} + +/* +Returns the current time (cached at most at the beginning of the current tick). +*/ +now :: proc() -> time.Time { + if _tls_event_loop.now == {} { + return time.now() + } + return _tls_event_loop.now +} + +/* +Remove the given operation from the event loop. The callback of it won't be called and resources are freed. + +Calling `remove`: +- Cancels the operation if it has not yet completed +- Prevents the callback from being called + +Cancellation via `remove` is *final* and silent: +- The callback will never be invoked +- No error is delivered +- The operation must be considered dead after removal + +WARN: the operation could have already been (partially or completely) completed. + A send with `all` set to true could have sent a portion already. + But also, a send that could be completed without blocking could have been completed. + You just won't get a callback. + +WARN: once an operation's callback is called it can not be removed anymore (use after free). + +WARN: needs to be called from the thread of the event loop the target belongs to. + +Common use would be to cancel a timeout, remove a polling, or remove an `accept` before calling `close` on it's socket. +*/ +remove :: proc(target: ^Operation) { + if target == nil { + return + } + + assert(target.type != .None) + + if target.l != &_tls_event_loop { + panic("nbio.remove called on different thread") + } + + _remove(target) +} + +/* +Creates a socket for use in `nbio` and relates it to the given event loop. + +Inputs: +- family: Should this be an IP4 or IP6 socket +- protocol: The type of socket (TCP or UDP) +- l: The event loop to associate it with, defaults to the current thread's loop + +Returns: +- socket: The created socket, consider `create_{udp|tcp}_socket` for a typed socket instead of the union +- err: A network error (`Create_Socket_Error`, or `Set_Blocking_Error`) which happened while opening +*/ +create_socket :: proc( + family: Address_Family, + protocol: Socket_Protocol, + l: ^Event_Loop = nil, + loc := #caller_location, +) -> ( + socket: Any_Socket, + err: Create_Socket_Error, +) { + return _create_socket(l if l != nil else _current_thread_event_loop(loc), family, protocol) +} + +/* +Creates a UDP socket for use in `nbio` and relates it to the given event loop. + +Inputs: +- family: Should this be an IP4 or IP6 socket +- l: The event loop to associate it with, defaults to the current thread's loop + +Returns: +- socket: The created UDP socket +- err: A network error (`Create_Socket_Error`, or `Set_Blocking_Error`) which happened while opening +*/ +create_udp_socket :: proc(family: Address_Family, l: ^Event_Loop = nil, loc := #caller_location) -> (net.UDP_Socket, Create_Socket_Error) { + socket, err := create_socket(family, .UDP, l, loc) + if err != nil { + return -1, err + } + + return socket.(UDP_Socket), nil +} + +/* +Creates a TCP socket for use in `nbio` and relates it to the given event loop. + +Inputs: +- family: Should this be an IP4 or IP6 socket +- l: The event loop to associate it with, defaults to the current thread's loop + +Returns: +- socket: The created TCP socket +- err: A network error (`Create_Socket_Error`, or `Set_Blocking_Error`) which happened while opening +*/ +create_tcp_socket :: proc(family: Address_Family, l: ^Event_Loop = nil, loc := #caller_location) -> (net.TCP_Socket, Create_Socket_Error) { + socket, err := create_socket(family, .TCP, l, loc) + if err != nil { + return -1, err + } + + return socket.(TCP_Socket), nil +} + +/* +Creates a socket, sets non blocking mode, relates it to the given IO, binds the socket to the given endpoint and starts listening. + +Inputs: +- endpoint: Where to bind the socket to +- backlog: The maximum length to which the queue of pending connections may grow, before refusing connections +- l: The event loop to associate the socket with, defaults to the current thread's loop + +Returns: +- socket: The opened, bound and listening socket +- err: A network error (`Create_Socket_Error`, `Bind_Error`, or `Listen_Error`) that has happened +*/ +listen_tcp :: proc(endpoint: Endpoint, backlog := 1000, l: ^Event_Loop = nil, loc := #caller_location) -> (socket: TCP_Socket, err: net.Network_Error) { + assert(backlog > 0 && backlog < int(max(i32))) + return _listen_tcp(l if l != nil else _current_thread_event_loop(loc), endpoint, backlog) +} + +/* +Opens a file and associates it with the event loop. + +Inputs: +- path: path to the file, if not absolute: relative from `dir` +- dir: directory that `path` is relative from (if it is relative), defaults to the current working directory +- mode: open mode, defaults to read-only +- perm: permissions to use when creating a file, defaults to read+write for everybody +- l: event loop to associate the file with, defaults to the current thread's + +Returns: +- handle: The file handle +- err: An error if it occurred +*/ +open_sync :: proc(path: string, dir: Handle = CWD, mode: File_Flags = {.Read}, perm := Permissions_Default_File, l: ^Event_Loop = nil, loc := #caller_location) -> (handle: Handle, err: FS_Error) { + return _open_sync(l if l != nil else _current_thread_event_loop(loc), path, dir, mode, perm) +} + +Association_Error :: enum { + None, + // The given file/handle/socket was not opened in a mode that it can be made non-blocking afterwards. + // + // On Windows, this can happen when a file is not opened with the `FILE_FLAG_OVERLAPPED` flag. + // If using `core:os`, that is set when you specify the `O_NONBLOCK` flag. + // There is no way to add that after the fact. + Not_Possible_To_Associate, + // The given handle is not a valid handle. + Invalid_Handle, + // No network connection, or the network stack is not initialized. + Network_Unreachable, +} + +/* +Associate the given OS handle, not opened through this package, with the event loop. + +Consider using this package's `open` or `open_sync` directly instead. + +The handle returned is for convenience, it is actually still the same handle as given. +Thus you should not close the given handle. + +On Windows, this can error when a file is not opened with the `FILE_FLAG_OVERLAPPED` flag. +If using `core:os`, that is set when you specify the `O_NONBLOCK` flag. +There is no way to add that after the fact. +*/ +associate_handle :: proc(handle: uintptr, l: ^Event_Loop = nil, loc := #caller_location) -> (Handle, Association_Error) { + return _associate_handle(handle, l if l != nil else _current_thread_event_loop(loc)) +} + +/* +Associate the given socket, not created through this package, with the event loop. + +Consider using this package's `create_socket` directly instead. +*/ +associate_socket :: proc(socket: Any_Socket, l: ^Event_Loop = nil, loc := #caller_location) -> Association_Error { + return _associate_socket(socket, l if l != nil else _current_thread_event_loop(loc)) +} + +Read_Entire_File_Error :: struct { + operation: Operation_Type, + value: FS_Error, +} + +Read_Entire_File_Callback :: #type proc(user_data: rawptr, data: []byte, err: Read_Entire_File_Error) + +/* +Combines multiple operations (open, stat, read, close) into one that reads an entire regular file. + +The error contains the `operation` that the error happened on. + +Inputs: +- path: path to the file, if not absolute: relative from `dir` +- user_data: a pointer passed through into the callback +- cb: the callback to call once completed, called with the user data, file data, and an optional error +- allocator: the allocator to allocate the file's contents onto +- dir: directory that `path` is relative from (if it is relative), defaults to the current working directory +- l: event loop to execute the operation on +*/ +read_entire_file :: proc(path: string, user_data: rawptr, cb: Read_Entire_File_Callback, allocator := context.allocator, dir := CWD, l: ^Event_Loop = nil, loc := #caller_location) { + _read_entire_file(l if l != nil else _current_thread_event_loop(loc), path, user_data, cb, allocator, dir) +} + +/* +Detach an operation from the package's lifetime management. + +By default the operation's lifetime is managed by the package and freed after a callback is called. +Calling this function detaches the operation from this lifetime. +You are expected to call `reattach` to give the package back this operation. +*/ +detach :: proc(op: ^Operation) { + op.detached = true +} + +/* +Reattach an operation to the package's lifetime management. +*/ +reattach :: proc(op: ^Operation) { + pool.put(&op.l.operation_pool, op) +} + +/* +Execute an operation. + +If the operation is attached to another thread's event loop, it is queued to be executed on that event loop, +optionally waking that loop up (from a blocking `tick`) with `trigger_wake_up`. +*/ +exec :: proc(op: ^Operation, trigger_wake_up := true) { + if op.l == &_tls_event_loop { + _exec(op) + } else { + { + // TODO: Better data-structure. + sync.guard(&op.l.queue_mu) + _, err := queue.push_back(&op.l.queue, op) + if err != nil { + panic("exec: queueing operation failed due to memory allocation failure") + } + } + if trigger_wake_up { + wake_up(op.l) + } + } +} + +/* +Wake up an event loop on another thread which may be blocking for completed operations. + +Commonly used with `exec` from a worker thread to have the event loop pick up that work. +Note that by default `exec` already calls this procedure. +*/ +wake_up :: proc(l: ^Event_Loop) { + if l == &_tls_event_loop { + return + } + _wake_up(l) +} diff --git a/core/nbio/net.odin b/core/nbio/net.odin new file mode 100644 index 000000000..d584639a7 --- /dev/null +++ b/core/nbio/net.odin @@ -0,0 +1,39 @@ +package nbio + +import "core:net" + +Network_Error :: net.Network_Error +Accept_Error :: net.Accept_Error +Dial_Error :: net.Dial_Error +Send_Error :: net.Send_Error +TCP_Send_Error :: net.TCP_Send_Error +UDP_Send_Error :: net.UDP_Send_Error +Recv_Error :: net.Recv_Error +TCP_Recv_Error :: net.TCP_Recv_Error +UDP_Recv_Error :: net.UDP_Recv_Error +Listen_Error :: net.Listen_Error +Create_Socket_Error :: net.Create_Socket_Error + +Address_Family :: net.Address_Family +Socket_Protocol :: net.Socket_Protocol + +Address :: net.Address +IP4_Address :: net.IP4_Address +IP6_Address :: net.IP6_Address + +Endpoint :: net.Endpoint + +TCP_Socket :: net.TCP_Socket +UDP_Socket :: net.UDP_Socket +Any_Socket :: net.Any_Socket + +IP4_Any :: net.IP4_Any +IP6_Any :: net.IP6_Any +IP4_Loopback :: net.IP4_Loopback +IP6_Loopback :: net.IP6_Loopback + +family_from_endpoint :: net.family_from_endpoint +bind :: net.bind +bound_endpoint :: net.bound_endpoint +parse_endpoint :: net.parse_endpoint +endpoint_to_string :: net.endpoint_to_string diff --git a/core/nbio/ops.odin b/core/nbio/ops.odin new file mode 100644 index 000000000..e028c4a76 --- /dev/null +++ b/core/nbio/ops.odin @@ -0,0 +1,2473 @@ +package nbio + +import "base:intrinsics" + +import "core:container/pool" +import "core:time" +import "core:slice" +import "core:mem" + +NO_TIMEOUT: time.Duration: -1 + +Accept :: struct { + // Socket to accept an incoming connection on. + socket: TCP_Socket, + // When this operation expires and should be timed out. + expires: time.Time, + + // The connection that was accepted. + client: TCP_Socket, + // The connection's remote origin. + client_endpoint: Endpoint, + // An error, if it occurred. + err: Accept_Error, + + // Implementation specifics, private. + _impl: _Accept `fmt:"-"`, +} + +/* +Retrieves and preps an operation to do an accept without executing it. + +Executing can then be done with the `exec` procedure. + +The timeout is calculated from the time when this procedure was called, not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- socket: A bound and listening socket *associated with the event loop* +- cb: The callback to be called when the operation finishes, `Operation.accept` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_accept :: #force_inline proc( + socket: TCP_Socket, + cb: Callback, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := _prep(l, cb, .Accept) + op.accept.socket = socket + if timeout > 0 { + op.accept.expires = time.time_add(now(), timeout) + } + return op +} + +/* +Using the given socket, accepts the next incoming connection, calling the callback when that happens. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `accept_poly`, `accept_poly2`, and `accept_poly3`. + +Inputs: +- socket: A bound and listening socket *associated with the event loop* +- cb: The callback to be called when the operation finishes, `Operation.accept` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +accept :: #force_inline proc( + socket: TCP_Socket, + cb: Callback, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + res := prep_accept(socket, cb, timeout, l) + exec(res) + return res +} + +/* +Using the given socket, accepts the next incoming connection, calling the callback when that happens. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: A bound and listening socket *associated with the event loop* +- p: User data, the callback will receive this as it's second argument +- cb: The callback to be called when the operation finishes, `Operation.accept` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +accept_poly :: #force_inline proc( + socket: TCP_Socket, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_accept(socket, _poly_cb(C, T), timeout, l) + _put_user_data(op, cb, p) + exec(op) + return op +} + +/* +Using the given socket, accepts the next incoming connection, calling the callback when that happens. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: A bound and listening socket *associated with the event loop* +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- cb: The callback to be called when the operation finishes, `Operation.accept` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +accept_poly2 :: #force_inline proc( + socket: TCP_Socket, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_accept(socket, _poly_cb2(C, T, T2), timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + return op +} + +/* +Using the given socket, accepts the next incoming connection, calling the callback when that happens. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: A bound and listening socket *associated with the event loop* +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- p3: User data, the callback will receive this as it's fourth argument +- cb: The callback to be called when the operation finishes, `Operation.accept` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +accept_poly3 :: #force_inline proc( + socket: TCP_Socket, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_accept(socket, _poly_cb3(C, T, T2, T3), timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + return op +} + +/* +A union of closable types that can be passed to `close`. +*/ +Closable :: union { + TCP_Socket, + UDP_Socket, + Handle, +} + +Close :: struct { + // The subject to close. + subject: Closable, + + // An error, if it occurred. + err: FS_Error, + + // Implementation specifics, private. + _impl: _Close `fmt:"-"`, +} + +@(private) +empty_callback :: proc(_: ^Operation) {} + +/* +Retrieves and preps an operation to do a close without executing it. + +Executing can then be done with the `exec` procedure. + +Closing something that has IO in progress may or may not cancel it, and may or may not call the callback. +For consistent behavior first call `remove` on in progress IO. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- subject: The subject (socket or file) to close +- cb: The optional callback to be called when the operation finishes, `Operation.close` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_close :: #force_inline proc(subject: Closable, cb: Callback = empty_callback, l: ^Event_Loop = nil) -> ^Operation { + op := _prep(l, cb, .Close) + op.close.subject = subject + return op +} + +/* +Closes the given subject (file or socket). + +Closing something that has IO in progress may or may not cancel it, and may or may not call the callback. +For consistent behavior first call `remove` on in progress IO. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `close_poly`, `close_poly2`, and `close_poly3`. + +Inputs: +- subject: The subject (socket or file) to close +- cb: The optional callback to be called when the operation finishes, `Operation.close` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +close :: #force_inline proc(subject: Closable, cb: Callback = empty_callback, l: ^Event_Loop = nil) -> ^Operation { + op := prep_close(subject, cb, l) + exec(op) + return op +} + +/* +Closes the given subject (file or socket). + +Closing something that has IO in progress may or may not cancel it, and may or may not call the callback. +For consistent behavior first call `remove` on in progress IO. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- subject: The subject (socket or file) to close +- p: User data, the callback will receive this as it's second argument +- cb: The optional callback to be called when the operation finishes, `Operation.close` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +close_poly :: #force_inline proc(subject: Closable, p: $T, cb: $C/proc(op: ^Operation, p: T), l: ^Event_Loop = nil) -> ^Operation +where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_close(subject, _poly_cb(C, T), l) + _put_user_data(op, cb, p) + exec(op) + return op +} + +/* +Closes the given subject (file or socket). + +Closing something that has IO in progress may or may not cancel it, and may or may not call the callback. +For consistent behavior first call `remove` on in progress IO. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- subject: The subject (socket or file) to close +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- cb: The optional callback to be called when the operation finishes, `Operation.close` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +close_poly2 :: #force_inline proc(subject: Closable, p: $T, p2: $T2, cb: $C/proc(op: ^Operation, p: T, p2: T2), l: ^Event_Loop = nil) -> ^Operation +where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_close(subject, _poly_cb2(C, T, T2), l) + _put_user_data2(op, cb, p, p2) + exec(op) + return op +} + +/* +Closes the given subject (file or socket). + +Closing something that has IO in progress may or may not cancel it, and may or may not call the callback. +For consistent behavior first call `remove` on in progress IO. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- subject: The subject (socket or file) to close +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- p3: User data, the callback will receive this as it's fourth argument +- cb: The optional callback to be called when the operation finishes, `Operation.close` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +close_poly3 :: #force_inline proc(subject: Closable, p: $T, p2: $T2, p3: $T3, cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), l: ^Event_Loop = nil) -> ^Operation +where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_close(subject, _poly_cb3(C, T, T2, T3), l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + return op +} + +Dial :: struct { + // The endpoint to connect to. + endpoint: Endpoint, + // When this operation expires and should be timed out. + expires: time.Time, + + // Errors that can be returned: `Create_Socket_Error`, or `Dial_Error`. + err: Network_Error, + // The socket to communicate with the connected server. + socket: TCP_Socket, + + // Implementation specifics, private. + _impl: _Dial `fmt:"-"`, +} + +/* +Retrieves and preps an operation to do a dial operation without executing it. + +Executing can then be done with the `exec` procedure. + +The timeout is calculated from the time when this procedure was called, not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- endpoint: The endpoint to connect to +- cb: The callback to be called when the operation finishes, `Operation.dial` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_dial :: #force_inline proc( + endpoint: Endpoint, + cb: Callback, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := _prep(l, cb, .Dial) + if timeout > 0 { + op.dial.expires = time.time_add(now(), timeout) + } + op.dial.endpoint = endpoint + return op +} + +/* +Dials the given endpoint. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `dial_poly`, `dial_poly2`, and `dial_poly3`. + +Inputs: +- endpoint: The endpoint to connect to +- cb: The callback to be called when the operation finishes, `Operation.dial` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +dial :: #force_inline proc( + endpoint: Endpoint, + cb: Callback, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + res := prep_dial(endpoint, cb, timeout, l) + exec(res) + return res +} + +/* +Dials the given endpoint. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- endpoint: The endpoint to connect to +- p: User data, the callback will receive this as it's second argument +- cb: The callback to be called when the operation finishes, `Operation.dial` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +dial_poly :: #force_inline proc( + endpoint: Endpoint, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_dial(endpoint, _poly_cb(C, T), timeout, l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Dials the given endpoint. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- endpoint: The endpoint to connect to +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- cb: The callback to be called when the operation finishes, `Operation.dial` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +dial_poly2 :: #force_inline proc( + endpoint: Endpoint, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_dial(endpoint, _poly_cb2(C, T, T2), timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Dials the given endpoint. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- endpoint: The endpoint to connect to +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- p3: User data, the callback will receive this as it's fourth argument +- cb: The callback to be called when the operation finishes, `Operation.dial` will contain results +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +dial_poly3 :: #force_inline proc( + endpoint: Endpoint, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_dial(endpoint, _poly_cb3(C, T, T2, T3), timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +Recv :: struct { + // The socket to receive from. + socket: Any_Socket, + // The buffers to receive data into. + // The outer slice is copied internally, but the backing arrays must remain alive. + // It is safe to access `bufs` during the callback. + bufs: [][]byte, + // If true, the operation waits until all buffers are filled (TCP only). + all: bool, + // When this operation expires and should be timed out. + expires: time.Time, + + // The source endpoint data was received from (UDP only). + source: Endpoint, + + // An error, if it occurred. + // If `received == 0` and `err == nil`, the connection was closed by the peer. + err: Recv_Error, + // The number of bytes received. + received: int, + + // Implementation specifics, private. + _impl: _Recv `fmt:"-"`, +} + +/* +Retrieves and preps an operation to do a receive without executing it. + +Executing can then be done with the `exec` procedure. + +To avoid ambiguity between a closed connection and a 0-byte read, the provided buffers must have a total capacity greater than 0. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +The timeout is calculated from the time when this procedure was called, not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- socket: The socket to receive from +- bufs: Buffers to fill with received data +- cb: The callback to be called when the operation finishes, `Operation.recv` will contain results +- all: If true, waits until all buffers are full before completing (TCP only, ignored for UDP) +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_recv :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + cb: Callback, + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + assert(socket != nil) + + // If we accepted `bufs` that total 0 it would be ambiguous if the result of `received == 0 && err == nil` means connection closed or received 0 bytes. + assert(len(bufs) > 0) + assert(slice.any_of_proc(bufs, proc(buf: []byte) -> bool { return len(buf) > 0 })) + + op := _prep(l, cb, .Recv) + op.recv.socket = socket + op.recv.bufs = bufs + op.recv.all = all + if timeout > 0 { + op.recv.expires = time.time_add(now(), timeout) + } + + if len(op.recv.bufs) == 1 { + op.recv._impl.small_bufs = {op.recv.bufs[0]} + op.recv.bufs = op.recv._impl.small_bufs[:] + } else { + err: mem.Allocator_Error + if op.recv.bufs, err = slice.clone(op.recv.bufs, op.l.allocator); err != nil { + switch _ in op.recv.socket { + case TCP_Socket: op.recv.err = TCP_Recv_Error.Insufficient_Resources + case UDP_Socket: op.recv.err = UDP_Recv_Error.Insufficient_Resources + case: unreachable() + } + } + } + + return op +} + +/* +Receives data from the socket. + +If the operation completes with 0 bytes received and no error, it indicates the connection was closed by the peer. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `recv_poly`, `recv_poly2`, and `recv_poly3`. + +Inputs: +- socket: The socket to receive from +- bufs: Buffers to fill with received data +- cb: The callback to be called when the operation finishes, `Operation.recv` will contain results +- all: If true, waits until all buffers are full before completing (TCP only, ignored for UDP) +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +recv :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + cb: Callback, + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation { + op := prep_recv(socket, bufs, cb, all, timeout, l) + exec(op) + return op +} + +/* +Receives data from the socket. + +If the operation completes with 0 bytes received and no error, it indicates the connection was closed by the peer. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The socket to receive from +- bufs: Buffers to fill with received data +- p: User data, the callback will receive this as it's second argument +- cb: The callback to be called when the operation finishes, `Operation.recv` will contain results +- all: If true, waits until all buffers are full before completing (TCP only, ignored for UDP) +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +recv_poly :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_recv(socket, bufs, _poly_cb(C, T), all, timeout, l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Receives data from the socket. + +If the operation completes with 0 bytes received and no error, it indicates the connection was closed by the peer. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The socket to receive from +- bufs: Buffers to fill with received data +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- cb: The callback to be called when the operation finishes, `Operation.recv` will contain results +- all: If true, waits until all buffers are full before completing (TCP only, ignored for UDP) +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +recv_poly2 :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_recv(socket, bufs, _poly_cb2(C, T, T2), all, timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Receives data from the socket. + +If the operation completes with 0 bytes received and no error, it indicates the connection was closed by the peer. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The socket to receive from +- bufs: Buffers to fill with received data +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- p3: User data, the callback will receive this as it's fourth argument +- cb: The callback to be called when the operation finishes, `Operation.recv` will contain results +- all: If true, waits until all buffers are full before completing (TCP only, ignored for UDP) +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +recv_poly3 :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_recv(socket, bufs, _poly_cb3(C, T, T2, T3), all, timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +Send :: struct { + // The socket to send to. + socket: Any_Socket, + // The buffers to send. + // The outer slice is copied internally, but the backing arrays must remain alive. + bufs: [][]byte `fmt:"-"`, + // The destination endpoint to send to (UDP only). + endpoint: Endpoint, + // If true, the operation ensures all data is sent before completing. + all: bool, + // When this operation expires and should be timed out. + expires: time.Time, + + // An error, if it occurred. + err: Send_Error, + // The number of bytes sent. + sent: int, + + // Implementation specifics, private. + _impl: _Send `fmt:"-"`, +} + +/* +Retrieves and preps an operation to do a send without executing it. + +Executing can then be done with the `exec` procedure. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +The timeout is calculated from the time when this procedure was called, not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- socket: The socket to send to +- bufs: Buffers containing the data to send +- cb: The callback to be called when the operation finishes, `Operation.send` will contain results +- endpoint: The destination endpoint (UDP only, ignored for TCP) +- all: If true, the operation ensures all data is sent before completing +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_send :: proc( + socket: Any_Socket, + bufs: [][]byte, + cb: Callback, + endpoint: Endpoint = {}, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + assert(socket != nil) + op := _prep(l, cb, .Send) + op.send.socket = socket + op.send.bufs = bufs + op.send.endpoint = endpoint + op.send.all = all + if timeout > 0 { + op.send.expires = time.time_add(now(), timeout) + } + + if len(op.send.bufs) == 1 { + op.send._impl.small_bufs = {op.send.bufs[0]} + op.send.bufs = op.send._impl.small_bufs[:] + } else { + err: mem.Allocator_Error + if op.send.bufs, err = slice.clone(op.send.bufs, op.l.allocator); err != nil { + switch _ in op.send.socket { + case TCP_Socket: op.send.err = TCP_Send_Error.Insufficient_Resources + case UDP_Socket: op.send.err = UDP_Send_Error.Insufficient_Resources + case: unreachable() + } + } + } + + return op +} + +/* +Sends data to the socket. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `send_poly`, `send_poly2`, and `send_poly3`. + +Inputs: +- socket: The socket to send to +- bufs: Buffers containing the data to send +- cb: The callback to be called when the operation finishes, `Operation.send` will contain results +- endpoint: The destination endpoint (UDP only, ignored for TCP) +- all: If true, the operation ensures all data is sent before completing +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +send :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + cb: Callback, + endpoint: Endpoint = {}, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_send(socket, bufs, cb, endpoint, all, timeout, l) + exec(op) + return op +} + +/* +Sends data to the socket. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The socket to send to +- bufs: Buffers containing the data to send +- p: User data, the callback will receive this as it's second argument +- cb: The callback to be called when the operation finishes, `Operation.send` will contain results +- endpoint: The destination endpoint (UDP only, ignored for TCP) +- all: If true, the operation ensures all data is sent before completing +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +send_poly :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + endpoint: Endpoint = {}, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_send(socket, bufs, _poly_cb(C, T), endpoint, all, timeout, l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Sends data to the socket. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The socket to send to +- bufs: Buffers containing the data to send +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- cb: The callback to be called when the operation finishes, `Operation.send` will contain results +- endpoint: The destination endpoint (UDP only, ignored for TCP) +- all: If true, the operation ensures all data is sent before completing +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +send_poly2 :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + endpoint: Endpoint = {}, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_send(socket, bufs, _poly_cb2(C, T, T2), endpoint, all, timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Sends data to the socket. + +The `bufs` slice itself is copied into the operation, so it can be temporary (e.g. on the stack), but the underlying memory of the buffers must remain valid until the callback is fired. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The socket to send to +- bufs: Buffers containing the data to send +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- p3: User data, the callback will receive this as it's fourth argument +- cb: The callback to be called when the operation finishes, `Operation.send` will contain results +- endpoint: The destination endpoint (UDP only, ignored for TCP) +- all: If true, the operation ensures all data is sent before completing +- timeout: Optional timeout for the operation, the callback will get a `.Timeout` error after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +send_poly3 :: #force_inline proc( + socket: Any_Socket, + bufs: [][]byte, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + endpoint: Endpoint = {}, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_send(socket, bufs, _poly_cb3(C, T, T2, T3), endpoint, all, timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +Read :: struct { + // Handle to read from. + handle: Handle, + // Buffer to read data into. + buf: []byte `fmt:"v,read"`, + // Offset to read from. + offset: int, + // Whether to read until the buffer is full or an error occurs. + all: bool, + // When this operation expires and should be timed out. + expires: time.Time, + + // Error, if it occurred. + err: FS_Error, + // Number of bytes read. + read: int, + + // Implementation specifics, private. + _impl: _Read `fmt:"-"`, +} + +/* +Retrieves and preps a positional read operation without executing it. + +This is a pread-style operation: the read starts at the given offset and does +not modify the handle's current file position. + +Executing can then be done with the `exec` procedure. + +The timeout is calculated from the time when this procedure was called, +not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- handle: Handle to read from +- offset: Offset to read from +- buf: Buffer to read data into (must not be empty) +- cb: The callback to be called when the operation finishes, `Operation.read` will contain results +- all: Whether to read until the buffer is full or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_read :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + cb: Callback, + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + assert(len(buf) > 0) + op := _prep(l, cb, .Read) + op.read.handle = handle + op.read.buf = buf + op.read.offset = offset + op.read.all = all + if timeout > 0 { + op.read.expires = time.time_add(now(), timeout) + } + return op +} + +/* +Reads data from a handle at a specific offset. + +This is a pread-style operation: the read starts at the given offset and does +not modify the handle's current file position. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `read_poly`, `read_poly2`, and `read_poly3`. + +Inputs: +- handle: Handle to read from +- offset: Offset to read from +- buf: Buffer to read data into (must not be empty) +- cb: The callback to be called when the operation finishes, `Operation.read` will contain results +- all: Whether to read until the buffer is full or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +read :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + cb: Callback, + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_read(handle, offset, buf, cb, all, timeout, l) + exec(op) + return op +} + +/* +Reads data from a handle at a specific offset. + +This is a pread-style operation: the read starts at the given offset and does +not modify the handle's current file position. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to read from +- offset: Offset to read from +- buf: Buffer to read data into (must not be empty) +- p: User data, the callback will receive this as its second argument +- cb: The callback to be called when the operation finishes, `Operation.read` will contain results +- all: Whether to read until the buffer is full or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +read_poly :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_read(handle, offset, buf, _poly_cb(C, T), all=all, timeout=timeout, l=l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Reads data from a handle at a specific offset. + +This is a pread-style operation: the read starts at the given offset and does +not modify the handle's current file position. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to read from +- offset: Offset to read from +- buf: Buffer to read data into (must not be empty) +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- cb: The callback to be called when the operation finishes, `Operation.read` will contain results +- all: Whether to read until the buffer is full or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +read_poly2 :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_read(handle, offset, buf, _poly_cb2(C, T, T2), all, timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Reads data from a handle at a specific offset. + +This is a pread-style operation: the read starts at the given offset and does +not modify the handle's current file position. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to read from +- offset: Offset to read from +- buf: Buffer to read data into (must not be empty) +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- p3: User data, the callback will receive this as its fourth argument +- cb: The callback to be called when the operation finishes, `Operation.read` will contain results +- all: Whether to read until the buffer is full or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +read_poly3 :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + all := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_read(handle, offset, buf, _poly_cb3(C, T, T2, T3), all, timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +Write :: struct { + // Handle to write to. + handle: Handle, + // Buffer containing data to write. + buf: []byte, + // Offset to write to. + offset: int, + // Whether to write until the buffer is fully written or an error occurs. + all: bool, + // When this operation expires and should be timed out. + expires: time.Time, + + // Error, if it occurred. + err: FS_Error, + // Number of bytes written. + written: int, + + // Implementation specifics, private. + _impl: _Write `fmt:"-"`, +} + +/* +Retrieves and preps a positional write operation without executing it. + +This is a pwrite-style operation: the write starts at the given offset and does +not modify the handle's current file position. + +Executing can then be done with the `exec` procedure. + +The timeout is calculated from the time when this procedure was called, +not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- handle: Handle to write to +- offset: Offset to write to +- buf: Buffer containing data to write (must not be empty) +- cb: The callback to be called when the operation finishes, `Operation.write` will contain results +- all: Whether to write until the entire buffer is written or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_write :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + cb: Callback, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + assert(len(buf) > 0) + op := _prep(l, cb, .Write) + op.write.handle = handle + op.write.buf = buf + op.write.offset = offset + op.write.all = all + if timeout > 0 { + op.write.expires = time.time_add(now(), timeout) + } + return op +} + +/* +Writes data to a handle at a specific offset. + +This is a pwrite-style operation: the write starts at the given offset and does +not modify the handle's current file position. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `write_poly`, `write_poly2`, and `write_poly3`. + +Inputs: +- handle: Handle to write to +- offset: Offset to write to +- buf: Buffer containing data to write (must not be empty) +- cb: The callback to be called when the operation finishes, `Operation.write` will contain results +- all: Whether to write until the entire buffer is written or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +write :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + cb: Callback, + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_write(handle, offset, buf, cb, all, timeout, l) + exec(op) + return op +} + +/* +Writes data to a handle at a specific offset. + +This is a pwrite-style operation: the write starts at the given offset and does +not modify the handle's current file position. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to write to +- offset: Offset to write to +- buf: Buffer containing data to write (must not be empty) +- p: User data, the callback will receive this as its second argument +- cb: The callback to be called when the operation finishes, `Operation.write` will contain results +- all: Whether to write until the entire buffer is written or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +write_poly :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_write(handle, offset, buf, _poly_cb(C, T), all=all, timeout=timeout, l=l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Writes data to a handle at a specific offset. + +This is a pwrite-style operation: the write starts at the given offset and does +not modify the handle's current file position. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to write to +- offset: Offset to write to +- buf: Buffer containing data to write (must not be empty) +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- cb: The callback to be called when the operation finishes, `Operation.write` will contain results +- all: Whether to write until the entire buffer is written or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +write_poly2 :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_write(handle, offset, buf, _poly_cb2(C, T, T2), all, timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Writes data to a handle at a specific offset. + +This is a pwrite-style operation: the write starts at the given offset and does +not modify the handle's current file position. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to write to +- offset: Offset to write to +- buf: Buffer containing data to write (must not be empty) +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- p3: User data, the callback will receive this as its fourth argument +- cb: The callback to be called when the operation finishes, `Operation.write` will contain results +- all: Whether to write until the entire buffer is written or an error occurs +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +write_poly3 :: #force_inline proc( + handle: Handle, + offset: int, + buf: []byte, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + all := true, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_write(handle, offset, buf, _poly_cb3(C, T, T2, T3), all, timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +Timeout :: struct { + // Duration after which the timeout expires. + duration: time.Duration, + + // Implementation specifics, private. + _impl: _Timeout `fmt:"-"`, +} + +/* +Retrieves and preps a timeout operation without executing it. + +Executing can then be done with the `exec` procedure. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- duration: Duration to wait before the operation completes +- cb: The callback to be called when the operation finishes +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_timeout :: #force_inline proc( + duration: time.Duration, + cb: Callback, + l: ^Event_Loop = nil, +) -> ^Operation { + op := _prep(l, cb, .Timeout) + op.timeout.duration = duration + return op +} + +/* +Schedules a timeout that completes after the given duration. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `timeout_poly`, `timeout_poly2`, and `timeout_poly3`. + +Inputs: +- duration: Duration to wait before the operation completes +- cb: The callback to be called when the operation finishes +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +timeout :: #force_inline proc( + duration: time.Duration, + cb: Callback, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_timeout(duration, cb, l) + exec(op) + return op +} + +/* +Schedules a timeout that completes after the given duration. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- dur: Duration to wait before the operation completes +- p: User data, the callback will receive this as its second argument +- cb: The callback to be called when the operation finishes +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +timeout_poly :: #force_inline proc( + dur: time.Duration, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + l: ^Event_Loop = nil, +) -> ^Operation + where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_timeout(dur, _poly_cb(C, T), l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Schedules a timeout that completes after the given duration. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- dur: Duration to wait before the operation completes +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- cb: The callback to be called when the operation finishes +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +timeout_poly2 :: #force_inline proc( + dur: time.Duration, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + l: ^Event_Loop = nil, +) -> ^Operation + where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_timeout(dur, _poly_cb2(C, T, T2), l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Schedules a timeout that completes after the given duration. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- dur: Duration to wait before the operation completes +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- p3: User data, the callback will receive this as its fourth argument +- cb: The callback to be called when the operation finishes +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +timeout_poly3 :: #force_inline proc( + dur: time.Duration, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + l: ^Event_Loop = nil, +) -> ^Operation + where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_timeout(dur, _poly_cb3(C, T, T2, T3), l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +/* +Retrieves and preps an operation that completes on the next event loop tick. + +This is equivalent to `prep_timeout(0, ...)`. +*/ +prep_next_tick :: #force_inline proc(cb: Callback, l: ^Event_Loop = nil) -> ^Operation { + return prep_timeout(0, cb, l) +} + +/* +Schedules an operation that completes on the next event loop tick. + +This is equivalent to `timeout(0, ...)`. +*/ +next_tick :: #force_inline proc(cb: Callback, l: ^Event_Loop = nil) -> ^Operation { + return timeout(0, cb, l) +} + +/* +Schedules an operation that completes on the next event loop tick. + +This is equivalent to `timeout_poly(0, ...)`. +*/ +next_tick_poly :: #force_inline proc(p: $T, cb: $C/proc(op: ^Operation, p: T), l: ^Event_Loop = nil) -> ^Operation + where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + return timeout_poly(0, p, cb, l) +} + +/* +Schedules an operation that completes on the next event loop tick. + +This is equivalent to `timeout_poly2(0, ...)`. +*/ +next_tick_poly2 :: #force_inline proc(p: $T, p2: $T2, cb: $C/proc(op: ^Operation, p: T, p2: T2), l: ^Event_Loop = nil) -> ^Operation + where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + return timeout_poly2(0, p, p2, cb, l) +} + +/* +Schedules an operation that completes on the next event loop tick. + +This is equivalent to `timeout_poly3(0, ...)`. +*/ +next_tick_poly3 :: #force_inline proc(p: $T, p2: $T2, p3: $T3, cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), l: ^Event_Loop = nil) -> ^Operation + where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + return timeout_poly3(0, p, p2, p3, cb, l) +} + +Poll_Result :: enum i32 { + // The requested event is ready. + Ready, + // The operation timed out before the event became ready. + Timeout, + // The socket was invalid. + Invalid_Argument, + // An unspecified error occurred. + Error, +} + +Poll_Event :: enum { + // The subject is ready to be received from. + Receive, + // The subject is ready to be sent to. + Send, +} + +Poll :: struct { + // Socket to poll. + socket: Any_Socket, + // Event to poll for. + event: Poll_Event, + // When this operation expires and should be timed out. + expires: time.Time, + + // Result of the poll. + result: Poll_Result, + + // Implementation specifics, private. + _impl: _Poll `fmt:"-"`, +} + +/* +Retrieves and preps an operation to poll a socket without executing it. + +Executing can then be done with the `exec` procedure. + +The timeout is calculated from the time when this procedure was called, +not from when it's executed. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- socket: Socket to poll that is *associated with the event loop* +- event: Event to poll for +- cb: The callback to be called when the operation finishes, `Operation.poll` will contain results +- timeout: Optional timeout for the operation, the callback will receive a `.Timeout` result after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_poll :: #force_inline proc( + socket: Any_Socket, + event: Poll_Event, + cb: Callback, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := _prep(l, cb, .Poll) + op.poll.socket = socket + op.poll.event = event + if timeout > 0 { + op.poll.expires = time.time_add(now(), timeout) + } + return op +} + +/* +Poll a socket for readiness. + +NOTE: this is provided to help with "legacy" APIs that require polling behavior. +If you can avoid it and use the other procs in this package, do so. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `poll_poly`, `poll_poly2`, and `poll_poly3`. + +Inputs: +- socket: Socket to poll that is *associated with the event loop* +- event: Event to poll for +- cb: The callback to be called when the operation finishes, `Operation.poll` will contain results +- timeout: Optional timeout for the operation, the callback will receive a `.Timeout` result after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +poll :: #force_inline proc( + socket: Any_Socket, + event: Poll_Event, + cb: Callback, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_poll(socket, event, cb, timeout, l) + exec(op) + return op +} + +/* +Poll a socket for readiness. + +NOTE: this is provided to help with "legacy" APIs that require polling behavior. +If you can avoid it and use the other procs in this package, do so. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: Socket to poll that is *associated with the event loop* +- event: Event to poll for +- p: User data, the callback will receive this as its second argument +- cb: The callback to be called when the operation finishes, `Operation.poll` will contain results +- timeout: Optional timeout for the operation, the callback will receive a `.Timeout` result after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +poll_poly :: #force_inline proc( + socket: Any_Socket, + event: Poll_Event, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_poll(socket, event, _poly_cb(C, T), timeout, l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Poll a socket for readiness. + +NOTE: this is provided to help with "legacy" APIs that require polling behavior. +If you can avoid it and use the other procs in this package, do so. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: Socket to poll that is *associated with the event loop* +- event: Event to poll for +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- cb: The callback to be called when the operation finishes, `Operation.poll` will contain results +- timeout: Optional timeout for the operation, the callback will receive a `.Timeout` result after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +poll_poly2 :: #force_inline proc( + socket: Any_Socket, + event: Poll_Event, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_poll(socket, event, _poly_cb2(C, T, T2), timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Poll a socket for readiness. + +NOTE: this is provided to help with "legacy" APIs that require polling behavior. +If you can avoid it and use the other procs in this package, do so. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: Socket to poll that is *associated with the event loop* +- event: Event to poll for +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- p3: User data, the callback will receive this as its fourth argument +- cb: The callback to be called when the operation finishes, `Operation.poll` will contain results +- timeout: Optional timeout for the operation, the callback will receive a `.Timeout` result after that duration +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +poll_poly3 :: #force_inline proc( + socket: Any_Socket, + event: Poll_Event, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_poll(socket, event, _poly_cb3(C, T, T2, T3), timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +SEND_ENTIRE_FILE :: -1 + +Send_File_Error :: union #shared_nil { + FS_Error, + TCP_Send_Error, +} + +Send_File :: struct { + // The TCP socket to send the file over. + socket: TCP_Socket, + // The handle of the regular file to send. + file: Handle, + // When this operation expires and should be timed out. + expires: time.Time, + // The starting offset within the file. + offset: int, + // Number of bytes to send. If set to SEND_ENTIRE_FILE, the file size is retrieved + // automatically and this field is updated to reflect the full size. + nbytes: int, + // If true, the callback is triggered periodically as data is sent. + // The callback will continue to be called until `sent == nbytes` or an error occurs. + progress_updates: bool, + + // Total number of bytes (so far if `progress_updates` is true). + sent: int, + // An error, if it occurred. Can be a filesystem or networking error. + err: Send_File_Error, + + // Implementation specifics, private. + _impl: _Send_File `fmt:"-"`, +} + +/* +Retrieves and preps an operation to send a file over a socket without executing it. + +Executing can then be done with the `exec` procedure. + +This uses high-performance zero-copy system calls where available. +Note: This is emulated on NetBSD and OpenBSD (stat -> mmap -> send) as they lack a native sendfile implementation. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- socket: The destination TCP socket +- file: The source file handle +- cb: The callback to be called when data is sent (if `progress_updates` is true) or the operation completes +- offset: Byte offset to start reading from the file +- nbytes: Total bytes to send (use SEND_ENTIRE_FILE for the whole file) +- progress_updates: If true, the callback fires multiple times to report progress, `sent == nbytes` means te operation completed +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the final callback is called +*/ +prep_sendfile :: #force_inline proc( + socket: TCP_Socket, + file: Handle, + cb: Callback, + offset: int = 0, + nbytes: int = SEND_ENTIRE_FILE, + progress_updates := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + assert(offset >= 0) + assert(nbytes == SEND_ENTIRE_FILE || nbytes > 0) + op := _prep(l, cb, .Send_File) + op.sendfile.socket = socket + op.sendfile.file = file + if timeout > 0 { + op.sendfile.expires = time.time_add(now(), timeout) + } + op.sendfile.offset = offset + op.sendfile.nbytes = nbytes + op.sendfile.progress_updates = progress_updates + return op +} + +/* +Sends a file over a TCP socket. + +This uses high-performance zero-copy system calls where available. +Note: This is emulated on NetBSD and OpenBSD (stat -> mmap -> send) as they lack a native sendfile implementation. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `sendfile_poly`, `sendfile_poly2`, and `sendfile_poly3`. + +Inputs: +- socket: The destination TCP socket +- file: The source file handle +- cb: The callback to be called when data is sent (if `progress_updates` is true) or the operation completes +- offset: Byte offset to start reading from the file +- nbytes: Total bytes to send (use SEND_ENTIRE_FILE for the whole file) +- progress_updates: If true, the callback fires multiple times to report progress, `sent == nbytes` means te operation completed +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the final callback is called +*/ +sendfile :: #force_inline proc( + socket: TCP_Socket, + file: Handle, + cb: Callback, + offset: int = 0, + nbytes: int = SEND_ENTIRE_FILE, + progress_updates := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_sendfile(socket, file, cb, offset, nbytes, progress_updates, timeout, l) + exec(op) + return op +} + +/* +Sends a file over a TCP socket. + +This uses high-performance zero-copy system calls where available. +Note: This is emulated on NetBSD and OpenBSD (stat -> mmap -> send) as they lack a native sendfile implementation. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The destination TCP socket +- file: The source file handle +- p: User data, the callback will receive this as it's second argument +- cb: The callback to be called when data is sent (if `progress_updates` is true) or the operation completes +- offset: Byte offset to start reading from the file +- nbytes: Total bytes to send (use SEND_ENTIRE_FILE for the whole file) +- progress_updates: If true, the callback fires multiple times to report progress, `sent == nbytes` means te operation completed +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the final callback is called +*/ +sendfile_poly :: #force_inline proc( + socket: TCP_Socket, + file: Handle, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + offset: int = 0, + nbytes: int = SEND_ENTIRE_FILE, + progress_updates := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_sendfile(socket, file, _poly_cb(C, T), offset, nbytes, progress_updates, timeout, l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Sends a file over a TCP socket. + +This uses high-performance zero-copy system calls where available. +Note: This is emulated on NetBSD and OpenBSD (stat -> mmap -> send) as they lack a native sendfile implementation. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The destination TCP socket +- file: The source file handle +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- cb: The callback to be called when data is sent (if `progress_updates` is true) or the operation completes +- offset: Byte offset to start reading from the file +- nbytes: Total bytes to send (use SEND_ENTIRE_FILE for the whole file) +- progress_updates: If true, the callback fires multiple times to report progress, `sent == nbytes` means te operation completed +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the final callback is called +*/ +sendfile_poly2 :: #force_inline proc( + socket: TCP_Socket, + file: Handle, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + offset: int = 0, + nbytes: int = SEND_ENTIRE_FILE, + progress_updates := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_sendfile(socket, file, _poly_cb2(C, T, T2), offset, nbytes, progress_updates, timeout, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Sends a file over a TCP socket. + +This uses high-performance zero-copy system calls where available. +Note: This is emulated on NetBSD and OpenBSD (stat -> mmap -> send) as they lack a native sendfile implementation. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- socket: The destination TCP socket +- file: The source file handle +- p: User data, the callback will receive this as it's second argument +- p2: User data, the callback will receive this as it's third argument +- p3: User data, the callback will receive this as it's fourth argument +- cb: The callback to be called when data is sent (if `progress_updates` is true) or the operation completes +- offset: Byte offset to start reading from the file +- nbytes: Total bytes to send (use SEND_ENTIRE_FILE for the whole file) +- progress_updates: If true, the callback fires multiple times to report progress, `sent == nbytes` means te operation completed +- timeout: Optional timeout for the operation +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the final callback is called +*/ +sendfile_poly3 :: #force_inline proc( + socket: TCP_Socket, + file: Handle, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + offset: int = 0, + nbytes: int = SEND_ENTIRE_FILE, + progress_updates := false, + timeout: time.Duration = NO_TIMEOUT, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_sendfile(socket, file, _poly_cb3(C, T, T2, T3), offset, nbytes, progress_updates, timeout, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +/* +File permission bit-set. + +This type represents POSIX-style file permissions, split into user, group, +and other categories, each with read, write, and execute flags. +*/ +Permissions :: distinct bit_set[Permission_Flag; u32] + +Permission_Flag :: enum u32 { + Execute_Other = 0, + Write_Other = 1, + Read_Other = 2, + + Execute_Group = 3, + Write_Group = 4, + Read_Group = 5, + + Execute_User = 6, + Write_User = 7, + Read_User = 8, +} + +// Convenience permission sets. +Permissions_Execute_All :: Permissions{.Execute_User, .Execute_Group, .Execute_Other} +Permissions_Write_All :: Permissions{.Write_User, .Write_Group, .Write_Other} +Permissions_Read_All :: Permissions{.Read_User, .Read_Group, .Read_Other} + +// Read and write permissions for user, group, and others. +Permissions_Read_Write_All :: Permissions_Read_All + Permissions_Write_All + +// Read, write, and execute permissions for user, group, and others. +Permissions_All :: Permissions_Read_All + Permissions_Write_All + Permissions_Execute_All + +// Default permissions used when creating a file (read and write for everyone). +Permissions_Default_File :: Permissions_Read_All + Permissions_Write_All + +// Default permissions used when creating a directory (read, write, and execute for everyone). +Permissions_Default_Directory :: Permissions_Read_All + Permissions_Write_All + Permissions_Execute_All + +File_Flags :: bit_set[File_Flag; int] + +File_Flag :: enum { + // Open for reading. + Read, + // Open for writing. + Write, + // Append writes to the end of the file. + Append, + // Create the file if it does not exist. + Create, + // Fail if the file already exists (used with Create). + Excl, + Sync, + // Truncate the file on open. + Trunc, +} + +Open :: struct { + // Base directory the path is relative to. + dir: Handle, + // Path to the file. + path: string, + // File open mode flags. + mode: File_Flags, + // Permissions used if the file is created. + perm: Permissions, + + // The opened file handle. + handle: Handle, + // An error, if it occurred. + err: FS_Error, + + // Implementation specifics, private. + _impl: _Open `fmt:"-"`, +} + +// Sentinel handle representing the current/present working directory. +CWD :: _CWD + +/* +Retrieves and preps an operation to open a file without executing it. + +Executing can then be done with the `exec` procedure. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- path: Path to the file, if not absolute: relative from `dir` +- cb: The callback to be called when the operation finishes, `Operation.open` will contain results +- mode: File open mode flags, defaults to read-only +- perm: Permissions to use when creating a file, defaults to read+write for everybody +- dir: Directory that `path` is relative from (if it is relative), defaults to the current working directory +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_open :: #force_inline proc( + path: string, + cb: Callback, + mode: File_Flags = {.Read}, + perm: Permissions = Permissions_Default_File, + dir: Handle = CWD, + l: ^Event_Loop = nil, +) -> ^Operation { + op := _prep(l, cb, .Open) + op.open.path = path + op.open.mode = mode + op.open.perm = perm + op.open.dir = dir + return op +} + +/* +Opens a file and associates it with the event loop. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `open_poly`, `open_poly2`, and `open_poly3`. + +Inputs: +- path: Path to the file, if not absolute: relative from `dir` +- cb: The callback to be called when the operation finishes, `Operation.open` will contain results +- mode: File open mode flags, defaults to read-only +- perm: Permissions to use when creating a file, defaults to read+write for everybody +- dir: Directory that `path` is relative from (if it is relative), defaults to the current working directory +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +open :: #force_inline proc( + path: string, + cb: Callback, + mode: File_Flags = {.Read}, + perm: Permissions = Permissions_Default_File, + dir: Handle = CWD, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_open(path, cb, mode, perm, dir, l) + exec(op) + return op +} + +/* +Opens a file and associates it with the event loop. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- path: Path to the file, if not absolute: relative from `dir` +- p: User data, the callback will receive this as its second argument +- cb: The callback to be called when the operation finishes, `Operation.open` will contain results +- mode: File open mode flags, defaults to read-only +- perm: Permissions to use when creating a file, defaults to read+write for everybody +- dir: Directory that `path` is relative from (if it is relative), defaults to the current working directory +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +open_poly :: #force_inline proc( + path: string, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + mode: File_Flags = {.Read}, + perm: Permissions = Permissions_Default_File, + dir: Handle = CWD, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_open(path, _poly_cb(C, T), mode, perm, dir, l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Opens a file and associates it with the event loop. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- path: Path to the file, if not absolute: relative from `dir` +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- cb: The callback to be called when the operation finishes, `Operation.open` will contain results +- mode: File open mode flags, defaults to read-only +- perm: Permissions to use when creating a file, defaults to read+write for everybody +- dir: Directory that `path` is relative from (if it is relative), defaults to the current working directory +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +open_poly2 :: #force_inline proc( + path: string, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + mode: File_Flags = {.Read}, + perm: Permissions = Permissions_Default_File, + dir: Handle = CWD, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_open(path, _poly_cb2(C, T, T2), mode, perm, dir, l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Asynchronously opens a file and associates it with the event loop. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- path: Path to the file, if not absolute: relative from `dir` +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- p3: User data, the callback will receive this as its fourth argument +- cb: The callback to be called when the operation finishes, `Operation.open` will contain results +- mode: File open mode flags, defaults to read-only +- perm: Permissions to use when creating a file, defaults to read+write for everybody +- dir: Directory that `path` is relative from (if it is relative), defaults to the current working directory +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +open_poly3 :: #force_inline proc( + path: string, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + mode: File_Flags = {.Read}, + perm: Permissions = Permissions_Default_File, + dir: Handle = CWD, + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_open(path, _poly_cb3(C, T, T2, T3), mode, perm, dir, l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +File_Type :: enum { + // File type could not be determined. + Undetermined, + // Regular file. + Regular, + // Directory. + Directory, + // Symbolic link. + Symlink, + // Pipe or socket. + Pipe_Or_Socket, + // Character or block device. + Device, +} + +Stat :: struct { + // Handle to stat. + handle: Handle, + + // The type of the file. + type: File_Type, + // Size of the file in bytes. + size: i64 `fmt:"M"`, + + // An error, if it occurred. + err: FS_Error, + + // Implementation specifics, private. + _impl: _Stat `fmt:"-"`, +} + +/* +Retrieves and preps an operation to stat a handle without executing it. + +Executing can then be done with the `exec` procedure. + +Any user data can be set on the returned operation's `user_data` field. + +Inputs: +- handle: Handle to retrieve stat +- cb: The callback to be called when the operation finishes, `Operation.stat` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +prep_stat :: #force_inline proc( + handle: Handle, + cb: Callback, + l: ^Event_Loop = nil, +) -> ^Operation { + op := _prep(l, cb, .Stat) + op.stat.handle = handle + return op +} + +/* +Stats a handle. + +Any user data can be set on the returned operation's `user_data` field. +Polymorphic variants for type safe user data are available under `stat_poly`, `stat_poly2`, and `stat_poly3`. + +Inputs: +- handle: Handle to retrieve status information for +- cb: The callback to be called when the operation finishes, `Operation.stat` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +stat :: #force_inline proc( + handle: Handle, + cb: Callback, + l: ^Event_Loop = nil, +) -> ^Operation { + op := prep_stat(handle, cb, l) + exec(op) + return op +} + +/* +Stats a handle. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to retrieve status information for +- p: User data, the callback will receive this as its second argument +- cb: The callback to be called when the operation finishes, `Operation.stat` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +stat_poly :: #force_inline proc( + handle: Handle, + p: $T, + cb: $C/proc(op: ^Operation, p: T), + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_stat(handle, _poly_cb(C, T), l) + _put_user_data(op, cb, p) + exec(op) + + return op +} + +/* +Stats a handle. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to retrieve status information for +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- cb: The callback to be called when the operation finishes, `Operation.stat` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +stat_poly2 :: #force_inline proc( + handle: Handle, + p: $T, p2: $T2, + cb: $C/proc(op: ^Operation, p: T, p2: T2), + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_stat(handle, _poly_cb2(C, T, T2), l) + _put_user_data2(op, cb, p, p2) + exec(op) + + return op +} + +/* +Stats a handle. + +This procedure uses polymorphism for type safe user data up to a certain size. + +Inputs: +- handle: Handle to retrieve status information for +- p: User data, the callback will receive this as its second argument +- p2: User data, the callback will receive this as its third argument +- p3: User data, the callback will receive this as its fourth argument +- cb: The callback to be called when the operation finishes, `Operation.stat` will contain results +- l: Event loop to associate the operation with, defaults to the current thread's loop + +Returns: A non-nil pointer to the operation, alive until the callback is called +*/ +stat_poly3 :: #force_inline proc( + handle: Handle, + p: $T, p2: $T2, p3: $T3, + cb: $C/proc(op: ^Operation, p: T, p2: T2, p3: T3), + l: ^Event_Loop = nil, +) -> ^Operation where size_of(T) + size_of(T2) + size_of(T3) <= size_of(rawptr) * MAX_USER_ARGUMENTS { + + op := prep_stat(handle, _poly_cb3(C, T, T2, T3), l) + _put_user_data3(op, cb, p, p2, p3) + exec(op) + + return op +} + +_prep :: proc(l: ^Event_Loop, cb: Callback, type: Operation_Type) -> ^Operation { + assert(cb != nil) + assert(type != .None) + l := l + if l == nil { l = _current_thread_event_loop() } + operation := pool.get(&l.operation_pool) + operation.l = l + operation.type = type + operation.cb = cb + return operation +} + +_poly_cb :: #force_inline proc($C: typeid, $T: typeid) -> proc(^Operation) { + return proc(op: ^Operation) { + ptr := uintptr(&op.user_data) + cb := intrinsics.unaligned_load((^C)(rawptr(ptr))) + p := intrinsics.unaligned_load((^T)(rawptr(ptr + size_of(C)))) + cb(op, p) + } +} + +_poly_cb2 :: #force_inline proc($C: typeid, $T: typeid, $T2: typeid) -> proc(^Operation) { + return proc(op: ^Operation) { + ptr := uintptr(&op.user_data) + cb := intrinsics.unaligned_load((^C) (rawptr(ptr))) + p := intrinsics.unaligned_load((^T) (rawptr(ptr + size_of(C)))) + p2 := intrinsics.unaligned_load((^T2)(rawptr(ptr + size_of(C) + size_of(T)))) + cb(op, p, p2) + } +} + +_poly_cb3 :: #force_inline proc($C: typeid, $T: typeid, $T2: typeid, $T3: typeid) -> proc(^Operation) { + return proc(op: ^Operation) { + ptr := uintptr(&op.user_data) + cb := intrinsics.unaligned_load((^C) (rawptr(ptr))) + p := intrinsics.unaligned_load((^T) (rawptr(ptr + size_of(C)))) + p2 := intrinsics.unaligned_load((^T2)(rawptr(ptr + size_of(C) + size_of(T)))) + p3 := intrinsics.unaligned_load((^T3)(rawptr(ptr + size_of(C) + size_of(T) + size_of(T2)))) + cb(op, p, p2, p3) + } +} + +_put_user_data :: #force_inline proc(op: ^Operation, cb: $C, p: $T) { + ptr := uintptr(&op.user_data) + intrinsics.unaligned_store((^C)(rawptr(ptr)), cb) + intrinsics.unaligned_store((^T)(rawptr(ptr + size_of(cb))), p) +} + +_put_user_data2 :: #force_inline proc(op: ^Operation, cb: $C, p: $T, p2: $T2) { + ptr := uintptr(&op.user_data) + intrinsics.unaligned_store((^C) (rawptr(ptr)), cb) + intrinsics.unaligned_store((^T) (rawptr(ptr + size_of(cb))), p) + intrinsics.unaligned_store((^T2)(rawptr(ptr + size_of(cb) + size_of(p))), p2) +} + +_put_user_data3 :: #force_inline proc(op: ^Operation, cb: $C, p: $T, p2: $T2, p3: $T3) { + ptr := uintptr(&op.user_data) + intrinsics.unaligned_store((^C) (rawptr(ptr)), cb) + intrinsics.unaligned_store((^T) (rawptr(ptr + size_of(cb))), p) + intrinsics.unaligned_store((^T2)(rawptr(ptr + size_of(cb) + size_of(p))), p2) + intrinsics.unaligned_store((^T3)(rawptr(ptr + size_of(cb) + size_of(p) + size_of(p2))), p3) +} diff --git a/core/net/addr.odin b/core/net/addr.odin index 6e2881ac8..a3fa02cce 100644 --- a/core/net/addr.odin +++ b/core/net/addr.odin @@ -1,4 +1,3 @@ -#+build windows, linux, darwin, freebsd package net /* @@ -22,7 +21,6 @@ package net import "core:strconv" import "core:strings" -import "core:fmt" /* Expects an IPv4 address with no leading or trailing whitespace: @@ -473,13 +471,20 @@ join_port :: proc(address_or_host: string, port: int, allocator := context.alloc addr := parse_address(addr_or_host) if addr == nil { // hostname - fmt.sbprintf(&b, "%v:%v", addr_or_host, port) + strings.write_string(&b, addr_or_host) + strings.write_string(&b, ":") + strings.write_int(&b, port) } else { switch _ in addr { case IP4_Address: - fmt.sbprintf(&b, "%v:%v", address_to_string(addr), port) + strings.write_string(&b, address_to_string(addr)) + strings.write_string(&b, ":") + strings.write_int(&b, port) case IP6_Address: - fmt.sbprintf(&b, "[%v]:%v", address_to_string(addr), port) + strings.write_string(&b, "[") + strings.write_string(&b, address_to_string(addr)) + strings.write_string(&b, "]:") + strings.write_int(&b, port) } } return strings.to_string(b) @@ -509,7 +514,13 @@ address_to_string :: proc(addr: Address, allocator := context.temp_allocator) -> b := strings.builder_make(allocator) switch v in addr { case IP4_Address: - fmt.sbprintf(&b, "%v.%v.%v.%v", v[0], v[1], v[2], v[3]) + strings.write_uint(&b, uint(v[0])) + strings.write_byte(&b, '.') + strings.write_uint(&b, uint(v[1])) + strings.write_byte(&b, '.') + strings.write_uint(&b, uint(v[2])) + strings.write_byte(&b, '.') + strings.write_uint(&b, uint(v[3])) case IP6_Address: // First find the longest run of zeroes. Zero_Run :: struct { @@ -563,25 +574,33 @@ address_to_string :: proc(addr: Address, allocator := context.temp_allocator) -> for val, i in v { if best.start == i || best.end == i { // For the left and right side of the best zero run, print a `:`. - fmt.sbprint(&b, ":") + strings.write_string(&b, ":") } else if i < best.start { /* If we haven't made it to the best run yet, print the digit. Make sure we only print a `:` after the digit if it's not immediately followed by the run's own leftmost `:`. */ - fmt.sbprintf(&b, "%x", val) + + buf: [32]byte + str := strconv.write_bits(buf[:], u64(val), 16, false, size_of(val), strconv.digits, {}) + strings.write_string(&b, str) + if i < best.start - 1 { - fmt.sbprintf(&b, ":") + strings.write_string(&b, ":") } } else if i > best.end { /* If there are any digits after the zero run, print them. But don't print the `:` at the end of the IP number. */ - fmt.sbprintf(&b, "%x", val) + + buf: [32]byte + str := strconv.write_bits(buf[:], u64(val), 16, false, size_of(val), strconv.digits, {}) + strings.write_string(&b, str) + if i != 7 { - fmt.sbprintf(&b, ":") + strings.write_string(&b, ":") } } } @@ -598,8 +617,14 @@ endpoint_to_string :: proc(ep: Endpoint, allocator := context.temp_allocator) -> s := address_to_string(ep.address, context.temp_allocator) b := strings.builder_make(allocator) switch a in ep.address { - case IP4_Address: fmt.sbprintf(&b, "%v:%v", s, ep.port) - case IP6_Address: fmt.sbprintf(&b, "[%v]:%v", s, ep.port) + case IP4_Address: + strings.write_string(&b, s) + strings.write_int(&b, ep.port) + case IP6_Address: + strings.write_string(&b, "[") + strings.write_string(&b, s) + strings.write_string(&b, "]:") + strings.write_int(&b, ep.port) } return strings.to_string(b) } diff --git a/core/net/common.odin b/core/net/common.odin index 70523050f..2758a7359 100644 --- a/core/net/common.odin +++ b/core/net/common.odin @@ -1,4 +1,3 @@ -#+build windows, linux, darwin, freebsd package net /* @@ -91,6 +90,7 @@ Parse_Endpoint_Error :: enum u32 { Resolve_Error :: enum u32 { None = 0, Unable_To_Resolve = 1, + Allocation_Failure, } DNS_Error :: enum u32 { @@ -144,11 +144,11 @@ Address :: union {IP4_Address, IP6_Address} IP4_Loopback :: IP4_Address{127, 0, 0, 1} IP6_Loopback :: IP6_Address{0, 0, 0, 0, 0, 0, 0, 1} -IP4_Any := IP4_Address{} -IP6_Any := IP6_Address{} +IP4_Any :: IP4_Address{} +IP6_Any :: IP6_Address{} -IP4_mDNS_Broadcast := Endpoint{address=IP4_Address{224, 0, 0, 251}, port=5353} -IP6_mDNS_Broadcast := Endpoint{address=IP6_Address{65282, 0, 0, 0, 0, 0, 0, 251}, port = 5353} +IP4_mDNS_Broadcast :: Endpoint{address=IP4_Address{224, 0, 0, 251}, port=5353} +IP6_mDNS_Broadcast :: Endpoint{address=IP6_Address{65282, 0, 0, 0, 0, 0, 0, 251}, port = 5353} Endpoint :: struct { address: Address, diff --git a/core/net/dns.odin b/core/net/dns.odin index 540991fe7..983f82681 100644 --- a/core/net/dns.odin +++ b/core/net/dns.odin @@ -1,4 +1,3 @@ -#+build windows, linux, darwin, freebsd package net /* @@ -22,13 +21,18 @@ package net Haesbaert: Security fixes */ -@(require) import "base:runtime" +@(require) +import "base:runtime" + +import "core:bufio" +import "core:io" +import "core:math/rand" import "core:mem" import "core:strings" import "core:time" -import "core:os" -import "core:math/rand" -@(require) import "core:sync" + +@(require) +import "core:sync" dns_config_initialized: sync.Once when ODIN_OS == .Windows { @@ -42,20 +46,12 @@ when ODIN_OS == .Windows { hosts_file = "/etc/hosts", } } else { - #panic("Please add a configuration for this OS.") + DEFAULT_DNS_CONFIGURATION :: DNS_Configuration{} } -/* - Replaces environment placeholders in `dns_configuration`. Only necessary on Windows. - Is automatically called, once, by `get_dns_records_*`. -*/ -@(private) init_dns_configuration :: proc() { when ODIN_OS == .Windows { - runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() - val := os.replace_environment_placeholders(dns_configuration.hosts_file, context.temp_allocator) - copy(dns_configuration.hosts_file_buf[:], val) - dns_configuration.hosts_file = string(dns_configuration.hosts_file_buf[:len(val)]) + _init_dns_configuration() } } @@ -178,9 +174,7 @@ resolve_ip6 :: proc(hostname_and_maybe_port: string) -> (ep6: Endpoint, err: Net See `destroy_records`. */ get_dns_records_from_os :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { - when ODIN_OS == .Windows { - sync.once_do(&dns_config_initialized, init_dns_configuration) - } + init_dns_configuration() return _get_dns_records_os(hostname, type, allocator) } @@ -196,51 +190,14 @@ get_dns_records_from_os :: proc(hostname: string, type: DNS_Record_Type, allocat See `destroy_records`. */ get_dns_records_from_nameservers :: proc(hostname: string, type: DNS_Record_Type, name_servers: []Endpoint, host_overrides: []DNS_Record, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { - when ODIN_OS == .Windows { - sync.once_do(&dns_config_initialized, init_dns_configuration) - } + init_dns_configuration() context.allocator = allocator - if type != .SRV { - // NOTE(tetra): 'hostname' can contain underscores when querying SRV records - ok := validate_hostname(hostname) - if !ok { - return nil, .Invalid_Hostname_Error - } - } - - hdr := DNS_Header{ - id = u16be(rand.uint32()), - is_response = false, - opcode = 0, - is_authoritative = false, - is_truncated = false, - is_recursion_desired = true, - is_recursion_available = false, - response_code = DNS_Response_Code.No_Error, - } + id := u16be(rand.uint32()) + dns_packet_buf: [DNS_PACKET_MIN_LEN]byte = --- + dns_packet := make_dns_packet(dns_packet_buf[:], id, hostname, type) or_return - id, bits := pack_dns_header(hdr) - dns_hdr := [6]u16be{} - dns_hdr[0] = id - dns_hdr[1] = bits - dns_hdr[2] = 1 - - dns_query := [2]u16be{ u16be(type), 1 } - - output := [(size_of(u16be) * 6) + NAME_MAX + (size_of(u16be) * 2)]u8{} - b := strings.builder_from_slice(output[:]) - - strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_hdr[:])) - ok := encode_hostname(&b, hostname) - if !ok { - return nil, .Invalid_Hostname_Error - } - strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_query[:])) - - dns_packet := output[:strings.builder_len(b)] - - dns_response_buf := [4096]u8{} + dns_response_buf: [4096]u8 = --- dns_response: []u8 for name_server in name_servers { conn, sock_err := make_unbound_udp_socket(family_from_endpoint(name_server)) @@ -283,6 +240,42 @@ get_dns_records_from_nameservers :: proc(hostname: string, type: DNS_Record_Type return } +DNS_PACKET_MIN_LEN :: (size_of(u16be) * 6) + NAME_MAX + (size_of(u16be) * 2) + +make_dns_packet :: proc(buf: []byte, id: u16be, hostname: string, type: DNS_Record_Type) -> (packet: []byte, err: DNS_Error) { + assert(len(buf) >= DNS_PACKET_MIN_LEN) + + hdr := DNS_Header{ + id = id, + is_response = false, + opcode = 0, + is_authoritative = false, + is_truncated = false, + is_recursion_desired = true, + is_recursion_available = false, + response_code = DNS_Response_Code.No_Error, + } + + _, bits := pack_dns_header(hdr) + dns_hdr := [6]u16be{} + dns_hdr[0] = id + dns_hdr[1] = bits + dns_hdr[2] = 1 + + dns_query := [2]u16be{ u16be(type), 1 } + + b := strings.builder_from_slice(buf[:]) + + strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_hdr[:])) + ok := encode_hostname(&b, hostname) + if !ok { + return nil, .Invalid_Hostname_Error + } + strings.write_bytes(&b, mem.slice_data_cast([]u8, dns_query[:])) + + return buf[:strings.builder_len(b)], nil +} + // `records` slice is also destroyed. destroy_dns_records :: proc(records: []DNS_Record, allocator := context.allocator) { context.allocator = allocator @@ -364,13 +357,8 @@ unpack_dns_header :: proc(id: u16be, bits: u16be) -> (hdr: DNS_Header) { return hdr } -load_resolv_conf :: proc(resolv_conf_path: string, allocator := context.allocator) -> (name_servers: []Endpoint, ok: bool) { - context.allocator = allocator - - res := os.read_entire_file_from_filename(resolv_conf_path) or_return - defer delete(res) - resolv_str := string(res) - +parse_resolv_conf :: proc(resolv_str: string, allocator := context.allocator) -> (name_servers: []Endpoint) { + resolv_str := resolv_str id_str := "nameserver" id_len := len(id_str) @@ -401,41 +389,51 @@ load_resolv_conf :: proc(resolv_conf_path: string, allocator := context.allocato append(&_name_servers, endpoint) } - return _name_servers[:], true + return _name_servers[:] } -load_hosts :: proc(hosts_file_path: string, allocator := context.allocator) -> (hosts: []DNS_Host_Entry, ok: bool) { - context.allocator = allocator +parse_hosts :: proc(stream: io.Stream, allocator := context.allocator) -> (hosts: []DNS_Host_Entry, ok: bool) { + s := bufio.scanner_init(&{}, stream, allocator) + defer bufio.scanner_destroy(s) - res := os.read_entire_file_from_filename(hosts_file_path, allocator) or_return - defer delete(res) + resize(&s.buf, 256) - _hosts := make([dynamic]DNS_Host_Entry, 0, allocator) - hosts_str := string(res) - for line in strings.split_lines_iterator(&hosts_str) { - if len(line) == 0 || line[0] == '#' { - continue + _hosts: [dynamic]DNS_Host_Entry + _hosts.allocator = allocator + defer if !ok { + for host in _hosts { + delete(host.name, allocator) } + delete(_hosts) + } - splits := strings.fields(line) - defer delete(splits) + for bufio.scanner_scan(s) { + line := bufio.scanner_text(s) - (len(splits) >= 2) or_continue + line, _, _ = strings.partition(line, "#") + (len(line) > 0) or_continue + + ip_str := strings.fields_iterator(&line) or_continue - ip_str := splits[0] addr := parse_address(ip_str) - if addr == nil { - continue - } + (addr != nil) or_continue - for hostname in splits[1:] { - if len(hostname) != 0 { - append(&_hosts, DNS_Host_Entry{hostname, addr}) - } + for hostname in strings.fields_iterator(&line) { + (len(hostname) > 0) or_continue + + clone, alloc_err := strings.clone(hostname, allocator) + if alloc_err != nil { return } + + _, alloc_err = append(&_hosts, DNS_Host_Entry{clone, addr}) + if alloc_err != nil { return } } } - return _hosts[:], true + if bufio.scanner_error(s) != nil { return } + + hosts = _hosts[:] + ok = true + return } // www.google.com -> 3www6google3com0 @@ -594,7 +592,7 @@ decode_hostname :: proc(packet: []u8, start_idx: int, allocator := context.alloc // Uses RFC 952 & RFC 1123 validate_hostname :: proc(hostname: string) -> (ok: bool) { - if len(hostname) > 255 || len(hostname) == 0 { + if len(hostname) > NAME_MAX || len(hostname) == 0 { return } @@ -604,7 +602,7 @@ validate_hostname :: proc(hostname: string) -> (ok: bool) { _hostname := hostname for label in strings.split_iterator(&_hostname, ".") { - if len(label) > 63 || len(label) == 0 { + if len(label) > LABEL_MAX || len(label) == 0 { return } @@ -868,4 +866,4 @@ parse_response :: proc(response: []u8, filter: DNS_Record_Type = nil, allocator xid = hdr.id return _records[:], xid, true -}
\ No newline at end of file +} diff --git a/core/net/dns_os.odin b/core/net/dns_os.odin new file mode 100644 index 000000000..19db0097a --- /dev/null +++ b/core/net/dns_os.odin @@ -0,0 +1,24 @@ +#+build darwin, freebsd, openbsd, netbsd, linux, windows, wasi +#+private +package net + +import "core:os" + +load_resolv_conf :: proc(resolv_conf_path: string, allocator := context.allocator) -> (name_servers: []Endpoint, ok: bool) { + context.allocator = allocator + + res := os.read_entire_file_from_filename(resolv_conf_path) or_return + defer delete(res) + resolv_str := string(res) + + return parse_resolv_conf(resolv_str), true +} + +load_hosts :: proc(hosts_file_path: string, allocator := context.allocator) -> (hosts: []DNS_Host_Entry, ok: bool) { + hosts_file, err := os.open(hosts_file_path) + if err != nil { return } + defer os.close(hosts_file) + + return parse_hosts(os.stream_from_handle(hosts_file), allocator) +} + diff --git a/core/net/dns_others.odin b/core/net/dns_others.odin new file mode 100644 index 000000000..842e833aa --- /dev/null +++ b/core/net/dns_others.odin @@ -0,0 +1,12 @@ +#+build !windows +#+build !linux +#+build !darwin +#+build !freebsd +#+build !netbsd +#+build !openbsd +package net + +@(private) +_get_dns_records_os :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { + return +} diff --git a/core/net/dns_unix.odin b/core/net/dns_unix.odin index fbc1909cd..be95b8341 100644 --- a/core/net/dns_unix.odin +++ b/core/net/dns_unix.odin @@ -1,4 +1,4 @@ -#+build linux, darwin, freebsd +#+build linux, darwin, freebsd, openbsd, netbsd package net /* Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures. @@ -42,14 +42,19 @@ _get_dns_records_os :: proc(hostname: string, type: DNS_Record_Type, allocator : } hosts, hosts_ok := load_hosts(dns_configuration.hosts_file) - defer delete(hosts) if !hosts_ok { return nil, .Invalid_Hosts_Config_Error } + defer { + for h in hosts { + delete(h.name) + } + delete(hosts) + } host_overrides := make([dynamic]DNS_Record) for host in hosts { - if strings.compare(host.name, hostname) != 0 { + if host.name != hostname { continue } @@ -79,4 +84,4 @@ _get_dns_records_os :: proc(hostname: string, type: DNS_Record_Type, allocator : } return get_dns_records_from_nameservers(hostname, type, name_servers, host_overrides[:]) -}
\ No newline at end of file +} diff --git a/core/net/dns_windows.odin b/core/net/dns_windows.odin index b1e7da97d..393df5fa7 100644 --- a/core/net/dns_windows.odin +++ b/core/net/dns_windows.odin @@ -20,11 +20,29 @@ package net Feoramund: FreeBSD platform code */ -import "core:strings" +import "base:runtime" + import "core:mem" +import "core:os" +import "core:strings" +import "core:sync" import win "core:sys/windows" +/* + Replaces environment placeholders in `dns_configuration`. Only necessary on Windows. + Is automatically called, once, by `get_dns_records_*`. +*/ +@(private) +_init_dns_configuration :: proc() { + sync.once_do(&dns_config_initialized, proc() { + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + val := os.replace_environment_placeholders(dns_configuration.hosts_file, context.temp_allocator) + copy(dns_configuration.hosts_file_buf[:], val) + dns_configuration.hosts_file = string(dns_configuration.hosts_file_buf[:len(val)]) + }) +} + @(private) _get_dns_records_os :: proc(hostname: string, type: DNS_Record_Type, allocator := context.allocator) -> (records: []DNS_Record, err: DNS_Error) { context.allocator = allocator @@ -171,4 +189,4 @@ _get_dns_records_os :: proc(hostname: string, type: DNS_Record_Type, allocator : records = recs[:] return -}
\ No newline at end of file +} diff --git a/core/net/errors.odin b/core/net/errors.odin index de53640fc..28153375c 100644 --- a/core/net/errors.odin +++ b/core/net/errors.odin @@ -139,6 +139,11 @@ Accept_Error :: enum i32 { Unknown, } +Recv_Error :: union #shared_nil { + TCP_Recv_Error, + UDP_Recv_Error, +} + TCP_Recv_Error :: enum i32 { None, // No network connection, or the network stack is not initialized. @@ -187,6 +192,11 @@ UDP_Recv_Error :: enum i32 { Unknown, } +Send_Error :: union #shared_nil { + TCP_Send_Error, + UDP_Send_Error, +} + TCP_Send_Error :: enum i32 { None, // No network connection, or the network stack is not initialized. diff --git a/core/net/errors_others.odin b/core/net/errors_others.odin index b80ead79c..3a752d58e 100644 --- a/core/net/errors_others.odin +++ b/core/net/errors_others.odin @@ -2,6 +2,8 @@ #+build !linux #+build !freebsd #+build !windows +#+build !netbsd +#+build !openbsd package net @(private="file", thread_local) @@ -18,10 +20,3 @@ _last_platform_error_string :: proc() -> string { _set_last_platform_error :: proc(err: i32) { _last_error = err } - -Parse_Endpoint_Error :: enum u32 { - None = 0, - Bad_Port = 1, - Bad_Address, - Bad_Hostname, -}
\ No newline at end of file diff --git a/core/net/errors_darwin.odin b/core/net/errors_posix.odin index a35e96bc0..b59cbc30b 100644 --- a/core/net/errors_darwin.odin +++ b/core/net/errors_posix.odin @@ -1,4 +1,4 @@ -#+build darwin +#+build darwin, netbsd, openbsd package net /* diff --git a/core/net/errors_windows.odin b/core/net/errors_windows.odin index 83c45ee7f..6d3724c82 100644 --- a/core/net/errors_windows.odin +++ b/core/net/errors_windows.odin @@ -63,7 +63,7 @@ _dial_error :: proc() -> Dial_Error { return .Already_Connecting case .WSAEADDRNOTAVAIL, .WSAEAFNOSUPPORT, .WSAEFAULT, .WSAENOTSOCK, .WSAEINPROGRESS, .WSAEINVAL: return .Invalid_Argument - case .WSAECONNREFUSED: + case .WSAECONNREFUSED, .CONNECTION_REFUSED: return .Refused case .WSAEISCONN: return .Already_Connected @@ -122,7 +122,7 @@ _accept_error :: proc() -> Accept_Error { return .Aborted case .WSAEFAULT, .WSAEINPROGRESS, .WSAENOTSOCK: return .Invalid_Argument - case .WSAEINTR: + case .WSAEINTR, .OPERATION_ABORTED: return .Interrupted case .WSAEINVAL: return .Not_Listening diff --git a/core/net/interface_others.odin b/core/net/interface_others.odin new file mode 100644 index 000000000..9a8a141df --- /dev/null +++ b/core/net/interface_others.odin @@ -0,0 +1,11 @@ +#+build !darwin +#+build !linux +#+build !freebsd +#+build !windows +#+build !netbsd +#+build !openbsd +package net + +_enumerate_interfaces :: proc(allocator := context.allocator) -> (interfaces: []Network_Interface, err: Interfaces_Error) { + return +} diff --git a/core/net/interface_darwin.odin b/core/net/interface_posix.odin index f18cff995..202951b29 100644 --- a/core/net/interface_darwin.odin +++ b/core/net/interface_posix.odin @@ -1,4 +1,4 @@ -#+build darwin +#+build darwin, openbsd, netbsd package net /* @@ -117,32 +117,47 @@ IF_Flag :: enum u32 { BROADCAST, DEBUG, LOOPBACK, - POINTTOPOINT, - NOTRAILERS, - RUNNING, - NOARP, - PROMISC, - ALLMULTI, - OACTIVE, - SIMPLEX, - LINK0, - LINK1, - LINK2, - MULTICAST, + // NOTE: different order on other BSDs but we don't even need these. + // POINTTOPOINT, + // NOTRAILERS, + // RUNNING, + // NOARP, + // PROMISC, + // ALLMULTI, + // OACTIVE, + // SIMPLEX, + // LINK0, + // LINK1, + // LINK2, + // MULTICAST, } @(private) IF_Flags :: bit_set[IF_Flag; u32] -@(private) -ifaddrs :: struct { - next: ^ifaddrs, - name: cstring, - flags: IF_Flags, - addr: ^posix.sockaddr, - netmask: ^posix.sockaddr, - dstaddr: ^posix.sockaddr, - data: rawptr, +when ODIN_OS == .Darwin || ODIN_OS == .OpenBSD { + @(private) + ifaddrs :: struct { + next: ^ifaddrs, + name: cstring, + flags: IF_Flags, + addr: ^posix.sockaddr, + netmask: ^posix.sockaddr, + dstaddr: ^posix.sockaddr, + data: rawptr, + } +} else when ODIN_OS == .NetBSD { + @(private) + ifaddrs :: struct { + next: ^ifaddrs, + name: cstring, + flags: IF_Flags, + addr: ^posix.sockaddr, + netmask: ^posix.sockaddr, + dstaddr: ^posix.sockaddr, + data: rawptr, + addrflags: u32, + } } @(private) diff --git a/core/net/socket.odin b/core/net/socket.odin index edb47cd0b..e2f96e2f3 100644 --- a/core/net/socket.odin +++ b/core/net/socket.odin @@ -1,4 +1,3 @@ -#+build windows, linux, darwin, freebsd package net /* @@ -20,6 +19,35 @@ package net Feoramund: FreeBSD platform code */ +Socket_Option :: enum i32 { + Broadcast = i32(_SOCKET_OPTION_BROADCAST), + Reuse_Address = i32(_SOCKET_OPTION_REUSE_ADDRESS), + Keep_Alive = i32(_SOCKET_OPTION_KEEP_ALIVE), + Out_Of_Bounds_Data_Inline = i32(_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE), + Linger = i32(_SOCKET_OPTION_LINGER), + Receive_Buffer_Size = i32(_SOCKET_OPTION_RECEIVE_BUFFER_SIZE), + Send_Buffer_Size = i32(_SOCKET_OPTION_SEND_BUFFER_SIZE), + Receive_Timeout = i32(_SOCKET_OPTION_RECEIVE_TIMEOUT), + Send_Timeout = i32(_SOCKET_OPTION_SEND_TIMEOUT), + + TCP_Nodelay = i32(_SOCKET_OPTION_TCP_NODELAY), + + Use_Loopback = i32(_SOCKET_OPTION_USE_LOOPBACK), + Reuse_Port = i32(_SOCKET_OPTION_REUSE_PORT), + No_SIGPIPE_From_EPIPE = i32(_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE), + Reuse_Port_Load_Balancing = i32(_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING), + + Exclusive_Addr_Use = i32(_SOCKET_OPTION_EXCLUSIVE_ADDR_USE), + Conditional_Accept = i32(_SOCKET_OPTION_CONDITIONAL_ACCEPT), + Dont_Linger = i32(_SOCKET_OPTION_DONT_LINGER), +} + +Shutdown_Manner :: enum i32 { + Receive = i32(_SHUTDOWN_MANNER_RECEIVE), + Send = i32(_SHUTDOWN_MANNER_SEND), + Both = i32(_SHUTDOWN_MANNER_BOTH), +} + any_socket_to_socket :: proc "contextless" (socket: Any_Socket) -> Socket { switch s in socket { case TCP_Socket: return Socket(s) diff --git a/core/net/socket_freebsd.odin b/core/net/socket_freebsd.odin index fa20742cb..bd600fd99 100644 --- a/core/net/socket_freebsd.odin +++ b/core/net/socket_freebsd.odin @@ -20,45 +20,35 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" import "core:sys/freebsd" import "core:time" Fd :: freebsd.Fd -Socket_Option :: enum c.int { - // TODO: Test and implement more socket options. - // DEBUG - Reuse_Address = cast(c.int)freebsd.Socket_Option.REUSEADDR, - Keep_Alive = cast(c.int)freebsd.Socket_Option.KEEPALIVE, - // DONTROUTE - Broadcast = cast(c.int)freebsd.Socket_Option.BROADCAST, - Use_Loopback = cast(c.int)freebsd.Socket_Option.USELOOPBACK, - Linger = cast(c.int)freebsd.Socket_Option.LINGER, - Out_Of_Bounds_Data_Inline = cast(c.int)freebsd.Socket_Option.OOBINLINE, - Reuse_Port = cast(c.int)freebsd.Socket_Option.REUSEPORT, - // TIMESTAMP - No_SIGPIPE_From_EPIPE = cast(c.int)freebsd.Socket_Option.NOSIGPIPE, - // ACCEPTFILTER - // BINTIME - // NO_OFFLOAD - // NO_DDP - Reuse_Port_Load_Balancing = cast(c.int)freebsd.Socket_Option.REUSEPORT_LB, - // RERROR - - Send_Buffer_Size = cast(c.int)freebsd.Socket_Option.SNDBUF, - Receive_Buffer_Size = cast(c.int)freebsd.Socket_Option.RCVBUF, - // SNDLOWAT - // RCVLOWAT - Send_Timeout = cast(c.int)freebsd.Socket_Option.SNDTIMEO, - Receive_Timeout = cast(c.int)freebsd.Socket_Option.RCVTIMEO, -} +_SOCKET_OPTION_BROADCAST :: freebsd.Socket_Option.BROADCAST +_SOCKET_OPTION_REUSE_ADDRESS :: freebsd.Socket_Option.REUSEADDR +_SOCKET_OPTION_KEEP_ALIVE :: freebsd.Socket_Option.KEEPALIVE +_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE :: freebsd.Socket_Option.OOBINLINE +_SOCKET_OPTION_LINGER :: freebsd.Socket_Option.LINGER +_SOCKET_OPTION_RECEIVE_BUFFER_SIZE :: freebsd.Socket_Option.RCVBUF +_SOCKET_OPTION_SEND_BUFFER_SIZE :: freebsd.Socket_Option.SNDBUF +_SOCKET_OPTION_RECEIVE_TIMEOUT :: freebsd.Socket_Option.RCVTIMEO +_SOCKET_OPTION_SEND_TIMEOUT :: freebsd.Socket_Option.SNDTIMEO -Shutdown_Manner :: enum c.int { - Receive = cast(c.int)freebsd.Shutdown_Method.RD, - Send = cast(c.int)freebsd.Shutdown_Method.WR, - Both = cast(c.int)freebsd.Shutdown_Method.RDWR, -} +_SOCKET_OPTION_TCP_NODELAY :: -1 + +_SOCKET_OPTION_USE_LOOPBACK :: freebsd.Socket_Option.USELOOPBACK +_SOCKET_OPTION_REUSE_PORT :: freebsd.Socket_Option.REUSEPORT +_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE :: freebsd.Socket_Option.NOSIGPIPE +_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: freebsd.Socket_Option.REUSEPORT_LB + +_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: -1 +_SOCKET_OPTION_CONDITIONAL_ACCEPT :: -1 +_SOCKET_OPTION_DONT_LINGER :: -1 + +_SHUTDOWN_MANNER_RECEIVE :: freebsd.Shutdown_Method.RD +_SHUTDOWN_MANNER_SEND :: freebsd.Shutdown_Method.WR +_SHUTDOWN_MANNER_BOTH :: freebsd.Shutdown_Method.RDWR @(private) _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { @@ -272,7 +262,7 @@ _set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc : ptr: rawptr len: freebsd.socklen_t - switch option { + #partial switch option { case .Reuse_Address, .Keep_Alive, @@ -344,7 +334,7 @@ _set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc : ptr = &int_value len = size_of(int_value) case: - unimplemented("set_option() option not yet implemented", loc) + return .Invalid_Option } real_socket := any_socket_to_socket(socket) diff --git a/core/net/socket_linux.odin b/core/net/socket_linux.odin index 9719ff61b..8348ce114 100644 --- a/core/net/socket_linux.odin +++ b/core/net/socket_linux.odin @@ -21,28 +21,33 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" import "core:time" import "core:sys/linux" -Socket_Option :: enum c.int { - Reuse_Address = c.int(linux.Socket_Option.REUSEADDR), - Keep_Alive = c.int(linux.Socket_Option.KEEPALIVE), - Out_Of_Bounds_Data_Inline = c.int(linux.Socket_Option.OOBINLINE), - TCP_Nodelay = c.int(linux.Socket_TCP_Option.NODELAY), - Linger = c.int(linux.Socket_Option.LINGER), - Receive_Buffer_Size = c.int(linux.Socket_Option.RCVBUF), - Send_Buffer_Size = c.int(linux.Socket_Option.SNDBUF), - Receive_Timeout = c.int(linux.Socket_Option.RCVTIMEO), - Send_Timeout = c.int(linux.Socket_Option.SNDTIMEO), - Broadcast = c.int(linux.Socket_Option.BROADCAST), -} +_SOCKET_OPTION_BROADCAST :: linux.Socket_Option.BROADCAST +_SOCKET_OPTION_REUSE_ADDRESS :: linux.Socket_Option.REUSEADDR +_SOCKET_OPTION_KEEP_ALIVE :: linux.Socket_Option.KEEPALIVE +_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE :: linux.Socket_Option.OOBINLINE +_SOCKET_OPTION_LINGER :: linux.Socket_Option.LINGER +_SOCKET_OPTION_RECEIVE_BUFFER_SIZE :: linux.Socket_Option.RCVBUF +_SOCKET_OPTION_SEND_BUFFER_SIZE :: linux.Socket_Option.SNDBUF +_SOCKET_OPTION_RECEIVE_TIMEOUT :: linux.Socket_Option.RCVTIMEO +_SOCKET_OPTION_SEND_TIMEOUT :: linux.Socket_Option.SNDTIMEO -Shutdown_Manner :: enum c.int { - Receive = c.int(linux.Shutdown_How.RD), - Send = c.int(linux.Shutdown_How.WR), - Both = c.int(linux.Shutdown_How.RDWR), -} +_SOCKET_OPTION_TCP_NODELAY :: linux.Socket_TCP_Option.NODELAY + +_SOCKET_OPTION_USE_LOOPBACK :: -1 +_SOCKET_OPTION_REUSE_PORT :: -1 +_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE :: -1 +_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: -1 + +_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: -1 +_SOCKET_OPTION_CONDITIONAL_ACCEPT :: -1 +_SOCKET_OPTION_DONT_LINGER :: -1 + +_SHUTDOWN_MANNER_RECEIVE :: linux.Shutdown_How.RD +_SHUTDOWN_MANNER_SEND :: linux.Shutdown_How.WR +_SHUTDOWN_MANNER_BOTH :: linux.Shutdown_How.RDWR // Wrappers and unwrappers for system-native types @@ -347,7 +352,7 @@ _set_option :: proc(sock: Any_Socket, option: Socket_Option, value: any, loc := int_value: i32 timeval_value: linux.Time_Val errno: linux.Errno - switch option { + #partial switch option { case .Reuse_Address, .Keep_Alive, @@ -400,10 +405,14 @@ _set_option :: proc(sock: Any_Socket, option: Socket_Option, value: any, loc := panic("set_option() value must be an integer here", loc) } errno = linux.setsockopt(os_sock, level, int(option), &int_value) + case: + return .Invalid_Socket } + if errno != .NONE { return _socket_option_error(errno) } + return nil } diff --git a/core/net/socket_others.odin b/core/net/socket_others.odin new file mode 100644 index 000000000..61cf7240e --- /dev/null +++ b/core/net/socket_others.odin @@ -0,0 +1,105 @@ +#+build !darwin +#+build !linux +#+build !freebsd +#+build !windows +#+build !netbsd +#+build !openbsd +#+private +package net + +_SOCKET_OPTION_BROADCAST :: -1 +_SOCKET_OPTION_REUSE_ADDRESS :: -1 +_SOCKET_OPTION_KEEP_ALIVE :: -1 +_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE :: -1 +_SOCKET_OPTION_LINGER :: -1 +_SOCKET_OPTION_RECEIVE_BUFFER_SIZE :: -1 +_SOCKET_OPTION_SEND_BUFFER_SIZE :: -1 +_SOCKET_OPTION_RECEIVE_TIMEOUT :: -1 +_SOCKET_OPTION_SEND_TIMEOUT :: -1 + +_SOCKET_OPTION_TCP_NODELAY :: -1 + +_SOCKET_OPTION_USE_LOOPBACK :: -1 +_SOCKET_OPTION_REUSE_PORT :: -1 +_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE :: -1 +_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: -1 + +_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: -1 +_SOCKET_OPTION_CONDITIONAL_ACCEPT :: -1 +_SOCKET_OPTION_DONT_LINGER :: -1 + +_SHUTDOWN_MANNER_RECEIVE :: -1 +_SHUTDOWN_MANNER_SEND :: -1 +_SHUTDOWN_MANNER_BOTH :: -1 + +_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (sock: TCP_Socket, err: Network_Error) { + err = Create_Socket_Error.Network_Unreachable + return +} + +_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (sock: Any_Socket, err: Create_Socket_Error) { + err = .Network_Unreachable + return +} + +_bind :: proc(skt: Any_Socket, ep: Endpoint) -> (err: Bind_Error) { + err = .Network_Unreachable + return +} + +_listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (skt: TCP_Socket, err: Network_Error) { + err = Create_Socket_Error.Network_Unreachable + return +} + +_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Socket_Info_Error) { + err = .Network_Unreachable + return +} + +_peer_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Socket_Info_Error) { + err = .Network_Unreachable + return +} + +_accept_tcp :: proc(sock: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (client: TCP_Socket, source: Endpoint, err: Accept_Error) { + err = .Network_Unreachable + return +} + +_close :: proc(skt: Any_Socket) { +} + +_recv_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_read: int, err: TCP_Recv_Error) { + err = .Network_Unreachable + return +} + +_recv_udp :: proc(skt: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: UDP_Recv_Error) { + err = .Network_Unreachable + return +} + +_send_tcp :: proc(skt: TCP_Socket, buf: []byte) -> (bytes_written: int, err: TCP_Send_Error) { + err = .Network_Unreachable + return +} + +_send_udp :: proc(skt: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: UDP_Send_Error) { + err = .Network_Unreachable + return +} + +_shutdown :: proc(skt: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) { + err = .Network_Unreachable + return +} + +_set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error { + return .Network_Unreachable +} + +_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) { + err = .Network_Unreachable + return +} diff --git a/core/net/socket_darwin.odin b/core/net/socket_posix.odin index 8e01eb4a8..243b2e06f 100644 --- a/core/net/socket_darwin.odin +++ b/core/net/socket_posix.odin @@ -1,4 +1,4 @@ -#+build darwin +#+build darwin, netbsd, openbsd package net /* @@ -20,28 +20,33 @@ package net Feoramund: FreeBSD platform code */ -import "core:c" import "core:sys/posix" import "core:time" -Socket_Option :: enum c.int { - Broadcast = c.int(posix.Sock_Option.BROADCAST), - Reuse_Address = c.int(posix.Sock_Option.REUSEADDR), - Keep_Alive = c.int(posix.Sock_Option.KEEPALIVE), - Out_Of_Bounds_Data_Inline = c.int(posix.Sock_Option.OOBINLINE), - TCP_Nodelay = c.int(posix.TCP_NODELAY), - Linger = c.int(posix.Sock_Option.LINGER), - Receive_Buffer_Size = c.int(posix.Sock_Option.RCVBUF), - Send_Buffer_Size = c.int(posix.Sock_Option.SNDBUF), - Receive_Timeout = c.int(posix.Sock_Option.RCVTIMEO), - Send_Timeout = c.int(posix.Sock_Option.SNDTIMEO), -} +_SOCKET_OPTION_BROADCAST :: posix.Sock_Option.BROADCAST +_SOCKET_OPTION_REUSE_ADDRESS :: posix.Sock_Option.REUSEADDR +_SOCKET_OPTION_KEEP_ALIVE :: posix.Sock_Option.KEEPALIVE +_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE :: posix.Sock_Option.OOBINLINE +_SOCKET_OPTION_LINGER :: posix.Sock_Option.LINGER +_SOCKET_OPTION_RECEIVE_BUFFER_SIZE :: posix.Sock_Option.RCVBUF +_SOCKET_OPTION_SEND_BUFFER_SIZE :: posix.Sock_Option.SNDBUF +_SOCKET_OPTION_RECEIVE_TIMEOUT :: posix.Sock_Option.RCVTIMEO +_SOCKET_OPTION_SEND_TIMEOUT :: posix.Sock_Option.SNDTIMEO -Shutdown_Manner :: enum c.int { - Receive = c.int(posix.SHUT_RD), - Send = c.int(posix.SHUT_WR), - Both = c.int(posix.SHUT_RDWR), -} +_SOCKET_OPTION_TCP_NODELAY :: posix.TCP_NODELAY + +_SOCKET_OPTION_USE_LOOPBACK :: -1 +_SOCKET_OPTION_REUSE_PORT :: -1 +_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE :: -1 +_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: -1 + +_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: -1 +_SOCKET_OPTION_CONDITIONAL_ACCEPT :: -1 +_SOCKET_OPTION_DONT_LINGER :: -1 + +_SHUTDOWN_MANNER_RECEIVE :: posix.SHUT_RD +_SHUTDOWN_MANNER_SEND :: posix.SHUT_WR +_SHUTDOWN_MANNER_BOTH :: posix.SHUT_RDWR @(private) _create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) { @@ -273,7 +278,7 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca ptr: rawptr len: posix.socklen_t - switch option { + #partial switch option { case .Broadcast, .Reuse_Address, @@ -327,6 +332,8 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca } ptr = &int_value len = size_of(int_value) + case: + return .Invalid_Option } skt := any_socket_to_socket(s) diff --git a/core/net/socket_windows.odin b/core/net/socket_windows.odin index 6dd2f0458..4eea0ea65 100644 --- a/core/net/socket_windows.odin +++ b/core/net/socket_windows.odin @@ -24,59 +24,30 @@ import "core:c" import win "core:sys/windows" import "core:time" -Socket_Option :: enum c.int { - // bool: Whether the address that this socket is bound to can be reused by other sockets. - // This allows you to bypass the cooldown period if a program dies while the socket is bound. - Reuse_Address = win.SO_REUSEADDR, - - // bool: Whether other programs will be inhibited from binding the same endpoint as this socket. - Exclusive_Addr_Use = win.SO_EXCLUSIVEADDRUSE, - - // bool: When true, keepalive packets will be automatically be sent for this connection. TODO: verify this understanding - Keep_Alive = win.SO_KEEPALIVE, - - // bool: When true, client connections will immediately be sent a TCP/IP RST response, rather than being accepted. - Conditional_Accept = win.SO_CONDITIONAL_ACCEPT, - - // bool: If true, when the socket is closed, but data is still waiting to be sent, discard that data. - Dont_Linger = win.SO_DONTLINGER, - - // bool: When true, 'out-of-band' data sent over the socket will be read by a normal net.recv() call, the same as normal 'in-band' data. - Out_Of_Bounds_Data_Inline = win.SO_OOBINLINE, - - // bool: When true, disables send-coalescing, therefore reducing latency. - TCP_Nodelay = win.TCP_NODELAY, - - // win.LINGER: Customizes how long (if at all) the socket will remain open when there - // is some remaining data waiting to be sent, and net.close() is called. - Linger = win.SO_LINGER, - - // win.DWORD: The size, in bytes, of the OS-managed receive-buffer for this socket. - Receive_Buffer_Size = win.SO_RCVBUF, - - // win.DWORD: The size, in bytes, of the OS-managed send-buffer for this socket. - Send_Buffer_Size = win.SO_SNDBUF, - - // win.DWORD: For blocking sockets, the time in milliseconds to wait for incoming data to be received, before giving up and returning .Timeout. - // For non-blocking sockets, ignored. - // Use a value of zero to potentially wait forever. - Receive_Timeout = win.SO_RCVTIMEO, - - // win.DWORD: For blocking sockets, the time in milliseconds to wait for outgoing data to be sent, before giving up and returning .Timeout. - // For non-blocking sockets, ignored. - // Use a value of zero to potentially wait forever. - Send_Timeout = win.SO_SNDTIMEO, - - // bool: Allow sending to, receiving from, and binding to, a broadcast address. - Broadcast = win.SO_BROADCAST, -} - - -Shutdown_Manner :: enum c.int { - Receive = win.SD_RECEIVE, - Send = win.SD_SEND, - Both = win.SD_BOTH, -} +_SOCKET_OPTION_BROADCAST :: win.SO_BROADCAST +_SOCKET_OPTION_REUSE_ADDRESS :: win.SO_REUSEADDR +_SOCKET_OPTION_KEEP_ALIVE :: win.SO_KEEPALIVE +_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE :: win.SO_OOBINLINE +_SOCKET_OPTION_LINGER :: win.SO_LINGER +_SOCKET_OPTION_RECEIVE_BUFFER_SIZE :: win.SO_RCVBUF +_SOCKET_OPTION_SEND_BUFFER_SIZE :: win.SO_SNDBUF +_SOCKET_OPTION_RECEIVE_TIMEOUT :: win.SO_RCVTIMEO +_SOCKET_OPTION_SEND_TIMEOUT :: win.SO_SNDTIMEO + +_SOCKET_OPTION_TCP_NODELAY :: win.TCP_NODELAY + +_SOCKET_OPTION_USE_LOOPBACK :: -1 +_SOCKET_OPTION_REUSE_PORT :: -1 +_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE :: -1 +_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: -1 + +_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: win.SO_EXCLUSIVEADDRUSE +_SOCKET_OPTION_CONDITIONAL_ACCEPT :: win.SO_CONDITIONAL_ACCEPT +_SOCKET_OPTION_DONT_LINGER :: win.SO_DONTLINGER + +_SHUTDOWN_MANNER_RECEIVE :: win.SD_RECEIVE +_SHUTDOWN_MANNER_SEND :: win.SD_SEND +_SHUTDOWN_MANNER_BOTH :: win.SD_BOTH @(init, private) ensure_winsock_initialized :: proc "contextless" () { @@ -322,7 +293,7 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca ptr: rawptr len: c.int - switch option { + #partial switch option { case .Reuse_Address, .Exclusive_Addr_Use, @@ -383,6 +354,8 @@ _set_option :: proc(s: Any_Socket, option: Socket_Option, value: any, loc := #ca } ptr = &int_value len = size_of(int_value) + case: + return .Invalid_Option } socket := any_socket_to_socket(s) diff --git a/core/os/os2/file.odin b/core/os/os2/file.odin index 9e7788c31..33446726e 100644 --- a/core/os/os2/file.odin +++ b/core/os/os2/file.odin @@ -67,7 +67,7 @@ File_Flag :: enum { Trunc, Sparse, Inheritable, - + Non_Blocking, Unbuffered_IO, } diff --git a/core/os/os2/file_linux.odin b/core/os/os2/file_linux.odin index fb25ca411..924251dfc 100644 --- a/core/os/os2/file_linux.odin +++ b/core/os/os2/file_linux.odin @@ -82,6 +82,7 @@ _open :: proc(name: string, flags: File_Flags, perm: Permissions) -> (f: ^File, if .Excl in flags { sys_flags += {.EXCL} } if .Sync in flags { sys_flags += {.DSYNC} } if .Trunc in flags { sys_flags += {.TRUNC} } + if .Non_Blocking in flags { sys_flags += {.NONBLOCK} } if .Inheritable in flags { sys_flags -= {.CLOEXEC} } fd, errno := linux.open(name_cstr, sys_flags, transmute(linux.Mode)transmute(u32)perm) diff --git a/core/os/os2/file_posix.odin b/core/os/os2/file_posix.odin index 874ec7c7d..cdc8e491a 100644 --- a/core/os/os2/file_posix.odin +++ b/core/os/os2/file_posix.odin @@ -61,12 +61,13 @@ _open :: proc(name: string, flags: File_Flags, perm: Permissions) -> (f: ^File, } } - if .Append in flags { sys_flags += {.APPEND} } - if .Create in flags { sys_flags += {.CREAT} } - if .Excl in flags { sys_flags += {.EXCL} } - if .Sync in flags { sys_flags += {.DSYNC} } - if .Trunc in flags { sys_flags += {.TRUNC} } - if .Inheritable in flags { sys_flags -= {.CLOEXEC} } + if .Append in flags { sys_flags += {.APPEND} } + if .Create in flags { sys_flags += {.CREAT} } + if .Excl in flags { sys_flags += {.EXCL} } + if .Sync in flags { sys_flags += {.DSYNC} } + if .Trunc in flags { sys_flags += {.TRUNC} } + if .Non_Blocking in flags { sys_flags += {.NONBLOCK} } + if .Inheritable in flags { sys_flags -= {.CLOEXEC} } temp_allocator := TEMP_ALLOCATOR_GUARD({}) cname := clone_to_cstring(name, temp_allocator) or_return diff --git a/core/os/os2/file_wasi.odin b/core/os/os2/file_wasi.odin index b60cce4be..fc37ef9f2 100644 --- a/core/os/os2/file_wasi.odin +++ b/core/os/os2/file_wasi.odin @@ -185,8 +185,9 @@ _open :: proc(name: string, flags: File_Flags, perm: Permissions) -> (f: ^File, if .Trunc in flags { oflags += {.TRUNC} } fdflags: wasi.fdflags_t - if .Append in flags { fdflags += {.APPEND} } - if .Sync in flags { fdflags += {.SYNC} } + if .Append in flags { fdflags += {.APPEND} } + if .Sync in flags { fdflags += {.SYNC} } + if .Non_Blocking in flags { fdflags += {.NONBLOCK} } // NOTE: rights are adjusted to what this package's functions might want to call. rights: wasi.rights_t diff --git a/core/os/os2/file_windows.odin b/core/os/os2/file_windows.odin index 9a969f07e..4df9398cc 100644 --- a/core/os/os2/file_windows.odin +++ b/core/os/os2/file_windows.odin @@ -126,7 +126,11 @@ _open_internal :: proc(name: string, flags: File_Flags, perm: Permissions) -> (h // NOTE(bill): Open has just asked to create a file in read-only mode. // If the file already exists, to make it akin to a *nix open call, // the call preserves the existing permissions. - h := win32.CreateFileW(path, access, share_mode, &sa, win32.TRUNCATE_EXISTING, win32.FILE_ATTRIBUTE_NORMAL, nil) + nix_attrs := win32.FILE_ATTRIBUTE_NORMAL + if .Non_Blocking in flags { + nix_attrs |= win32.FILE_FLAG_OVERLAPPED + } + h := win32.CreateFileW(path, access, share_mode, &sa, win32.TRUNCATE_EXISTING, nix_attrs, nil) if h == win32.INVALID_HANDLE { switch e := win32.GetLastError(); e { case win32.ERROR_FILE_NOT_FOUND, _ERROR_BAD_NETPATH, win32.ERROR_PATH_NOT_FOUND: @@ -140,6 +144,10 @@ _open_internal :: proc(name: string, flags: File_Flags, perm: Permissions) -> (h } } + if .Non_Blocking in flags { + attrs |= win32.FILE_FLAG_OVERLAPPED + } + h := win32.CreateFileW(path, access, share_mode, &sa, create_mode, attrs, nil) if h == win32.INVALID_HANDLE { return 0, _get_platform_error() diff --git a/core/os/os2/temp_file.odin b/core/os/os2/temp_file.odin index f0bc3788e..2c0236428 100644 --- a/core/os/os2/temp_file.odin +++ b/core/os/os2/temp_file.odin @@ -14,7 +14,7 @@ MAX_ATTEMPTS :: 1<<13 // Should be enough for everyone, right? // // The caller must `close` the file once finished with. @(require_results) -create_temp_file :: proc(dir, pattern: string) -> (f: ^File, err: Error) { +create_temp_file :: proc(dir, pattern: string, additional_flags: File_Flags = {}) -> (f: ^File, err: Error) { temp_allocator := TEMP_ALLOCATOR_GUARD({}) dir := dir if dir != "" else temp_directory(temp_allocator) or_return prefix, suffix := _prefix_and_suffix(pattern) or_return @@ -26,7 +26,7 @@ create_temp_file :: proc(dir, pattern: string) -> (f: ^File, err: Error) { attempts := 0 for { name := concatenate_strings_from_buffer(name_buf[:], prefix, random_string(rand_buf[:]), suffix) - f, err = open(name, {.Read, .Write, .Create, .Excl}, Permissions_Read_Write_All) + f, err = open(name, {.Read, .Write, .Create, .Excl} + additional_flags, Permissions_Read_Write_All) if err == .Exist { close(f) attempts += 1 diff --git a/core/os/os_windows.odin b/core/os/os_windows.odin index 03c194596..cb7e42f67 100644 --- a/core/os/os_windows.odin +++ b/core/os/os_windows.odin @@ -333,8 +333,14 @@ open :: proc(path: string, mode: int = O_RDONLY, perm: int = 0) -> (Handle, Erro case: create_mode = win32.OPEN_EXISTING } + + attrs := win32.FILE_ATTRIBUTE_NORMAL|win32.FILE_FLAG_BACKUP_SEMANTICS + if mode & (O_NONBLOCK) == O_NONBLOCK { + attrs |= win32.FILE_FLAG_OVERLAPPED + } + wide_path := win32.utf8_to_wstring(path) - handle := Handle(win32.CreateFileW(wide_path, access, share_mode, sa, create_mode, win32.FILE_ATTRIBUTE_NORMAL|win32.FILE_FLAG_BACKUP_SEMANTICS, nil)) + handle := Handle(win32.CreateFileW(wide_path, access, share_mode, sa, create_mode, attrs, nil)) if handle != INVALID_HANDLE { return handle, nil } @@ -862,4 +868,4 @@ pipe :: proc() -> (r, w: Handle, err: Error) { err = get_last_error() } return -}
\ No newline at end of file +} diff --git a/core/simd/simd.odin b/core/simd/simd.odin index 14bf03f43..e2373d3e2 100644 --- a/core/simd/simd.odin +++ b/core/simd/simd.odin @@ -2207,7 +2207,7 @@ swizzle :: builtin.swizzle /* Extract the set of most-significant bits of a SIMD vector. -This procedure checks the the most-significant bit (MSB) for each lane of vector +This procedure checks the most-significant bit (MSB) for each lane of vector and returns the numbers of lanes with the most-significant bit set. This procedure can be used in conjuction with `lanes_eq` (and other similar procedures) to count the number of matched lanes by computing the cardinality of the resulting @@ -2253,7 +2253,7 @@ extract_msbs :: intrinsics.simd_extract_msbs /* Extract the set of least-significant bits of a SIMD vector. -This procedure checks the the least-significant bit (LSB) for each lane of vector +This procedure checks the least-significant bit (LSB) for each lane of vector and returns the numbers of lanes with the least-significant bit set. This procedure can be used in conjuction with `lanes_eq` (and other similar procedures) to count the number of matched lanes by computing the cardinality of the resulting diff --git a/core/slice/slice.odin b/core/slice/slice.odin index 58d35b9f0..12ebfce3b 100644 --- a/core/slice/slice.odin +++ b/core/slice/slice.odin @@ -924,3 +924,39 @@ bitset_to_enum_slice_with_make :: proc(bs: $T, $E: typeid, allocator := context. } bitset_to_enum_slice :: proc{bitset_to_enum_slice_with_make, bitset_to_enum_slice_with_buffer} + +/* +Removes the first n elements (`elems`) from a slice of slices, spanning inner slices and dropping empty ones. + +If `elems` is out of bounds (more than the total) this will trigger a bounds check. + +Example: + import "core:fmt" + import "core:slice" + + advance_slices_example :: proc() { + slices := [][]byte { + {1, 2, 3, 4}, + {5, 6, 7}, + } + + fmt.println(slice.advance_slices(slices, 4)) + } + +Output: + [[5, 6, 7]] +*/ +advance_slices :: proc(slices: $S/[][]$T, elems: int) -> S { + slices, elems := slices, elems + for elems > 0 { + slice := &slices[0] + take := builtin.min(elems, len(slice)) + slice^ = slice[take:] + if len(slice) == 0 { + slices = slices[1:] + } + elems -= take + } + + return slices +} diff --git a/core/slice/sort.odin b/core/slice/sort.odin index d438cfc1b..9f91a0f73 100644 --- a/core/slice/sort.odin +++ b/core/slice/sort.odin @@ -300,7 +300,9 @@ Example: import "core:slice" import "core:fmt" - main :: proc() { + stable_sort_by_example :: proc() { + Example :: struct { n: int, s: string } + arr := []Example { {2, "name"}, {3, "Bill"}, @@ -312,10 +314,9 @@ Example: }) for e in arr do fmt.printf("%s ", e.s) + fmt.println() } - Example :: struct { n: int, s: string } - Output: My name is Bill */ @@ -335,7 +336,9 @@ Example: import "core:slice" import "core:fmt" - main :: proc() { + stable_sort_by_cmp_example :: proc() { + Example :: struct { n: int, s: string } + arr := []Example { {2, "name"}, {3, "Bill"}, @@ -347,9 +350,9 @@ Example: }) for e in arr do fmt.printf("%s ", e.s) + fmt.println() } - Example :: struct { n: int, s: string } Output: My name is Bill */ diff --git a/core/sync/chan/chan.odin b/core/sync/chan/chan.odin index 05312e5a2..17618763f 100644 --- a/core/sync/chan/chan.odin +++ b/core/sync/chan/chan.odin @@ -1166,7 +1166,7 @@ Example: import "core:sync/chan" import "core:fmt" - select_raw_example :: proc() { + try_select_raw_example :: proc() { c, err := chan.create(chan.Chan(int), 1, context.allocator) assert(err == .None) defer chan.destroy(c) @@ -1198,11 +1198,11 @@ Example: Output: - SELECT: 0 true + SELECT: 0 Send RECEIVED VALUE 0 - SELECT: 0 true + SELECT: 0 Recv RECEIVED VALUE 1 - SELECT: 0 false + SELECT: -1 None */ @(require_results) @@ -1219,7 +1219,7 @@ try_select_raw :: proc "odin" (recvs: []^Raw_Chan, sends: []^Raw_Chan, send_msgs count := 0 for c, i in recvs { - if can_recv(c) { + if !c.closed && can_recv(c) { candidates[count] = { is_recv = true, idx = i, @@ -1232,7 +1232,7 @@ try_select_raw :: proc "odin" (recvs: []^Raw_Chan, sends: []^Raw_Chan, send_msgs if i > builtin.len(send_msgs)-1 || send_msgs[i] == nil { continue } - if can_send(c) { + if !c.closed && can_send(c) { candidates[count] = { is_recv = false, idx = i, diff --git a/core/sys/kqueue/kqueue.odin b/core/sys/kqueue/kqueue.odin index 25ee9bdce..b51f3426f 100644 --- a/core/sys/kqueue/kqueue.odin +++ b/core/sys/kqueue/kqueue.odin @@ -32,6 +32,7 @@ kevent :: proc(kq: KQ, change_list: []KEvent, event_list: []KEvent, timeout: ^po timeout, ) if n_events == -1 { + n_events = 0 err = posix.errno() } return @@ -60,6 +61,7 @@ Filter :: enum _Filter_Backing { Proc = _FILTER_PROC, // Check for changes to the subject process. Signal = _FILTER_SIGNAL, // Check for signals delivered to the process. Timer = _FILTER_TIMER, // Timers. + User = _FILTER_USER, // User events. } RW_Flag :: enum u32 { @@ -82,18 +84,30 @@ Proc_Flag :: enum u32 { Exit = log2(0x80000000), // Process exited. Fork = log2(0x40000000), // Process forked. Exec = log2(0x20000000), // Process exec'd. - Signal = log2(0x08000000), // Shared with `Filter.Signal`. } Proc_Flags :: bit_set[Proc_Flag; u32] Timer_Flag :: enum u32 { Seconds = log2(0x00000001), // Data is seconds. - USeconds = log2(0x00000002), // Data is microseconds. - NSeconds = log2(_NOTE_NSECONDS), // Data is nanoseconds. + USeconds = log2(_NOTE_USECONDS), // Data is microseconds. Absolute = log2(_NOTE_ABSOLUTE), // Absolute timeout. } Timer_Flags :: bit_set[Timer_Flag; u32] +User_Flag :: enum u32 { + Trigger = log2(0x01000000), + FFAnd = log2(0x40000000), + FFOr = log2(0x80000000), +} +User_Flags :: bit_set[User_Flag; u32] + +USER_FLAGS_COPY :: User_Flags{.FFOr, .FFAnd} +USER_FLAGS_CONTROL_MASK :: transmute(User_Flags)u32(0xc0000000) +USER_FLAGS_MASK :: transmute(User_Flags)u32(0x00FFFFFF) + +// Data is nanoseconds. +TIMER_FLAGS_NSECONDS :: _TIMER_FLAGS_NSECONDS + when ODIN_OS == .Darwin { _Filter_Backing :: distinct i16 @@ -106,10 +120,14 @@ when ODIN_OS == .Darwin { _FILTER_PROC :: -5 _FILTER_SIGNAL :: -6 _FILTER_TIMER :: -7 + _FILTER_USER :: -10 + _NOTE_USECONDS :: 0x00000002 _NOTE_NSECONDS :: 0x00000004 _NOTE_ABSOLUTE :: 0x00000008 + _TIMER_FLAGS_NSECONDS :: Timer_Flags{Timer_Flag(log2(_NOTE_NSECONDS))} + KEvent :: struct #align(4) { // Value used to identify this event. The exact interpretation is determined by the attached filter. ident: uintptr, @@ -119,11 +137,12 @@ when ODIN_OS == .Darwin { flags: Flags, // Filter specific flags. fflags: struct #raw_union { - rw: RW_Flags, - vnode: VNode_Flags, - fproc: Proc_Flags, + rw: RW_Flags `raw_union_tag:"filter=.Read, filter=.Write"`, + vnode: VNode_Flags `raw_union_tag:"filter=.VNode"`, + fproc: Proc_Flags `raw_union_tag:"filter=.Proc"`, // vm: VM_Flags, - timer: Timer_Flags, + timer: Timer_Flags `raw_union_tag:"filter=.Timer"`, + user: User_Flags `raw_union_tag:"filter=.User"`, }, // Filter specific data. data: c.long /* intptr_t */, @@ -143,9 +162,13 @@ when ODIN_OS == .Darwin { _FILTER_PROC :: -5 _FILTER_SIGNAL :: -6 _FILTER_TIMER :: -7 + _FILTER_USER :: -11 - _NOTE_NSECONDS :: 0x00000004 - _NOTE_ABSOLUTE :: 0x00000008 + _NOTE_USECONDS :: 0x00000004 + _NOTE_NSECONDS :: 0x00000008 + _NOTE_ABSOLUTE :: 0x00000010 + + _TIMER_FLAGS_NSECONDS :: Timer_Flags{Timer_Flag(log2(_NOTE_NSECONDS))} KEvent :: struct { // Value used to identify this event. The exact interpretation is determined by the attached filter. @@ -156,11 +179,12 @@ when ODIN_OS == .Darwin { flags: Flags, // Filter specific flags. fflags: struct #raw_union { - rw: RW_Flags, - vnode: VNode_Flags, - fproc: Proc_Flags, + rw: RW_Flags `raw_union_tag:"filter=.Read, filter=.Write"`, + vnode: VNode_Flags `raw_union_tag:"filter=.VNode"`, + fproc: Proc_Flags `raw_union_tag:"filter=.Proc"`, // vm: VM_Flags, - timer: Timer_Flags, + timer: Timer_Flags `raw_union_tag:"filter=.Timer"`, + user: User_Flags `raw_union_tag:"filter=.User"`, }, // Filter specific data. data: i64, @@ -181,11 +205,14 @@ when ODIN_OS == .Darwin { _FILTER_PROC :: 4 _FILTER_SIGNAL :: 5 _FILTER_TIMER :: 6 + _FILTER_USER :: 8 - _NOTE_NSECONDS :: 0x00000003 + _NOTE_USECONDS :: 0x00000002 _NOTE_ABSOLUTE :: 0x00000010 - KEvent :: struct #align(4) { + _TIMER_FLAGS_NSECONDS :: Timer_Flags{.Seconds, .USeconds} + + KEvent :: struct { // Value used to identify this event. The exact interpretation is determined by the attached filter. ident: uintptr, // Filter for event. @@ -194,18 +221,17 @@ when ODIN_OS == .Darwin { flags: Flags, // Filter specific flags. fflags: struct #raw_union { - rw: RW_Flags, - vnode: VNode_Flags, - fproc: Proc_Flags, + rw: RW_Flags `raw_union_tag:"filter=.Read, filter=.Write"`, + vnode: VNode_Flags `raw_union_tag:"filter=.VNode"`, + fproc: Proc_Flags `raw_union_tag:"filter=.Proc"`, // vm: VM_Flags, - timer: Timer_Flags, + timer: Timer_Flags `raw_union_tag:"filter=.Timer"`, + user: User_Flags `raw_union_tag:"filter=.User"`, }, // Filter specific data. data: i64, // Opaque user data passed through the kernel unchanged. udata: rawptr, - // Extensions. - ext: [4]u64, } } else when ODIN_OS == .OpenBSD { @@ -219,10 +245,14 @@ when ODIN_OS == .Darwin { _FILTER_PROC :: -5 _FILTER_SIGNAL :: -6 _FILTER_TIMER :: -7 + _FILTER_USER :: -10 - _NOTE_NSECONDS :: 0x00000003 + _NOTE_USECONDS :: 0x00000002 + _NOTE_NSECONDS :: 0x00000004 _NOTE_ABSOLUTE :: 0x00000010 + _TIMER_FLAGS_NSECONDS :: Timer_Flags{Timer_Flag(log2(_NOTE_NSECONDS))} + KEvent :: struct #align(4) { // Value used to identify this event. The exact interpretation is determined by the attached filter. ident: uintptr, @@ -232,11 +262,12 @@ when ODIN_OS == .Darwin { flags: Flags, // Filter specific flags. fflags: struct #raw_union { - rw: RW_Flags, - vnode: VNode_Flags, - fproc: Proc_Flags, + rw: RW_Flags `raw_union_tag:"filter=.Read, filter=.Write"`, + vnode: VNode_Flags `raw_union_tag:"filter=.VNode"`, + fproc: Proc_Flags `raw_union_tag:"filter=.Proc"`, // vm: VM_Flags, - timer: Timer_Flags, + timer: Timer_Flags `raw_union_tag:"filter=.Timer"`, + user: User_Flags `raw_union_tag:"filter=.User"`, }, // Filter specific data. data: i64, @@ -245,12 +276,20 @@ when ODIN_OS == .Darwin { } } +when ODIN_OS == .NetBSD { + @(private) + LKEVENT :: "__kevent50" +} else { + @(private) + LKEVENT :: "kevent" +} + @(private) log2 :: intrinsics.constant_log2 foreign lib { @(link_name="kqueue") _kqueue :: proc() -> KQ --- - @(link_name="kevent") + @(link_name=LKEVENT) _kevent :: proc(kq: KQ, change_list: [^]KEvent, n_changes: c.int, event_list: [^]KEvent, n_events: c.int, timeout: ^posix.timespec) -> c.int --- } diff --git a/core/sys/linux/bits.odin b/core/sys/linux/bits.odin index 2487ebe92..2ca7558a2 100644 --- a/core/sys/linux/bits.odin +++ b/core/sys/linux/bits.odin @@ -2270,6 +2270,12 @@ Swap_Flags_Bits :: enum { DISCARD = log2(0x10000), } +Eventfd_Flags_Bits :: enum { + SEMAPHORE, + CLOEXEC = auto_cast Open_Flags_Bits.CLOEXEC, + NONBLOCK = auto_cast Open_Flags_Bits.NONBLOCK, +} + Sched_Policy :: enum u32 { OTHER = 0, BATCH = 3, diff --git a/core/sys/linux/sys.odin b/core/sys/linux/sys.odin index b4943411a..392761e1d 100644 --- a/core/sys/linux/sys.odin +++ b/core/sys/linux/sys.odin @@ -3070,7 +3070,10 @@ timerfd_create :: proc "contextless" (clock_id: Clock_Id, flags: Open_Flags) -> return errno_unwrap2(ret, Fd) } -// TODO(flysand): eventfd +eventfd :: proc "contextless" (initval: u32, flags: Eventfd_Flags) -> (Fd, Errno) { + ret := syscall(SYS_eventfd2, initval, transmute(i32)flags) + return errno_unwrap2(ret, Fd) +} // TODO(flysand): fallocate diff --git a/core/sys/linux/types.odin b/core/sys/linux/types.odin index d0cc6c433..c2334e5b6 100644 --- a/core/sys/linux/types.odin +++ b/core/sys/linux/types.odin @@ -1589,23 +1589,23 @@ IO_Uring_SQE :: struct { using __ioprio: struct #raw_union { ioprio: u16, sq_accept_flags: IO_Uring_Accept_Flags, - sq_send_recv_flags: IO_Uring_Send_Recv_Flags, + sq_send_recv_flags: IO_Uring_Send_Recv_Flags `raw_union_tag:"opcode=.SENDMSG, opcode=.RECVMSG, opcode=.SEND, opcode=.RECV"`, }, fd: Fd, using __offset: struct #raw_union { // Offset into file. - off: u64, - addr2: u64, + off: u64 `raw_union_tag:"opcode=.READV, opcode=.WRITEV, opcode=.SPLICE, opcode=.POLL_REMOVE, opcode=.EPOLL_CTL, opcode=.TIMEOUT, opcode=.ACCEPT, opcode=.CONNECT, opcode=.READ, opcode=.WRITE, opcode=.FILES_UPDATE, opcode=.SOCKET"`, + addr2: u64 `raw_union_tag:"opcode=.SEND, opcode=.BIND"`, using _: struct { cmd_op: u32, __pad1: u32, }, - statx: ^Statx, + statx: ^Statx `raw_union_tag:"opcode=.STATX"`, }, using __iovecs: struct #raw_union { // Pointer to buffer or iovecs. - addr: u64, - splice_off_in: u64, + addr: u64 `raw_union_tag:"opcode=.READV, opcode=.WRITEV, opcode=.POLL_REMOVE, opcode=.EPOLL_CTL, opcode=.SENDMSG, opcode=.RECVMSG, opcode=.SEND, opcode=.RECV, opcode=.TIMEOUT, opcode=.TIMEOUT_REMOVE, opcode=.ACCEPT, opcode=.ASYNC_CANCEL, opcode=.LINK_TIMEOUT, opcode=.CONNECT, opcode=.MADVISE, opcode=.OPENAT, opcode=.STATX, opcode=.READ, opcode=.WRITE, opcode=.FILES_UPDATE, opcode=.BIND, opcode=.LISTEN"`, + splice_off_in: u64 `raw_union_tag:"opcode=.SPLICE"`, using _: struct { level: u32, optname: u32, @@ -1613,28 +1613,28 @@ IO_Uring_SQE :: struct { }, using __len: struct #raw_union { // Buffer size or number of iovecs. - len: u32, - poll_flags: IO_Uring_Poll_Add_Flags, - statx_mask: Statx_Mask, - epoll_ctl_op: EPoll_Ctl_Opcode, - shutdown_how: Shutdown_How, + len: u32 `raw_union_tag:"opcode=.READV, opcode=.WRITEV, opcode=.SPLICE, opcode=.SEND, opcode=.RECV, opcode=.TIMEOUT, opcode=.LINK_TIMEOUT, opcode=.MADVISE, opcode=.OPENAT, opcode=.READ, opcode=.WRITE, opcode=.TEE, opcode=.FILES_UPDATE, opcode=.SOCKET"`, + poll_flags: IO_Uring_Poll_Add_Flags `raw_union_tag:"opcode=.POLL_ADD, opcode=.POLL_REMOVE"`, + statx_mask: Statx_Mask `raw_union_tag:"opcode=.STATX"`, + epoll_ctl_op: EPoll_Ctl_Opcode `raw_union_tag:"opcode=.EPOLL_CTL"`, + shutdown_how: Shutdown_How `raw_union_tag:"opcode=.SHUTDOWN"`, }, using __contents: struct #raw_union { - rw_flags: i32, - fsync_flags: IO_Uring_Fsync_Flags, + rw_flags: i32 `raw_union_tag:"opcode=.READV, opcode=.WRITEV, opcode=.SOCKET"`, + fsync_flags: IO_Uring_Fsync_Flags `raw_union_tag:"opcode=.FSYNC"`, // compatibility. - poll_events: Fd_Poll_Events, + poll_events: Fd_Poll_Events `raw_union_tag:"opcode=.POLL_ADD, opcode=.POLL_REMOVE"`, // word-reversed for BE. poll32_events: u32, sync_range_flags: u32, - msg_flags: Socket_Msg, - timeout_flags: IO_Uring_Timeout_Flags, - accept_flags: Socket_FD_Flags, + msg_flags: Socket_Msg `raw_union_tag:"opcode=.SENDMSG, opcode=.RECVMSG, opcode=.SEND, opcode=.RECV"`, + timeout_flags: IO_Uring_Timeout_Flags `raw_union_tag:"opcode=.TIMEOUT, opcode=.TIMEOUT_REMOVE, opcode=.LINK_TIMEOUT"`, + accept_flags: Socket_FD_Flags `raw_union_tag:"opcode=.ACCEPT"`, cancel_flags: u32, - open_flags: Open_Flags, - statx_flags: FD_Flags, - fadvise_advice: u32, - splice_flags: IO_Uring_Splice_Flags, + open_flags: Open_Flags `raw_union_tag:"opcode=.OPENAT"`, + statx_flags: FD_Flags `raw_union_tag:"opcode=.STATX"`, + fadvise_advice: u32 `raw_union_tag:"opcode=.MADVISE"`, + splice_flags: IO_Uring_Splice_Flags `raw_union_tag:"opcode=.SPLICE, opcode=.TEE"`, rename_flags: u32, unlink_flags: u32, hardlink_flags: u32, @@ -1653,10 +1653,10 @@ IO_Uring_SQE :: struct { // Personality to use, if used. personality: u16, using _: struct #raw_union { - splice_fd_in: Fd, - file_index: u32, + splice_fd_in: Fd `raw_union_tag:"opcode=.SPLICE, opcode=.TEE"`, + file_index: u32 `raw_union_tag:"opcode=.ACCEPT, opcode=.OPENAT, opcode=.CLOSE, opcode=.SOCKET"`, using _: struct { - addr_len: u16, + addr_len: u16 `raw_union_tag:"opcode=.SEND"`, __pad3: [1]u16, }, }, @@ -1749,6 +1749,8 @@ Umount2_Flags :: bit_set[Umount2_Flags_Bits; u32] Swap_Flags :: bit_set[Swap_Flags_Bits; u32] +Eventfd_Flags :: bit_set[Eventfd_Flags_Bits; i32] + Cpu_Set :: bit_set[0 ..< 128] Sched_Param :: struct { diff --git a/core/sys/linux/uring/doc.odin b/core/sys/linux/uring/doc.odin new file mode 100644 index 000000000..8b9ae5ee8 --- /dev/null +++ b/core/sys/linux/uring/doc.odin @@ -0,0 +1,88 @@ +/* +Wrapper/convenience package over the raw io_uring syscalls, providing help with setup, creation, and operating the ring. + +The following example shows a simple `cat` program implementation using the package. + +Example: + package main + + import "base:runtime" + + import "core:fmt" + import "core:os" + import "core:sys/linux" + import "core:sys/linux/uring" + + Request :: struct { + path: cstring, + buffer: []byte, + completion: linux.IO_Uring_CQE, + } + + main :: proc() { + if len(os.args) < 2 { + fmt.eprintfln("Usage: %s [file name] <[file name] ...>", os.args[0]) + os.exit(1) + } + + requests := make_soa(#soa []Request, len(os.args)-1) + defer delete(requests) + + ring: uring.Ring + params := uring.DEFAULT_PARAMS + err := uring.init(&ring, ¶ms) + fmt.assertf(err == nil, "uring.init: %v", err) + defer uring.destroy(&ring) + + for &request, i in requests { + request.path = runtime.args__[i+1] + // sets up a read requests and adds it to the ring buffer. + submit_read_request(request.path, &request.buffer, &ring) + } + + ulen := u32(len(requests)) + + // submit the requests and wait for them to complete right away. + n, serr := uring.submit(&ring, ulen) + fmt.assertf(serr == nil, "uring.submit: %v", serr) + assert(n == ulen) + + // copy the completed requests out of the ring buffer. + cn := uring.copy_cqes_ready(&ring, requests.completion[:ulen]) + assert(cn == ulen) + + for request in requests { + // check result of the requests. + fmt.assertf(request.completion.res >= 0, "read %q failed: %v", request.path, linux.Errno(-request.completion.res)) + // print out. + fmt.print(string(request.buffer)) + + delete(request.buffer) + } + } + + submit_read_request :: proc(path: cstring, buffer: ^[]byte, ring: ^uring.Ring) { + fd, err := linux.open(path, {}) + fmt.assertf(err == nil, "open(%q): %v", path, err) + + file_sz := get_file_size(fd) + + buffer^ = make([]byte, file_sz) + + _, ok := uring.read(ring, 0, fd, buffer^, 0) + assert(ok, "could not get read sqe") + } + + get_file_size :: proc(fd: linux.Fd) -> uint { + st: linux.Stat + err := linux.fstat(fd, &st) + fmt.assertf(err == nil, "fstat: %v", err) + + if linux.S_ISREG(st.mode) { + return uint(st.size) + } + + panic("not a regular file") + } +*/ +package uring diff --git a/core/sys/linux/uring/ops.odin b/core/sys/linux/uring/ops.odin new file mode 100644 index 000000000..7b3392e98 --- /dev/null +++ b/core/sys/linux/uring/ops.odin @@ -0,0 +1,847 @@ +package uring + +import "core:sys/linux" + +// Do not perform any I/O. This is useful for testing the performance of the uring implementation itself. +nop :: proc(ring: ^Ring, user_data: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .NOP + sqe.user_data = user_data + + ok = true + return +} + +// Vectored read operation, see also readv(2). +readv :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, iovs: []linux.IO_Vec, off: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .READV + sqe.fd = fd + sqe.addr = cast(u64)uintptr(raw_data(iovs)) + sqe.len = u32(len(iovs)) + sqe.off = off + sqe.user_data = user_data + + ok = true + return +} + +// Vectored write operation, see also writev(2). +writev :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, iovs: []linux.IO_Vec, off: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .WRITEV + sqe.fd = fd + sqe.addr = cast(u64)uintptr(raw_data(iovs)) + sqe.len = u32(len(iovs)) + sqe.off = off + sqe.user_data = user_data + + ok = true + return +} + +read_fixed :: proc() { + unimplemented() +} + +write_fixed :: proc() { + unimplemented() +} + +/* +File sync. See also fsync(2). + +Optionally off and len can be used to specify a range within the file to be synced rather than syncing the entire file, which is the default behavior. + +Note that, while I/O is initiated in the order in which it appears in the submission queue, completions are unordered. +For example, an application which places a write I/O followed by an fsync in the submission queue cannot expect the fsync to apply to the write. +The two operations execute in parallel, so the fsync may complete before the write is issued to the storage. +The same is also true for previously issued writes that have not completed prior to the fsync. +To enforce ordering one may utilize linked SQEs, +IOSQE_IO_DRAIN or wait for the arrival of CQEs of requests which have to be ordered before a given request before submitting its SQE. +*/ +fsync :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, flags: linux.IO_Uring_Fsync_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .FSYNC + sqe.fsync_flags = flags + sqe.fd = fd + sqe.user_data = user_data + + ok = true + return +} + +/* +Poll the fd specified in the submission queue entry for the events specified in the poll_events field. + +Unlike poll or epoll without EPOLLONESHOT, by default this interface always works in one shot mode. +That is, once the poll operation is completed, it will have to be resubmitted. + +If IORING_POLL_ADD_MULTI is set in the SQE len field, then the poll will work in multi shot mode instead. +That means it'll repatedly trigger when the requested event becomes true, and hence multiple CQEs can be generated from this single SQE. +The CQE flags field will have IORING_CQE_F_MORE set on completion if the application should expect further CQE entries from the original request. +If this flag isn't set on completion, then the poll request has been terminated and no further events will be generated. +This mode is available since 5.13. + +This command works like an async poll(2) and the completion event result is the returned mask of events. + +Without IORING_POLL_ADD_MULTI and the initial poll operation with IORING_POLL_ADD_MULTI the operation is level triggered, +i.e. if there is data ready or events pending etc. +at the time of submission a corresponding CQE will be posted. +Potential further completions beyond the first caused by a IORING_POLL_ADD_MULTI are edge triggered. +*/ +poll_add :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, events: linux.Fd_Poll_Events, flags: linux.IO_Uring_Poll_Add_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .POLL_ADD + sqe.fd = fd + sqe.poll_events = events + sqe.poll_flags = flags + sqe.user_data = user_data + + ok = true + return +} + +/* +Remove an existing poll request. + +If found, the res field of the struct io_uring_cqe will contain 0. +If not found, res will contain -ENOENT, or -EALREADY if the poll request was in the process of completing already. +*/ +poll_remove :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, events: linux.Fd_Poll_Events) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .POLL_REMOVE + sqe.fd = fd + sqe.poll_events = events + sqe.user_data = user_data + + ok = true + return +} + +/* +Update the events of an existing poll request. + +The request will update an existing poll request with the mask of events passed in with this request. +The lookup is based on the user_data field of the original SQE submitted. + +Updating an existing poll is available since 5.13. +*/ +poll_update_events :: proc(ring: ^Ring, user_data: u64, orig_user_data: u64, fd: linux.Fd, events: linux.Fd_Poll_Events) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .POLL_REMOVE + sqe.fd = fd + sqe.addr = orig_user_data + sqe.poll_events = events + sqe.user_data = user_data + sqe.poll_flags = {.UPDATE_EVENTS} + + ok = true + return +} + +/* +Update the user data of an existing poll request. + +The request will update the user_data of an existing poll request based on the value passed. + +Updating an existing poll is available since 5.13. +*/ +poll_update_user_data :: proc(ring: ^Ring, user_data: u64, orig_user_data: u64, new_user_data: u64, fd: linux.Fd) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .POLL_REMOVE + sqe.fd = fd + sqe.off = orig_user_data + sqe.addr = new_user_data + sqe.user_data = user_data + sqe.poll_flags = {.UPDATE_USER_DATA} + + ok = true + return +} + +/* +Add, remove or modify entries in the interest list of epoll(7). + +See epoll_ctl(2) for details of the system call. + +Available since 5.6. +*/ +epoll_ctl :: proc(ring: ^Ring, user_data: u64, epfd: linux.Fd, op: linux.EPoll_Ctl_Opcode, fd: linux.Fd, event: ^linux.EPoll_Event) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .EPOLL_CTL + sqe.fd = epfd + sqe.off = u64(fd) + sqe.epoll_ctl_op = op + sqe.addr = cast(u64)uintptr(event) + sqe.user_data = user_data + + ok = true + return +} + +sync_file_range :: proc() { + unimplemented() +} + +/* +Issue the equivalent of a sendmsg(2) system call. + +See also sendmsg(2) for the general description of the related system call. + +poll_first: if set, uring will assume the socket is currently full and attempting to send data will be unsuccessful. +For this case, uring will arm internal poll and trigger a send of the data when there is enough space available. +This initial send attempt can be wasteful for the case where the socket is expected to be full, setting this flag will +bypass the initial send attempt and go straight to arming poll. +If poll does indicate that data can be sent, the operation will proceed. + +Available since 5.3. +*/ +sendmsg :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, msghdr: ^linux.Msg_Hdr, flags: linux.Socket_Msg, poll_first := false) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .SENDMSG + sqe.fd = fd + sqe.addr = cast(u64)uintptr(msghdr) + sqe.msg_flags = flags + sqe.user_data = user_data + sqe.sq_send_recv_flags = {.RECVSEND_POLL_FIRST} if poll_first else {} + + ok = true + return +} + +/* +Works just like sendmsg, but receives instead of sends. + +poll_first: If set, uring will assume the socket is currently empty and attempting to receive data will be unsuccessful. +For this case, uring will arm internal poll and trigger a receive of the data when the socket has data to be read. +This initial receive attempt can be wasteful for the case where the socket is expected to be empty, setting this flag will bypass the initial receive attempt and go straight to arming poll. +If poll does indicate that data is ready to be received, the operation will proceed. + +Available since 5.3. +*/ +recvmsg :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, msghdr: ^linux.Msg_Hdr, flags: linux.Socket_Msg, poll_first := false) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .RECVMSG + sqe.fd = fd + sqe.addr = cast(u64)uintptr(msghdr) + sqe.msg_flags = flags + sqe.user_data = user_data + sqe.sq_send_recv_flags = {.RECVSEND_POLL_FIRST} if poll_first else {} + + ok = true + return +} + +/* +Issue the equivalent of a send(2) system call. + +See also send(2) for the general description of the related system call. + +poll_first: If set, uring will assume the socket is currently full and attempting to send data will be unsuccessful. +For this case, uring will arm internal poll and trigger a send of the data when there is enough space available. +This initial send attempt can be wasteful for the case where the socket is expected to be full, setting this flag will bypass the initial send attempt and go straight to arming poll. +If poll does indicate that data can be sent, the operation will proceed. + +Available since 5.6. +*/ +send :: proc(ring: ^Ring, user_data: u64, sockfd: linux.Fd, buf: []byte, flags: linux.Socket_Msg, poll_first := false) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .SEND + sqe.fd = sockfd + sqe.addr = cast(u64)uintptr(raw_data(buf)) + sqe.len = u32(len(buf)) + sqe.msg_flags = flags + sqe.user_data = user_data + sqe.sq_send_recv_flags = {.RECVSEND_POLL_FIRST} if poll_first else {} + + ok = true + return +} + +sendto :: proc(ring: ^Ring, user_data: u64, sockfd: linux.Fd, buf: []byte, flags: linux.Socket_Msg, dest: ^$T, poll_first := false) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) + where T == linux.Sock_Addr_In || T == linux.Sock_Addr_In6 || T == linux.Sock_Addr_Un || T == linux.Sock_Addr_Any { + + sqe = send(ring, user_data, sockfd, buf, flags, poll_first) or_return + sqe.addr2 = u64(uintptr(dest)) + sqe.addr_len = u16(size_of(T)) + + ok = true + return +} + +/* +Works just like send, but receives instead of sends. + +poll_first: If set, uring will assume the socket is currently empty and attempting to receive data will be unsuccessful. +For this case, uring will arm internal poll and trigger a receive of the data when the socket has data to be read. +This initial receive attempt can be wasteful for the case where the socket is expected to be empty, setting this flag will bypass the initial receive attempt and go straight to arming poll. +If poll does indicate that data is ready to be received, the operation will proceed. + +Available since 5.6. +*/ +recv :: proc(ring: ^Ring, user_data: u64, sockfd: linux.Fd, buf: []byte, flags: linux.Socket_Msg, poll_first := false) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .RECV + sqe.fd = sockfd + sqe.addr = cast(u64)uintptr(raw_data(buf)) + sqe.len = cast(u32)uintptr(len(buf)) + sqe.msg_flags = flags + sqe.user_data = user_data + sqe.sq_send_recv_flags = {.RECVSEND_POLL_FIRST} if poll_first else {} + + ok = true + return +} + +/* +Register a timeout operation. + +The timeout will complete when either the timeout expires, or after the specified number of +events complete (if `count` is greater than `0`). + +`flags` may be `0` for a relative timeout, or `IORING_TIMEOUT_ABS` for an absolute timeout. + +The completion event result will be `-ETIME` if the timeout completed through expiration, +`0` if the timeout completed after the specified number of events, or `-ECANCELED` if the +timeout was removed before it expired. + +uring timeouts use the `CLOCK.MONOTONIC` clock source. +*/ +timeout :: proc(ring: ^Ring, user_data: u64, ts: ^linux.Time_Spec, count: u32, flags: linux.IO_Uring_Timeout_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .TIMEOUT + sqe.fd = -1 + sqe.addr = cast(u64)uintptr(ts) + sqe.len = 1 + sqe.off = u64(count) + sqe.timeout_flags = flags + sqe.user_data = user_data + + ok = true + return +} + +/* +Rmove an existing timeout operation. + +The timeout is identified by it's `user_data`. + +The completion event result will be `0` if the timeout was found and cancelled successfully, +`-EBUSY` if the timeout was found but expiration was already in progress, or +`-ENOENT` if the timeout was not found. +*/ +timeout_remove :: proc(ring: ^Ring, user_data: u64, timeout_user_data: u64, flags: linux.IO_Uring_Timeout_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .TIMEOUT_REMOVE + sqe.fd = -1 + sqe.addr = timeout_user_data + sqe.timeout_flags = flags + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of an accept4(2) system call. + +See also accept4(2) for the general description of the related system call. + +If the file_index field is set to a positive number, the file won't be installed into the normal file table as usual +but will be placed into the fixed file table at index file_index - 1. +In this case, instead of returning a file descriptor, the result will contain either 0 on success or an error. +If the index points to a valid empty slot, the installation is guaranteed to not fail. +If there is already a file in the slot, it will be replaced, similar to IORING_OP_FILES_UPDATE. +Please note that only uring has access to such files and no other syscall can use them. See IOSQE_FIXED_FILE and IORING_REGISTER_FILES. + +Available since 5.5. +*/ +accept :: proc(ring: ^Ring, user_data: u64, sockfd: linux.Fd, addr: ^$T, addr_len: ^i32, flags: linux.Socket_FD_Flags, file_index: u32 = 0) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) +where T == linux.Sock_Addr_In || T == linux.Sock_Addr_In6 || T == linux.Sock_Addr_Un || T == linux.Sock_Addr_Any { + + sqe = get_sqe(ring) or_return + sqe.opcode = .ACCEPT + sqe.fd = sockfd + sqe.addr = cast(u64)uintptr(addr) + sqe.off = cast(u64)uintptr(addr_len) + sqe.accept_flags = flags + sqe.user_data = user_data + sqe.file_index = file_index + + ok = true + return +} + +/* +Attempt to cancel an already issued request. + +The request is identified by it's user data. + +The cancelation request will complete with one of the following results codes. + +If found, the res field of the cqe will contain 0. +If not found, res will contain -ENOENT. + +If found and attempted canceled, the res field will contain -EALREADY. +In this case, the request may or may not terminate. +In general, requests that are interruptible (like socket IO) will get canceled, while disk IO requests cannot be canceled if already started. + +Available since 5.5. +*/ +async_cancel :: proc(ring: ^Ring, orig_user_data: u64, user_data: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .ASYNC_CANCEL + sqe.addr = orig_user_data + sqe.user_data = user_data + + ok = true + return +} + +/* +Adds a link timeout operation. + +You need to set linux.IOSQE_IO_LINK to flags of the target operation +and then call this method right after the target operation. +See https://lwn.net/Articles/803932/ for detail. + +If the dependent request finishes before the linked timeout, the timeout +is canceled. If the timeout finishes before the dependent request, the +dependent request will be canceled. + +The completion event result of the link_timeout will be +`-ETIME` if the timeout finishes before the dependent request +(in this case, the completion event result of the dependent request will +be `-ECANCELED`), or +`-EALREADY` if the dependent request finishes before the linked timeout. + +Available since 5.5. +*/ +link_timeout :: proc(ring: ^Ring, user_data: u64, ts: ^linux.Time_Spec, flags: linux.IO_Uring_Timeout_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring, 0) or_return + sqe.opcode = .LINK_TIMEOUT + sqe.fd = -1 + sqe.addr = cast(u64)uintptr(ts) + sqe.len = 1 + sqe.timeout_flags = flags + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of a connect(2) system call. + +See also connect(2) for the general description of the related system call. + +Available since 5.5. +*/ +connect :: proc(ring: ^Ring, user_data: u64, sockfd: linux.Fd, addr: ^$T) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) +where T == linux.Sock_Addr_In || T == linux.Sock_Addr_In6 || T == linux.Sock_Addr_Un || T == linux.Sock_Addr_Any { + + sqe = get_sqe(ring) or_return + sqe.opcode = .CONNECT + sqe.fd = sockfd + sqe.addr = cast(u64)uintptr(addr) + sqe.off = size_of(T) + sqe.user_data = user_data + + ok = true + return +} + +fallocate :: proc() { + unimplemented() +} + +fadvise :: proc() { + unimplemented() +} + +/* +Issue the equivalent of a madvise(2) system call. + +See also madvise(2) for the general description of the related system call. + +Available since 5.6. +*/ +madvise :: proc(ring: ^Ring, user_data: u64, addr: rawptr, size: u32, advise: linux.MAdvice) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .MADVISE + sqe.addr = u64(uintptr(addr)) + sqe.len = size + sqe.fadvise_advice = cast(u32)transmute(int)advise + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of a openat(2) system call. + +See also openat(2) for the general description of the related system call. + +Available since 5.6. + +If the file_index is set to a positive number, +the file won't be installed into the normal file table as usual but will be placed into the fixed file table at index file_index - 1. +In this case, instead of returning a file descriptor, the result will contain either 0 on success or an error. +If the index points to a valid empty slot, the installation is guaranteed to not fail. +If there is already a file in the slot, it will be replaced, similar to IORING_OP_FILES_UPDATE. +Please note that only uring has access to such files and no other syscall can use them. +See IOSQE_FIXED_FILE and IORING_REGISTER_FILES. + +Available since 5.15. +*/ +openat :: proc(ring: ^Ring, user_data: u64, dirfd: linux.Fd, path: cstring, mode: linux.Mode, flags: linux.Open_Flags, file_index: u32 = 0) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .OPENAT + sqe.fd = dirfd + sqe.addr = cast(u64)transmute(uintptr)path + sqe.len = transmute(u32)mode + sqe.open_flags = flags + sqe.user_data = user_data + sqe.file_index = file_index + + ok = true + return +} + +openat2 :: proc() { + unimplemented() +} + +/* +Issue the equivalent of a close(2) system call. + +See also close(2) for the general description of the related system call. + +Available since 5.6. + +If the file_index field is set to a positive number, this command can be used to close files that were +direct opened through IORING_OP_OPENAT, IORING_OP_OPENAT2, or IORING_OP_ACCEPT using the uring specific direct descriptors. +Note that only one of the descriptor fields may be set. +The direct close feature is available since the 5.15 kernel, where direct descriptors were introduced. +*/ +close :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, file_index: u32 = 0) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .CLOSE + sqe.fd = fd + sqe.user_data = user_data + sqe.file_index = file_index + + ok = true + return +} + +/* +Issue the equivalent of a statx(2) system call. + +See also statx(2) for the general description of the related system call. + +Available since 5.6. +*/ +statx :: proc(ring: ^Ring, user_data: u64, dirfd: linux.Fd, pathname: cstring, flags: linux.FD_Flags, mask: linux.Statx_Mask, buf: ^linux.Statx) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .STATX + sqe.fd = dirfd + sqe.addr = cast(u64)transmute(uintptr)pathname + sqe.statx_flags = flags + sqe.statx_mask = mask + sqe.statx = buf + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of a pread(2) system call. + +If offset is set to -1 , the offset will use (and advance) the file position, like the read(2) system calls. +These are non-vectored versions of the IORING_OP_READV and IORING_OP_WRITEV opcodes. +See also read(2) for the general description of the related system call. + +Available since 5.6. +*/ +read :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, buf: []u8, offset: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .READ + sqe.fd = fd + sqe.addr = cast(u64)uintptr(raw_data(buf)) + sqe.len = u32(len(buf)) + sqe.off = offset + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of a pwrite(2) system call. + +If offset is set to -1 , the offset will use (and advance) the file position, like the read(2) system calls. +These are non-vectored versions of the IORING_OP_READV and IORING_OP_WRITEV opcodes. +See also write(2) for the general description of the related system call. + +Available since 5.6. +*/ +write :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, buf: []u8, offset: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .WRITE + sqe.fd = fd + sqe.addr = cast(u64)uintptr(raw_data(buf)) + sqe.len = u32(len(buf)) + sqe.off = offset + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of a splice(2) system call. + +A sentinel value of -1 is used to pass the equivalent of a NULL for the offsets to splice(2). + +Please note that one of the file descriptors must refer to a pipe. +See also splice(2) for the general description of the related system call. + +Available since 5.7. + +*/ +splice :: proc(ring: ^Ring, user_data: u64, fd_in: linux.Fd, off_in: i64, fd_out: linux.Fd, off_out: i64, len: u32, flags: linux.IO_Uring_Splice_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .SPLICE + sqe.splice_fd_in = fd_in + sqe.splice_off_in = cast(u64)off_in + sqe.fd = fd_out + sqe.off = cast(u64)off_out + sqe.len = len + sqe.splice_flags = flags + sqe.user_data = user_data + + ok = true + return +} + +/* +Issue the equivalent of a tee(2) system call. + +Please note that both of the file descriptors must refer to a pipe. +See also tee(2) for the general description of the related system call. + +Available since 5.8. +*/ +tee :: proc(ring: ^Ring, user_data: u64, fd_in: linux.Fd, fd_out: linux.Fd, len: u32, flags: linux.IO_Uring_Splice_Flags) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .TEE + sqe.splice_fd_in = fd_in + sqe.fd = fd_out + sqe.len = len + sqe.splice_flags = flags + sqe.user_data = user_data + + ok = true + return +} + +/* +This command is an alternative to using IORING_REGISTER_FILES_UPDATE which then works in an async fashion, like the rest of the uring commands. + +Note that the array of file descriptors pointed to in addr must remain valid until this operation has completed. + +Available since 5.6. +*/ +files_update :: proc(ring: ^Ring, user_data: u64, fds: []linux.Fd, off: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .FILES_UPDATE + sqe.addr = cast(u64)uintptr(raw_data(fds)) + sqe.len = cast(u32)len(fds) + sqe.off = off + sqe.user_data = user_data + + ok = true + return +} + +provide_buffers :: proc() { + unimplemented() +} + +remove_buffers :: proc() { + unimplemented() +} + +/* +Issue the equivalent of a shutdown(2) system call. + +Available since 5.11. +*/ +shutdown :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, how: linux.Shutdown_How) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .SHUTDOWN + sqe.fd = fd + sqe.shutdown_how = how + sqe.user_data = user_data + + ok = true + return +} + +renameat :: proc() { + unimplemented() +} + +unlinkat :: proc() { + unimplemented() +} + +mkdirat :: proc() { + unimplemented() +} + +symlinkat :: proc() { + unimplemented() +} + +linkat :: proc() { + unimplemented() +} + +msg_ring :: proc() { + unimplemented() +} + +/* +Issue the equivalent of a socket(2) system call. + +See also socket(2) for the general description of the related system call. + +Available since 5.19. + +If the file_index field is set to a positive number, the file won't be installed into the normal file +table as usual but will be placed into the fixed file table at index file_index - 1. +In this case, instead of returning a file descriptor, the result will contain either 0 on success or an error. +If the index points to a valid empty slot, the installation is guaranteed to not fail. +If there is already a file in the slot, it will be replaced, similar to IORING_OP_FILES_UPDATE. +Please note that only uring has access to such files and no other syscall can use them. +See IOSQE_FIXED_FILE and IORING_REGISTER_FILES. +*/ +socket :: proc(ring: ^Ring, user_data: u64, domain: linux.Address_Family, socktype: linux.Socket_Type, protocol: linux.Protocol, file_index: u32 = 0) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .SOCKET + sqe.user_data = user_data + sqe.fd = cast(linux.Fd)domain + sqe.off = cast(u64)socktype + sqe.len = cast(u32)protocol + sqe.rw_flags = {} + sqe.file_index = file_index + + ok = true + return +} + +uring_cmd :: proc() { + unimplemented() +} + +send_zc :: proc() { + unimplemented() +} + +sendmsg_zc :: proc() { + unimplemented() +} + +waitid :: proc() { + unimplemented() +} + +setxattr :: proc() { + unimplemented() +} + +getxattr :: proc() { + unimplemented() +} + +fsetxattr :: proc() { + unimplemented() +} + +fgetxattr :: proc() { + unimplemented() +} + +/* +Issues the equivalent of the bind(2) system call. + +Available since 6.11. +*/ +bind :: proc(ring: ^Ring, user_data: u64, sock: linux.Fd, addr: ^$T) -> (sqe: linux.IO_Uring_SQE, ok: bool) + where + T == linux.Sock_Addr_In || + T == linux.Sock_Addr_In6 || + T == linux.Sock_Addr_Un || + T == linux.Sock_Addr_Any +{ + sqe = get_sqe(ring) or_return + sqe.opcode = .BIND + sqe.user_data = user_data + sqe.fd = sock + sqe.addr = cast(u64)uintptr(addr) + sqe.addr2 = size_of(T) + + ok = true + return +} + +/* +Issues the equivalent of the listen(2) system call. + +fd must contain the file descriptor of the socket and addr must contain the backlog parameter, i.e. the maximum amount of pending queued connections. + +Available since 6.11. +*/ +listen :: proc(ring: ^Ring, user_data: u64, fd: linux.Fd, backlog: u64) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sqe = get_sqe(ring) or_return + sqe.opcode = .LISTEN + sqe.user_data = user_data + sqe.fd = fd + sqe.addr = backlog + + ok = true + return +} + +ftruncate :: proc() { + unimplemented() +} + +read_multishot :: proc() { + unimplemented() +} + +futex_wait :: proc() { + unimplemented() +} + +futex_wake :: proc() { + unimplemented() +} + +futex_waitv :: proc() { + unimplemented() +} + +fixed_fd_install :: proc() { + unimplemented() +} + +fixed_file :: proc() { + unimplemented() +} diff --git a/core/sys/linux/uring/uring.odin b/core/sys/linux/uring/uring.odin new file mode 100644 index 000000000..8e4315e6a --- /dev/null +++ b/core/sys/linux/uring/uring.odin @@ -0,0 +1,294 @@ +package uring + +import "core:math" +import "core:sync" +import "core:sys/linux" + +DEFAULT_THREAD_IDLE_MS :: 1000 +DEFAULT_ENTRIES :: 32 +MAX_ENTRIES :: 4096 + +Ring :: struct { + fd: linux.Fd, + sq: Submission_Queue, + cq: Completion_Queue, + flags: linux.IO_Uring_Setup_Flags, + features: linux.IO_Uring_Features, +} + +DEFAULT_PARAMS :: linux.IO_Uring_Params { + sq_thread_idle = DEFAULT_THREAD_IDLE_MS, +} + +// Initialize and setup an uring, `entries` must be a power of 2 between 1 and 4096. +init :: proc(ring: ^Ring, params: ^linux.IO_Uring_Params, entries: u32 = DEFAULT_ENTRIES) -> (err: linux.Errno) { + assert(entries <= MAX_ENTRIES, "too many entries") + assert(entries != 0, "entries must be positive") + assert(math.is_power_of_two(int(entries)), "entries must be a power of two") + + fd := linux.io_uring_setup(entries, params) or_return + defer if err != nil { linux.close(fd) } + + if .SINGLE_MMAP not_in params.features { + // NOTE: Could support this, but currently isn't. + err = .ENOSYS + return + } + + assert(.CQE32 not_in params.flags, "unsupported flag") // NOTE: Could support this by making IO_Uring generic. + assert(.SQE128 not_in params.flags, "unsupported flag") // NOTE: Could support this by making IO_Uring generic. + + sq := submission_queue_make(fd, params) or_return + + ring.fd = fd + ring.sq = sq + ring.cq = completion_queue_make(fd, params, &sq) + ring.flags = params.flags + ring.features = params.features + + return +} + +destroy :: proc(ring: ^Ring) { + assert(ring.fd >= 0) + submission_queue_destroy(&ring.sq) + linux.close(ring.fd) + ring.fd = -1 +} + +// Returns a pointer to a vacant submission queue entry, or nil if the submission queue is full. +// NOTE: extra is so you can make sure there is space for related entries, defaults to 1 so +// a link timeout op can always be added after another. +get_sqe :: proc(ring: ^Ring, extra: int = 1) -> (sqe: ^linux.IO_Uring_SQE, ok: bool) { + sq := &ring.sq + head: u32 = sync.atomic_load_explicit(sq.head, .Acquire) + next := sq.sqe_tail + 1 + + if int(next - head) > len(sq.sqes)-extra { + sqe = nil + ok = false + return + } + + sqe = &sq.sqes[sq.sqe_tail & sq.mask] + sqe^ = {} + + sq.sqe_tail = next + ok = true + return +} + +free_space :: proc(ring: ^Ring) -> int { + sq := &ring.sq + head := sync.atomic_load_explicit(sq.head, .Acquire) + next := sq.sqe_tail + 1 + free := len(sq.sqes) - int(next - head) + assert(free >= 0) + return free +} + +// Sync internal state with kernel ring state on the submission queue side. +// Returns the number of all pending events in the submission queue. +// Rationale is to determine that an enter call is needed. +flush_sq :: proc(ring: ^Ring) -> (n_pending: u32) { + sq := &ring.sq + to_submit := sq.sqe_tail - sq.sqe_head + if to_submit != 0 { + tail := sq.tail^ + i: u32 = 0 + for ; i < to_submit; i += 1 { + sq.array[tail & sq.mask] = sq.sqe_head & sq.mask + tail += 1 + sq.sqe_head += 1 + } + sync.atomic_store_explicit(sq.tail, tail, .Release) + } + n_pending = sq_ready(ring) + return +} + +// Returns true if we are not using an SQ thread (thus nobody submits but us), +// or if IORING_SQ_NEED_WAKEUP is set and the SQ thread must be explicitly awakened. +// For the latter case, we set the SQ thread wakeup flag. +// Matches the implementation of sq_ring_needs_enter() in liburing. +sq_ring_needs_enter :: proc(ring: ^Ring, flags: ^linux.IO_Uring_Enter_Flags) -> bool { + assert(flags^ == {}) + if .SQPOLL not_in ring.flags { return true } + if .NEED_WAKEUP in sync.atomic_load_explicit(ring.sq.flags, .Relaxed) { + flags^ += {.SQ_WAKEUP} + return true + } + return false +} + + +// Submits the submission queue entries acquired via get_sqe(). +// Returns the number of entries submitted. +// Optionally wait for a number of events by setting `wait_nr`, and/or set a maximum wait time by setting `timeout`. +submit :: proc(ring: ^Ring, wait_nr: u32 = 0, timeout: ^linux.Time_Spec = nil) -> (n_submitted: u32, err: linux.Errno) { + n_submitted = flush_sq(ring) + flags: linux.IO_Uring_Enter_Flags + if sq_ring_needs_enter(ring, &flags) || wait_nr > 0 { + if wait_nr > 0 || .IOPOLL in ring.flags { + flags += {.GETEVENTS} + } + + flags += {.EXT_ARG} + ext: linux.IO_Uring_Getevents_Arg + ext.ts = timeout + + n_submitted_: int + n_submitted_, err = linux.io_uring_enter2(ring.fd, n_submitted, wait_nr, flags, &ext) + assert(n_submitted_ >= 0) + n_submitted = u32(n_submitted_) + } + return +} + +// Returns the number of submission queue entries in the submission queue. +sq_ready :: proc(ring: ^Ring) -> u32 { + // Always use the shared ring state (i.e. head and not sqe_head) to avoid going out of sync, + // see https://github.com/axboe/liburing/issues/92. + return ring.sq.sqe_tail - sync.atomic_load_explicit(ring.sq.head, .Acquire) +} + +// Returns the number of completion queue entries in the completion queue (yet to consume). +cq_ready :: proc(ring: ^Ring) -> (n_ready: u32) { + return sync.atomic_load_explicit(ring.cq.tail, .Acquire) - ring.cq.head^ +} + +// Copies as many CQEs as are ready, and that can fit into the destination `cqes` slice. +// If none are available, enters into the kernel to wait for at most `wait_nr` CQEs. +// Returns the number of CQEs copied, advancing the CQ ring. +// Provides all the wait/peek methods found in liburing, but with batching and a single method. +// TODO: allow for timeout. +copy_cqes :: proc(ring: ^Ring, cqes: []linux.IO_Uring_CQE, wait_nr: u32) -> (n_copied: u32, err: linux.Errno) { + n_copied = copy_cqes_ready(ring, cqes) + if n_copied > 0 { return } + if wait_nr > 0 || cq_ring_needs_flush(ring) { + _ = linux.io_uring_enter(ring.fd, 0, wait_nr, {.GETEVENTS}, nil) or_return + n_copied = copy_cqes_ready(ring, cqes) + } + return +} + +copy_cqes_ready :: proc(ring: ^Ring, cqes: []linux.IO_Uring_CQE) -> (n_copied: u32) { + n_ready := cq_ready(ring) + n_copied = min(u32(len(cqes)), n_ready) + head := ring.cq.head^ + tail := head + n_copied + shift := u32(.CQE32 in ring.flags) + + i := 0 + for head != tail { + cqes[i] = ring.cq.cqes[(head & ring.cq.mask) << shift] + head += 1 + i += 1 + } + cq_advance(ring, n_copied) + return +} + +cq_ring_needs_flush :: proc(ring: ^Ring) -> bool { + return .CQ_OVERFLOW in sync.atomic_load_explicit(ring.sq.flags, .Relaxed) +} + +// For advanced use cases only that implement custom completion queue methods. +// If you use copy_cqes() or copy_cqe() you must not call cqe_seen() or cq_advance(). +// Must be called exactly once after a zero-copy CQE has been processed by your application. +// Not idempotent, calling more than once will result in other CQEs being lost. +// Matches the implementation of cqe_seen() in liburing. +cqe_seen :: proc(ring: ^Ring) { + cq_advance(ring, 1) +} + +// For advanced use cases only that implement custom completion queue methods. +// Matches the implementation of cq_advance() in liburing. +cq_advance :: proc(ring: ^Ring, count: u32) { + if count == 0 { return } + sync.atomic_store_explicit(ring.cq.head, ring.cq.head^ + count, .Release) +} + +Submission_Queue :: struct { + head: ^u32, + tail: ^u32, + mask: u32, + flags: ^linux.IO_Uring_Submission_Queue_Flags, + dropped: ^u32, + array: []u32, + sqes: []linux.IO_Uring_SQE, + mmap: []u8, + mmap_sqes: []u8, + + // We use `sqe_head` and `sqe_tail` in the same way as liburing: + // We increment `sqe_tail` (but not `tail`) for each call to `get_sqe()`. + // We then set `tail` to `sqe_tail` once, only when these events are actually submitted. + // This allows us to amortize the cost of the @atomicStore to `tail` across multiple SQEs. + sqe_head: u32, + sqe_tail: u32, +} + +submission_queue_make :: proc(fd: linux.Fd, params: ^linux.IO_Uring_Params) -> (sq: Submission_Queue, err: linux.Errno) { + assert(fd >= 0, "uninitialized queue fd") + assert(.SINGLE_MMAP in params.features, "unsupported feature") // NOTE: Could support this, but currently isn't. + + sq_size := params.sq_off.array + params.sq_entries * size_of(u32) + cq_size := params.cq_off.cqes + params.cq_entries * size_of(linux.IO_Uring_CQE) + size := max(sq_size, cq_size) + + // PERF: .POPULATE commits all pages right away, is that desired? + + cqe_map := cast([^]byte)(linux.mmap(0, uint(size), {.READ, .WRITE}, {.SHARED, .POPULATE}, fd, linux.IORING_OFF_SQ_RING) or_return) + defer if err != nil { linux.munmap(cqe_map, uint(size)) } + + size_sqes := params.sq_entries * size_of(linux.IO_Uring_SQE) + sqe_map := cast([^]byte)(linux.mmap(0, uint(size_sqes), {.READ, .WRITE}, {.SHARED, .POPULATE}, fd, linux.IORING_OFF_SQES) or_return) + + array := cast([^]u32)cqe_map[params.sq_off.array:] + sqes := cast([^]linux.IO_Uring_SQE)sqe_map + + sq.head = cast(^u32)&cqe_map[params.sq_off.head] + sq.tail = cast(^u32)&cqe_map[params.sq_off.tail] + sq.mask = (cast(^u32)&cqe_map[params.sq_off.ring_mask])^ + sq.flags = cast(^linux.IO_Uring_Submission_Queue_Flags)&cqe_map[params.sq_off.flags] + sq.dropped = cast(^u32)&cqe_map[params.sq_off.dropped] + sq.array = array[:params.sq_entries] + sq.sqes = sqes[:params.sq_entries] + sq.mmap = cqe_map[:size] + sq.mmap_sqes = sqe_map[:size_sqes] + + return +} + +submission_queue_destroy :: proc(sq: ^Submission_Queue) -> (err: linux.Errno) { + err = linux.munmap(raw_data(sq.mmap), uint(len(sq.mmap))) + err2 := linux.munmap(raw_data(sq.mmap_sqes), uint(len(sq.mmap_sqes))) + if err == nil { err = err2 } + return +} + +Completion_Queue :: struct { + head: ^u32, + tail: ^u32, + mask: u32, + overflow: ^u32, + cqes: []linux.IO_Uring_CQE, +} + +completion_queue_make :: proc(fd: linux.Fd, params: ^linux.IO_Uring_Params, sq: ^Submission_Queue) -> Completion_Queue { + assert(fd >= 0, "uninitialized queue fd") + assert(.SINGLE_MMAP in params.features, "required feature SINGLE_MMAP not supported") + + mmap := sq.mmap + cqes := cast([^]linux.IO_Uring_CQE)&mmap[params.cq_off.cqes] + + return( + { + head = cast(^u32)&mmap[params.cq_off.head], + tail = cast(^u32)&mmap[params.cq_off.tail], + mask = (cast(^u32)&mmap[params.cq_off.ring_mask])^, + overflow = cast(^u32)&mmap[params.cq_off.overflow], + cqes = cqes[:params.cq_entries], + } \ + ) +} diff --git a/core/sys/posix/fcntl.odin b/core/sys/posix/fcntl.odin index db095c418..52d97f528 100644 --- a/core/sys/posix/fcntl.odin +++ b/core/sys/posix/fcntl.odin @@ -70,7 +70,7 @@ foreign lib { [[ More; https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html ]] */ - openat :: proc(fd: FD, path: cstring, flags: O_Flags, mode: mode_t = {}) -> FD --- + openat :: proc(fd: FD, path: cstring, flags: O_Flags, #c_vararg mode: ..mode_t) -> FD --- } FCNTL_Cmd :: enum c.int { diff --git a/core/sys/windows/kernel32.odin b/core/sys/windows/kernel32.odin index 0ad11121e..dc76cb037 100644 --- a/core/sys/windows/kernel32.odin +++ b/core/sys/windows/kernel32.odin @@ -30,6 +30,16 @@ EV_RXCHAR :: DWORD(0x0001) EV_RXFLAG :: DWORD(0x0002) EV_TXEMPTY :: DWORD(0x0004) +WAITORTIMERCALLBACK :: #type proc "system" (lpParameter: PVOID, TimerOrWaitFired: BOOLEAN) + +WT_EXECUTEDEFAULT :: 0x00000000 +WT_EXECUTEINIOTHREAD :: 0x00000001 +WT_EXECUTEINPERSISTENTTHREAD :: 0x00000080 +WT_EXECUTEINWAITTHREAD :: 0x00000004 +WT_EXECUTELONGFUNCTION :: 0x00000010 +WT_EXECUTEONLYONCE :: 0x00000008 +WT_TRANSFER_IMPERSONATION :: 0x00000100 + @(default_calling_convention="system") foreign kernel32 { OutputDebugStringA :: proc(lpOutputString: LPCSTR) --- // The only A thing that is allowed @@ -567,7 +577,7 @@ foreign kernel32 { // [MS-Docs](https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatusex) GetQueuedCompletionStatusEx :: proc(CompletionPort: HANDLE, lpCompletionPortEntries: ^OVERLAPPED_ENTRY, ulCount: c_ulong, ulNumEntriesRemoved: ^c_ulong, dwMilliseconds: DWORD, fAlertable: BOOL) -> BOOL --- // [MS-Docs](https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-postqueuedcompletionstatus) - PostQueuedCompletionStatus :: proc(CompletionPort: HANDLE, dwNumberOfBytesTransferred: DWORD, dwCompletionKey: c_ulong, lpOverlapped: ^OVERLAPPED) -> BOOL --- + PostQueuedCompletionStatus :: proc(CompletionPort: HANDLE, dwNumberOfBytesTransferred: DWORD, dwCompletionKey: ULONG_PTR, lpOverlapped: ^OVERLAPPED) -> BOOL --- // [MS-Docs](https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-gethandleinformation) GetHandleInformation :: proc(hObject: HANDLE, lpdwFlags: ^DWORD) -> BOOL --- @@ -575,6 +585,17 @@ foreign kernel32 { RtlNtStatusToDosError :: proc(status: NTSTATUS) -> ULONG --- GetSystemPowerStatus :: proc(lpSystemPowerStatus: ^SYSTEM_POWER_STATUS) -> BOOL --- + + RegisterWaitForSingleObject :: proc( + phNewWaitObject: PHANDLE, + hObject: HANDLE, + Callback: WAITORTIMERCALLBACK, + Context: PVOID, + dwMilliseconds: ULONG, + dwFlags: ULONG, + ) -> BOOL --- + + UnregisterWaitEx :: proc(WaitHandle: HANDLE, CompletionEvent: HANDLE) -> BOOL --- } DEBUG_PROCESS :: 0x00000001 diff --git a/core/sys/windows/mswsock.odin b/core/sys/windows/mswsock.odin new file mode 100644 index 000000000..9019fc821 --- /dev/null +++ b/core/sys/windows/mswsock.odin @@ -0,0 +1,40 @@ +#+build windows +package sys_windows + +foreign import mswsock "system:mswsock.lib" + +foreign mswsock { + TransmitFile :: proc( + hSocket: SOCKET, + hFile: HANDLE, + nNumberOfBytesToWrite: DWORD, + nNumberOfBytesPerSend: DWORD, + lpOverlapped: LPOVERLAPPED, + lpTransmitBuffers: rawptr, + dwReserved: DWORD, + ) -> BOOL --- + + AcceptEx :: proc( + sListenSocket: SOCKET, + sAcceptSocket: SOCKET, + lpOutputBuffer: PVOID, + dwReceiveDataLength: DWORD, + dwLocalAddressLength: DWORD, + dwRemoteAddressLength: DWORD, + lpdwBytesReceived: LPDWORD, + lpOverlapped: LPOVERLAPPED, + ) -> BOOL --- + + GetAcceptExSockaddrs :: proc( + lpOutputBuffer: PVOID, + dwReceiveDataLength: DWORD, + dwLocalAddressLength: DWORD, + dwRemoteAddressLength: DWORD, + LocalSockaddr: ^^sockaddr, + LocalSockaddrLength: LPINT, + RemoteSockaddr: ^^sockaddr, + RemoteSockaddrLength: LPINT, + ) --- +} + +SO_UPDATE_CONNECT_CONTEXT :: 0x7010
\ No newline at end of file diff --git a/core/sys/windows/ntdll.odin b/core/sys/windows/ntdll.odin index 747130749..8362bb9df 100644 --- a/core/sys/windows/ntdll.odin +++ b/core/sys/windows/ntdll.odin @@ -36,6 +36,20 @@ foreign ntdll_lib { QueryFlags: ULONG, FileName : PUNICODE_STRING, ) -> NTSTATUS --- + + NtCreateFile :: proc( + FileHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + IoStatusBlock: PIO_STATUS_BLOCK, + AllocationSize: PLARGE_INTEGER, + FileAttributes: ULONG, + ShareAccess: ULONG, + CreateDisposition: ULONG, + CreateOptions: ULONG, + EaBuffer: PVOID, + EaLength: ULONG, + ) -> NTSTATUS --- } @@ -256,4 +270,13 @@ RTL_DRIVE_LETTER_CURDIR :: struct { LIST_ENTRY :: struct { Flink: ^LIST_ENTRY, Blink: ^LIST_ENTRY, -}
\ No newline at end of file +} + +FILE_SUPERSEDE :: 0x00000000 +FILE_OPEN :: 0x00000001 +FILE_CREATE :: 0x00000002 +FILE_OPEN_IF :: 0x00000003 +FILE_OVERWRITE :: 0x00000004 +FILE_OVERWRITE_IF :: 0x00000005 + +FILE_NON_DIRECTORY_FILE :: 0x00000040 diff --git a/core/sys/windows/types.odin b/core/sys/windows/types.odin index e1a14fb94..0d8854724 100644 --- a/core/sys/windows/types.odin +++ b/core/sys/windows/types.odin @@ -3139,6 +3139,7 @@ OBJECT_ATTRIBUTES :: struct { SecurityDescriptor: rawptr, SecurityQualityOfService: rawptr, } +POBJECT_ATTRIBUTES :: ^OBJECT_ATTRIBUTES PUNICODE_STRING :: ^UNICODE_STRING UNICODE_STRING :: struct { @@ -3150,9 +3151,14 @@ UNICODE_STRING :: struct { OVERLAPPED :: struct { Internal: ^c_ulong, InternalHigh: ^c_ulong, - Offset: DWORD, - OffsetHigh: DWORD, - hEvent: HANDLE, + using _: struct #raw_union { + using _: struct { + Offset: DWORD, + OffsetHigh: DWORD, + }, + OffsetFull: u64, // Convenience field to set Offset and OffsetHigh with one value. + }, + hEvent: HANDLE, } OVERLAPPED_ENTRY :: struct { diff --git a/core/sys/windows/winerror.odin b/core/sys/windows/winerror.odin index 23467761d..2807ca0f8 100644 --- a/core/sys/windows/winerror.odin +++ b/core/sys/windows/winerror.odin @@ -248,6 +248,13 @@ E_HANDLE :: 0x80070006 // Handle that is not valid E_OUTOFMEMORY :: 0x8007000E // Failed to allocate necessary memory E_INVALIDARG :: 0x80070057 // One or more arguments are not valid +SEC_E_INCOMPLETE_MESSAGE :: 0x80090318 + +SEC_I_INCOMPLETE_CREDENTIALS :: 0x00090320 +SEC_I_CONTINUE_NEEDED :: 0x00090312 +SEC_I_CONTEXT_EXPIRED :: 0x00090317 +SEC_I_RENEGOTIATE :: 0x00090321 + // Severity values SEVERITY :: enum DWORD { SUCCESS = 0, diff --git a/core/sys/windows/ws2_32.odin b/core/sys/windows/ws2_32.odin index ad9089c6e..77a288d6f 100644 --- a/core/sys/windows/ws2_32.odin +++ b/core/sys/windows/ws2_32.odin @@ -38,7 +38,7 @@ WSANETWORKEVENTS :: struct { WSAID_ACCEPTEX :: GUID{0xb5367df1, 0xcbac, 0x11cf, {0x95, 0xca, 0x00, 0x80, 0x5f, 0x48, 0xa1, 0x92}} WSAID_GETACCEPTEXSOCKADDRS :: GUID{0xb5367df2, 0xcbac, 0x11cf, {0x95, 0xca, 0x00, 0x80, 0x5f, 0x48, 0xa1, 0x92}} -WSAID_CONNECTX :: GUID{0x25a207b9, 0xddf3, 0x4660, {0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}} +WSAID_CONNECTEX :: GUID{0x25a207b9, 0xddf3, 0x4660, {0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}} SIO_GET_EXTENSION_FUNCTION_POINTER :: IOC_INOUT | IOC_WS2 | 6 SIO_UDP_CONNRESET :: IOC_IN | IOC_VENDOR | 12 @@ -129,7 +129,7 @@ foreign ws2_32 { dwBufferCount: DWORD, lpNumberOfBytesSent: LPDWORD, dwFlags: DWORD, - lpTo: ^SOCKADDR_STORAGE_LH, + lpTo: ^sockaddr, iToLen: c_int, lpOverlapped: LPWSAOVERLAPPED, lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE, @@ -151,8 +151,8 @@ foreign ws2_32 { dwBufferCount: DWORD, lpNumberOfBytesRecvd: LPDWORD, lpFlags: LPDWORD, - lpFrom: ^SOCKADDR_STORAGE_LH, - lpFromlen: ^c_int, + lpFrom: ^sockaddr, + lpFromlen: LPINT, lpOverlapped: LPWSAOVERLAPPED, lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE, ) -> c_int --- diff --git a/core/thread/thread.odin b/core/thread/thread.odin index 26c1a3e27..a07801b98 100644 --- a/core/thread/thread.odin +++ b/core/thread/thread.odin @@ -5,7 +5,10 @@ import "base:runtime" import "core:mem" import "base:intrinsics" -_ :: intrinsics +@(private) +unall :: intrinsics.unaligned_load +@(private) +unals :: intrinsics.unaligned_store /* Value, specifying whether `core:thread` functionality is available on the @@ -347,7 +350,9 @@ create_and_start_with_poly_data :: proc(data: $T, fn: proc(data: T), init_contex thread_proc :: proc(t: ^Thread) { fn := cast(proc(T))t.data assert(t.user_index >= 1) - data := (^T)(&t.user_args[0])^ + + data := unall((^T)(&t.user_args)) + fn(data) } if t = create(thread_proc, priority); t == nil { @@ -356,9 +361,7 @@ create_and_start_with_poly_data :: proc(data: $T, fn: proc(data: T), init_contex t.data = rawptr(fn) t.user_index = 1 - data := data - - mem.copy(&t.user_args[0], &data, size_of(T)) + unals((^T)(&t.user_args), data) if self_cleanup { intrinsics.atomic_or(&t.flags, {.Self_Cleanup}) @@ -393,9 +396,10 @@ create_and_start_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), fn := cast(proc(T1, T2))t.data assert(t.user_index >= 2) - user_args := mem.slice_to_bytes(t.user_args[:]) - arg1 := (^T1)(raw_data(user_args))^ - arg2 := (^T2)(raw_data(user_args[size_of(T1):]))^ + ptr := uintptr(&t.user_args) + + arg1 := unall((^T1)(rawptr(ptr))) + arg2 := unall((^T2)(rawptr(ptr + size_of(T1)))) fn(arg1, arg2) } @@ -405,11 +409,10 @@ create_and_start_with_poly_data2 :: proc(arg1: $T1, arg2: $T2, fn: proc(T1, T2), t.data = rawptr(fn) t.user_index = 2 - arg1, arg2 := arg1, arg2 - user_args := mem.slice_to_bytes(t.user_args[:]) + ptr := uintptr(&t.user_args) - n := copy(user_args, mem.ptr_to_bytes(&arg1)) - _ = copy(user_args[n:], mem.ptr_to_bytes(&arg2)) + unals((^T1)(rawptr(ptr)), arg1) + unals((^T2)(rawptr(ptr + size_of(T1))), arg2) if self_cleanup { intrinsics.atomic_or(&t.flags, {.Self_Cleanup}) @@ -444,10 +447,11 @@ create_and_start_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: pr fn := cast(proc(T1, T2, T3))t.data assert(t.user_index >= 3) - user_args := mem.slice_to_bytes(t.user_args[:]) - arg1 := (^T1)(raw_data(user_args))^ - arg2 := (^T2)(raw_data(user_args[size_of(T1):]))^ - arg3 := (^T3)(raw_data(user_args[size_of(T1) + size_of(T2):]))^ + ptr := uintptr(&t.user_args) + + arg1 := unall((^T1)(rawptr(ptr))) + arg2 := unall((^T2)(rawptr(ptr + size_of(T1)))) + arg3 := unall((^T3)(rawptr(ptr + size_of(T1) + size_of(T2)))) fn(arg1, arg2, arg3) } @@ -457,12 +461,11 @@ create_and_start_with_poly_data3 :: proc(arg1: $T1, arg2: $T2, arg3: $T3, fn: pr t.data = rawptr(fn) t.user_index = 3 - arg1, arg2, arg3 := arg1, arg2, arg3 - user_args := mem.slice_to_bytes(t.user_args[:]) + ptr := uintptr(&t.user_args) - n := copy(user_args, mem.ptr_to_bytes(&arg1)) - n += copy(user_args[n:], mem.ptr_to_bytes(&arg2)) - _ = copy(user_args[n:], mem.ptr_to_bytes(&arg3)) + unals((^T1)(rawptr(ptr)), arg1) + unals((^T2)(rawptr(ptr + size_of(T1))), arg2) + unals((^T3)(rawptr(ptr + size_of(T1) + size_of(T2))), arg3) if self_cleanup { intrinsics.atomic_or(&t.flags, {.Self_Cleanup}) diff --git a/examples/all/all_linux.odin b/examples/all/all_linux.odin index 2b70fa1e1..d7d005ba0 100644 --- a/examples/all/all_linux.odin +++ b/examples/all/all_linux.odin @@ -2,4 +2,5 @@ package all @(require) import "core:sys/linux" -@(require) import "vendor:x11/xlib"
\ No newline at end of file +@(require) import "core:sys/linux/uring" +@(require) import "vendor:x11/xlib" diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index 7895b4640..9a7613ba5 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -17,6 +17,7 @@ package all @(require) import "core:container/avl" @(require) import "core:container/bit_array" +@(require) import "core:container/pool" @(require) import "core:container/priority_queue" @(require) import "core:container/queue" @(require) import "core:container/small_array" @@ -104,6 +105,8 @@ package all @(require) import "core:mem/tlsf" @(require) import "core:mem/virtual" +@(require) import "core:nbio" + @(require) import "core:odin/ast" @(require) import doc_format "core:odin/doc-format" @(require) import "core:odin/parser" @@ -156,4 +159,4 @@ package all @(require) import "core:unicode/utf8/utf8string" @(require) import "core:unicode/utf16" -main :: proc() {}
\ No newline at end of file +main :: proc() {} diff --git a/tests/core/container/test_core_rbtree.odin b/tests/core/container/test_core_rbtree.odin index d220b7ed6..78d710b7f 100644 --- a/tests/core/container/test_core_rbtree.odin +++ b/tests/core/container/test_core_rbtree.odin @@ -4,22 +4,15 @@ import rb "core:container/rbtree" import "core:math/rand" import "core:testing" import "base:intrinsics" -import "core:mem" import "core:slice" import "core:log" test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) { - track: mem.Tracking_Allocator - mem.tracking_allocator_init(&track, context.allocator) - track.bad_free_callback = mem.tracking_allocator_bad_free_callback_add_to_array - defer mem.tracking_allocator_destroy(&track) - context.allocator = mem.tracking_allocator(&track) - log.infof("Testing Red-Black Tree($Key=%v,$Value=%v) using random seed %v.", type_info_of(Key), type_info_of(Value), t.seed) tree: rb.Tree(Key, Value) rb.init(&tree) - testing.expect(t, rb.len(&tree) == 0, "empty: len should be 0") + testing.expect(t, rb.len(tree) == 0, "empty: len should be 0") testing.expect(t, rb.first(&tree) == nil, "empty: first should be nil") testing.expect(t, rb.last(&tree) == nil, "empty: last should be nil") iter := rb.iterator(&tree, .Forward) @@ -48,7 +41,7 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) { } entry_count := len(inserted_map) - testing.expect(t, rb.len(&tree) == entry_count, "insert: len after") + testing.expect(t, rb.len(tree) == entry_count, "insert: len after") validate_rbtree(t, &tree) first := rb.first(&tree) @@ -58,8 +51,8 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) { // Ensure that all entries can be found. for k, v in inserted_map { - testing.expect(t, v == rb.find(&tree, k), "Find(): Node") - testing.expect(t, k == v.key, "Find(): Node key") + testing.expect(t, v == rb.find(tree, k), "Find(): Node") + testing.expect(t, k == v.key, "Find(): Node key") } // Test the forward/backward iterators. @@ -97,17 +90,17 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) { (^int)(user_data)^ -= 1 } for k, i in inserted_keys { - node := rb.find(&tree, k) + node := rb.find(tree, k) testing.expect(t, node != nil, "remove: find (pre)") ok := rb.remove(&tree, k) testing.expect(t, ok, "remove: succeeds") - testing.expect(t, entry_count - (i + 1) == rb.len(&tree), "remove: len (post)") + testing.expect(t, entry_count - (i + 1) == rb.len(tree), "remove: len (post)") validate_rbtree(t, &tree) - testing.expect(t, nil == rb.find(&tree, k), "remove: find (post") + testing.expect(t, nil == rb.find(tree, k), "remove: find (post") } - testing.expect(t, rb.len(&tree) == 0, "remove: len should be 0") + testing.expect(t, rb.len(tree) == 0, "remove: len should be 0") testing.expectf(t, callback_count == 0, "remove: on_remove should've been called %v times, it was %v", entry_count, callback_count) testing.expect(t, rb.first(&tree) == nil, "remove: first should be nil") testing.expect(t, rb.last(&tree) == nil, "remove: last should be nil") @@ -129,28 +122,25 @@ test_rbtree_integer :: proc(t: ^testing.T, $Key: typeid, $Value: typeid) { ok = rb.iterator_remove(&iter) testing.expect(t, !ok, "iterator/remove: redundant removes should fail") - testing.expect(t, rb.find(&tree, k) == nil, "iterator/remove: node should be gone") + testing.expect(t, rb.find(tree, k) == nil, "iterator/remove: node should be gone") testing.expect(t, rb.iterator_get(&iter) == nil, "iterator/remove: get should return nil") // Ensure that iterator_next still works. node, ok = rb.iterator_next(&iter) - testing.expect(t, ok == (rb.len(&tree) > 0), "iterator/remove: next should return false") + testing.expect(t, ok == (rb.len(tree) > 0), "iterator/remove: next should return false") testing.expect(t, node == rb.first(&tree), "iterator/remove: next should return first") validate_rbtree(t, &tree) } - testing.expect(t, rb.len(&tree) == entry_count - 1, "iterator/remove: len should drop by 1") + testing.expect(t, rb.len(tree) == entry_count - 1, "iterator/remove: len should drop by 1") rb.destroy(&tree) - testing.expect(t, rb.len(&tree) == 0, "destroy: len should be 0") + testing.expect(t, rb.len(tree) == 0, "destroy: len should be 0") testing.expectf(t, callback_count == 0, "remove: on_remove should've been called %v times, it was %v", entry_count, callback_count) // print_tree_node(tree._root) delete(inserted_map) delete(inserted_keys) - testing.expectf(t, len(track.allocation_map) == 0, "Expected 0 leaks, have %v", len(track.allocation_map)) - testing.expectf(t, len(track.bad_free_array) == 0, "Expected 0 bad frees, have %v", len(track.bad_free_array)) - return } @(test) diff --git a/tests/core/nbio/fs.odin b/tests/core/nbio/fs.odin new file mode 100644 index 000000000..6e079f96e --- /dev/null +++ b/tests/core/nbio/fs.odin @@ -0,0 +1,100 @@ +package tests_nbio + +import "core:nbio" +import "core:testing" +import "core:time" +import os "core:os/os2" + +@(test) +close_invalid_handle :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + nbio.close(max(nbio.Handle)) + + ev(t, nbio.run(), nil) + } +} + +@(test) +write_read_close :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + @static content := [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + @static result: [20]byte + + FILENAME :: "test_write_read_close" + + nbio.open_poly(FILENAME, t, on_open, mode={.Read, .Write, .Create, .Trunc}) + + on_open :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.open.err, nil) + + nbio.write_poly(op.open.handle, 0, content[:], t, on_write) + } + + on_write :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.write.err, nil) + ev(t, op.write.written, len(content)) + + nbio.read_poly(op.write.handle, 0, result[:], t, on_read, all=true) + } + + on_read :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.read.err, nil) + ev(t, op.read.read, len(result)) + ev(t, result, content) + + nbio.close_poly(op.read.handle, t, on_close) + } + + on_close :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.close.err, nil) + os.remove(FILENAME) + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +read_empty_file :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + FILENAME :: "test_read_empty_file" + + handle, err := nbio.open_sync(FILENAME, mode={.Read, .Write, .Create, .Trunc}) + ev(t, err, nil) + + buf: [128]byte + nbio.read_poly(handle, 0, buf[:], t, proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.read.err, nbio.FS_Error.EOF) + ev(t, op.read.read, 0) + + nbio.close_poly(op.read.handle, t, proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.close.err, nil) + os.remove(FILENAME) + }) + }) + + ev(t, nbio.run(), nil) + } +} + +@(test) +read_entire_file :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + nbio.read_entire_file(#file, t, on_read) + + on_read :: proc(t: rawptr, data: []byte, err: nbio.Read_Entire_File_Error) { + t := (^testing.T)(t) + ev(t, err.value, nil) + ev(t, string(data), #load(#file, string)) + delete(data) + } + } +} diff --git a/tests/core/nbio/nbio.odin b/tests/core/nbio/nbio.odin new file mode 100644 index 000000000..2f454f55b --- /dev/null +++ b/tests/core/nbio/nbio.odin @@ -0,0 +1,258 @@ +package tests_nbio + +import "core:log" +import "core:nbio" +import "core:testing" +import "core:thread" +import "core:time" +import os "core:os/os2" + +ev :: testing.expect_value +e :: testing.expect + +@(deferred_in=event_loop_guard_exit) +event_loop_guard :: proc(t: ^testing.T) -> bool { + err := nbio.acquire_thread_event_loop() + if err == .Unsupported || !nbio.FULLY_SUPPORTED { + log.warn("nbio unsupported, skipping") + return false + } + + ev(t, err, nil) + return true +} + +event_loop_guard_exit :: proc(t: ^testing.T) { + ev(t, nbio.run(), nil) // Could have some things to clean up from a `defer` in the test. + nbio.release_thread_event_loop() +} + +// Tests that all poly variants are correctly passing through arguments, and that +// all procs eventually get their callback called. +// +// This is important because the poly procs are only checked when they are called, +// So this will also catch any typos in their implementations. +@(test) +all_poly_work :: proc(tt: ^testing.T) { + if event_loop_guard(tt) { + testing.set_fail_timeout(tt, time.Minute) + + @static t: ^testing.T + t = tt + + @static n: int + n = 0 + NUM_TESTS :: 39 + + UDP_SOCKET :: max(nbio.UDP_Socket) + TCP_SOCKET :: max(nbio.TCP_Socket) + + tmp, terr := os.create_temp_file("", "tests_nbio_poly*", {.Non_Blocking}) + ev(t, terr, nil) + defer os.close(tmp) + + HANDLE, aerr := nbio.associate_handle(os.fd(tmp)) + ev(t, aerr, nil) + + _buf: [1]byte + buf := _buf[:] + + one :: proc(op: ^nbio.Operation, one: int) { + n += 1 + ev(t, one, 1) + } + + two :: proc(op: ^nbio.Operation, one: int, two: int) { + n += 1 + ev(t, one, 1) + ev(t, two, 2) + } + + three :: proc(op: ^nbio.Operation, one: int, two: int, three: int) { + n += 1 + ev(t, one, 1) + ev(t, two, 2) + ev(t, three, 3) + } + + nbio.accept_poly(TCP_SOCKET, 1, one) + nbio.accept_poly2(TCP_SOCKET, 1, 2, two) + nbio.accept_poly3(TCP_SOCKET, 1, 2, 3, three) + + nbio.close_poly(max(nbio.Handle), 1, one) + nbio.close_poly2(max(nbio.Handle), 1, 2, two) + nbio.close_poly3(max(nbio.Handle), 1, 2, 3, three) + + nbio.dial_poly({nbio.IP4_Address{127, 0, 0, 1}, 0}, 1, one) + nbio.dial_poly2({nbio.IP4_Address{127, 0, 0, 1}, 0}, 1, 2, two) + nbio.dial_poly3({nbio.IP4_Address{127, 0, 0, 1}, 0}, 1, 2, 3, three) + + nbio.recv_poly(TCP_SOCKET, {buf}, 1, one) + nbio.recv_poly2(TCP_SOCKET, {buf}, 1, 2, two) + nbio.recv_poly3(TCP_SOCKET, {buf}, 1, 2, 3, three) + + nbio.send_poly(TCP_SOCKET, {buf}, 1, one) + nbio.send_poly2(TCP_SOCKET, {buf}, 1, 2, two) + nbio.send_poly3(TCP_SOCKET, {buf}, 1, 2, 3, three) + + nbio.sendfile_poly(TCP_SOCKET, HANDLE, 1, one) + nbio.sendfile_poly2(TCP_SOCKET, HANDLE, 1, 2, two) + nbio.sendfile_poly3(TCP_SOCKET, HANDLE, 1, 2, 3, three) + + nbio.read_poly(HANDLE, 0, buf, 1, one) + nbio.read_poly2(HANDLE, 0, buf, 1, 2, two) + nbio.read_poly3(HANDLE, 0, buf, 1, 2, 3, three) + + nbio.write_poly(HANDLE, 0, buf, 1, one) + nbio.write_poly2(HANDLE, 0, buf, 1, 2, two) + nbio.write_poly3(HANDLE, 0, buf, 1, 2, 3, three) + + nbio.next_tick_poly(1, one) + nbio.next_tick_poly2(1, 2, two) + nbio.next_tick_poly3(1, 2, 3, three) + + nbio.timeout_poly(1, 1, one) + nbio.timeout_poly2(1, 1, 2, two) + nbio.timeout_poly3(1, 1, 2, 3, three) + + nbio.poll_poly(TCP_SOCKET, .Receive, 1, one) + nbio.poll_poly2(TCP_SOCKET, .Receive, 1, 2, two) + nbio.poll_poly3(TCP_SOCKET, .Receive, 1, 2, 3, three) + + nbio.open_poly("", 1, one) + nbio.open_poly2("", 1, 2, two) + nbio.open_poly3("", 1, 2, 3, three) + + nbio.stat_poly(HANDLE, 1, one) + nbio.stat_poly2(HANDLE, 1, 2, two) + nbio.stat_poly3(HANDLE, 1, 2, 3, three) + + ev(t, n, 0) // Test that no callbacks are ran before the loop is ticked. + ev(t, nbio.run(), nil) + ev(t, n, NUM_TESTS) // Test that all callbacks have ran. + } +} + +@(test) +two_ops_at_the_same_time :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + server, err := nbio.create_udp_socket(.IP4) + ev(t, err, nil) + defer nbio.close(server) + + berr := nbio.bind(server, {nbio.IP4_Loopback, 0}) + ev(t, berr, nil) + ep, eperr := nbio.bound_endpoint(server) + ev(t, eperr, nil) + + // Server. + { + nbio.poll_poly(server, .Receive, t, on_poll) + + on_poll :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.poll.result, nbio.Poll_Result.Ready) + } + + buf: [128]byte + nbio.recv_poly(server, {buf[:]}, t, on_recv) + + on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.recv.err, nil) + } + } + + // Client. + { + sock, cerr := nbio.create_udp_socket(.IP4) + ev(t, cerr, nil) + + // Make sure the server would block. + nbio.timeout_poly3(time.Millisecond*10, t, sock, ep.port, on_timeout) + + on_timeout :: proc(op: ^nbio.Operation, t: ^testing.T, sock: nbio.UDP_Socket, port: int) { + nbio.send_poly(sock, {transmute([]byte)string("Hiya")}, t, on_send, {nbio.IP4_Loopback, port}) + } + + on_send :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.send.err, nil) + ev(t, op.send.sent, 4) + + // Do another send after a bit, some backends don't trigger both ops when one was enough to + // use up the socket. + nbio.timeout_poly3(time.Millisecond*10, t, op.send.socket.(nbio.UDP_Socket), op.send.endpoint.port, on_timeout2) + } + + on_timeout2 :: proc(op: ^nbio.Operation, t: ^testing.T, sock: nbio.UDP_Socket, port: int) { + nbio.send_poly(sock, {transmute([]byte)string("Hiya")}, t, on_send2, {nbio.IP4_Loopback, port}) + } + + on_send2 :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.send.err, nil) + ev(t, op.send.sent, 4) + + nbio.close(op.send.socket.(nbio.UDP_Socket)) + } + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +timeout :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + start := time.now() + + nbio.timeout_poly2(time.Millisecond*20, t, start, on_timeout) + + on_timeout :: proc(op: ^nbio.Operation, t: ^testing.T, start: time.Time) { + since := time.since(start) + log.infof("timeout ran after: %v", since) + testing.expect(t, since >= time.Millisecond*19) // A ms grace, for some reason it is sometimes ran after 19.8ms. + if since < 20 { + log.warnf("timeout ran after: %v", since) + } + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +wake_up :: proc(t: ^testing.T) { + testing.set_fail_timeout(t, time.Minute) + if event_loop_guard(t) { + for _ in 0..<2 { + sock, _ := open_next_available_local_port(t) + + // Add an accept, with nobody dialling this should block the event loop forever. + accept := nbio.accept(sock, proc(op: ^nbio.Operation) { + log.error("shouldn't be called") + }) + + // Make sure the accept is in progress. + ev(t, nbio.tick(timeout=0), nil) + + hit: bool + thr := thread.create_and_start_with_poly_data2(nbio.current_thread_event_loop(), &hit, proc(l: ^nbio.Event_Loop, hit: ^bool) { + hit^ = true + nbio.wake_up(l) + }, context) + defer thread.destroy(thr) + + // Should block forever until the thread calling wake_up will make it return. + ev(t, nbio.tick(), nil) + e(t, hit) + + nbio.remove(accept) + nbio.close(sock) + + ev(t, nbio.run(), nil) + ev(t, nbio.tick(timeout=0), nil) + } + } +} diff --git a/tests/core/nbio/net.odin b/tests/core/nbio/net.odin new file mode 100644 index 000000000..688ee0b45 --- /dev/null +++ b/tests/core/nbio/net.odin @@ -0,0 +1,400 @@ +package tests_nbio + +import "core:mem" +import "core:nbio" +import "core:net" +import "core:testing" +import "core:time" +import "core:log" + +open_next_available_local_port :: proc(t: ^testing.T, addr: net.Address = net.IP4_Loopback, loc := #caller_location) -> (sock: net.TCP_Socket, ep: net.Endpoint) { + err: net.Network_Error + sock, err = nbio.listen_tcp({addr, 0}) + if err != nil { + log.errorf("listen_tcp: %v", err, location=loc) + return + } + + ep, err = net.bound_endpoint(sock) + if err != nil { + log.errorf("bound_endpoint: %v", err, location=loc) + } + + return +} + +@(test) +client_and_server_send_recv :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + server, ep := open_next_available_local_port(t) + + CONTENT :: [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + + State :: struct { + server: net.TCP_Socket, + server_client: net.TCP_Socket, + client: net.TCP_Socket, + recv_buf: [20]byte, + send_buf: [20]byte, + } + + state := State{ + server = server, + send_buf = CONTENT, + } + + close_ok :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.close.err, nil) + } + + // Server + { + nbio.accept_poly2(server, t, &state, on_accept) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T, state: ^State) { + ev(t, op.accept.err, nil) + + state.server_client = op.accept.client + + log.debugf("accepted connection from: %v", op.accept.client_endpoint) + + nbio.recv_poly2(state.server_client, {state.recv_buf[:]}, t, state, on_recv) + } + + on_recv :: proc(op: ^nbio.Operation, t: ^testing.T, state: ^State) { + ev(t, op.recv.err, nil) + ev(t, op.recv.received, 20) + ev(t, state.recv_buf, CONTENT) + + nbio.close_poly(state.server_client, t, close_ok) + nbio.close_poly(state.server, t, close_ok) + } + + ev(t, nbio.tick(0), nil) + } + + // Client + { + nbio.dial_poly2(ep, t, &state, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T, state: ^State) { + ev(t, op.dial.err, nil) + + state.client = op.dial.socket + + nbio.send_poly2(state.client, {state.send_buf[:]}, t, state, on_send) + } + + on_send :: proc(op: ^nbio.Operation, t: ^testing.T, state: ^State) { + ev(t, op.send.err, nil) + ev(t, op.send.sent, 20) + + nbio.close_poly(state.client, t, close_ok) + } + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +close_and_remove_accept :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + server, _ := open_next_available_local_port(t) + + accept := nbio.accept_poly(server, t, proc(_: ^nbio.Operation, t: ^testing.T) { + testing.fail_now(t) + }) + + ev(t, nbio.tick(0), nil) + + nbio.close_poly(server, t, proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.close.err, nil) + }) + + nbio.remove(accept) + ev(t, nbio.run(), nil) + } +} + +// Tests that when a client calls `close` on it's socket, `recv` returns with `0, nil` (connection closed). +@(test) +close_errors_recv :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + server, ep := open_next_available_local_port(t) + + // Server + { + nbio.accept_poly(server, t, on_accept) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.accept.err, nil) + + bytes := make([]byte, 128, context.temp_allocator) + nbio.recv_poly(op.accept.client, {bytes}, t, on_recv) + } + + on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.recv.received, 0) + ev(t, op.recv.err, nil) + } + + ev(t, nbio.tick(0), nil) + } + + // Client + { + nbio.dial_poly(ep, t, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.dial.err, nil) + nbio.close_poly(op.dial.socket, t, on_close) + } + + on_close :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.close.err, nil) + } + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +ipv6 :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + server, ep := open_next_available_local_port(t, net.IP6_Loopback) + + nbio.accept_poly(server, t, on_accept) + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.accept.err, nil) + addr, is_ipv6 := op.accept.client_endpoint.address.(net.IP6_Address) + e(t, is_ipv6) + ev(t, addr, net.IP6_Loopback) + e(t, op.accept.client_endpoint.port != 0) + nbio.close(op.accept.client) + nbio.close(op.accept.socket) + } + + nbio.dial_poly(ep, t, on_dial) + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.dial.err, nil) + nbio.close(op.dial.socket) + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +accept_timeout :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + sock, _ := open_next_available_local_port(t) + + hit: bool + nbio.accept_poly2(sock, t, &hit, on_accept, timeout=time.Millisecond) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T, hit: ^bool) { + hit^ = true + ev(t, op.accept.err, net.Accept_Error.Timeout) + nbio.close(op.accept.socket) + } + + ev(t, nbio.run(), nil) + + e(t, hit) + } +} + +@(test) +poll_timeout :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + sock, err := nbio.create_udp_socket(.IP4) + ev(t, err, nil) + berr := nbio.bind(sock, {nbio.IP4_Loopback, 0}) + ev(t, berr, nil) + + nbio.poll_poly(sock, .Receive, t, on_poll, time.Millisecond) + on_poll :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.poll.result, nbio.Poll_Result.Timeout) + } + + ev(t, nbio.run(), nil) + } +} + +/* +This test walks through the scenario where a user wants to `poll` in order to check if some other package (in this case `core:net`), +would be able to do an operation without blocking. + +It also tests whether a poll can be issues when it is already in a ready state. +And it tests big send/recv buffers being handled properly. +*/ +@(test) +poll :: proc(t: ^testing.T) { + if event_loop_guard(t) { +// testing.set_fail_timeout(t, time.Minute) + + can_recv: bool + + sock, ep := open_next_available_local_port(t) + + // Server + { + nbio.accept_poly2(sock, t, &can_recv, on_accept) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T, can_recv: ^bool) { + ev(t, op.accept.err, nil) + + check_recv :: proc(op: ^nbio.Operation, t: ^testing.T, can_recv: ^bool, client: net.TCP_Socket) { + // Not ready to unblock the client yet, requeue for after 10ms. + if !can_recv^ { + nbio.timeout_poly3(time.Millisecond * 10, t, can_recv, client, check_recv) + return + } + + free_all(context.temp_allocator) + + // Connection was closed by client, close server. + if op.type == .Recv && op.recv.received == 0 && op.recv.err == nil { + nbio.close(client) + return + } + + if op.type == .Recv { + log.debugf("received %M this time", op.recv.received) + } + + // Receive some data to unblock the client, which should complete the poll it does, allowing it to send data again. + buf, mem_err := make([]byte, mem.Gigabyte, context.temp_allocator) + ev(t, mem_err, nil) + nbio.recv_poly3(client, {buf}, t, can_recv, client, check_recv) + } + nbio.timeout_poly3(time.Millisecond * 10, t, can_recv, op.accept.client, check_recv) + } + + ev(t, nbio.tick(0), nil) + } + + // Client + { + nbio.dial_poly2(ep, t, &can_recv, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T, can_recv: ^bool) { + ev(t, op.dial.err, nil) + + // Do a poll even though we know it's ready, so we can test that all implementations can handle that. + nbio.poll_poly2(op.dial.socket, .Send, t, can_recv, on_poll1) + } + + on_poll1 :: proc(op: ^nbio.Operation, t: ^testing.T, can_recv: ^bool) { + ev(t, op.poll.result, nil) + + // Send 4 GB of data, which in my experience causes a Would_Block error because we filled up the internal buffer. + buf, mem_err := make([]byte, mem.Gigabyte*4, context.temp_allocator) + ev(t, mem_err, nil) + + // Use `core:net` as example external code that doesn't care about the event loop. + net.set_blocking(op.poll.socket, false) + n, send_err := net.send(op.poll.socket, buf) + ev(t, send_err, net.TCP_Send_Error.Would_Block) + + log.debugf("blocking after %M", n) + + // Tell the server it can start issueing recv calls, so it unblocks us. + can_recv^ = true + + // Now poll again, when the server reads enough data it should complete, telling us we can send without blocking again. + nbio.poll_poly(op.poll.socket, .Send, t, on_poll2) + } + + on_poll2 :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.poll.result, nil) + + buf: [128]byte + bytes_written, send_err := net.send(op.poll.socket, buf[:]) + ev(t, bytes_written, 128) + ev(t, send_err, nil) + + nbio.close(op.poll.socket.(net.TCP_Socket)) + } + } + + ev(t, nbio.run(), nil) + nbio.close(sock) + ev(t, nbio.run(), nil) + } +} + +@(test) +sendfile :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + CONTENT :: #load(#file) + + sock, ep := open_next_available_local_port(t) + + // Server + { + nbio.accept_poly(sock, t, on_accept) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.accept.err, nil) + e(t, op.accept.client != 0) + + log.debugf("connection from: %v", op.accept.client_endpoint) + nbio.open_poly3(#file, t, op.accept.socket, op.accept.client, on_open) + } + + on_open :: proc(op: ^nbio.Operation, t: ^testing.T, server, client: net.TCP_Socket) { + ev(t, op.open.err, nil) + + nbio.sendfile_poly2(client, op.open.handle, t, server, on_sendfile) + } + + on_sendfile :: proc(op: ^nbio.Operation, t: ^testing.T, server: net.TCP_Socket) { + ev(t, op.sendfile.err, nil) + ev(t, op.sendfile.sent, len(CONTENT)) + + nbio.close(op.sendfile.file) + nbio.close(op.sendfile.socket) + nbio.close(server) + } + } + + // Client + { + nbio.dial_poly(ep, t, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.dial.err, nil) + + buf := make([]byte, len(CONTENT), context.temp_allocator) + nbio.recv_poly(op.dial.socket, {buf}, t, on_recv, all=true) + } + + on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.recv.err, nil) + ev(t, op.recv.received, len(CONTENT)) + ev(t, string(op.recv.bufs[0]), string(CONTENT)) + + nbio.close(op.recv.socket.(net.TCP_Socket)) + } + } + + ev(t, nbio.run(), nil) + } +} diff --git a/tests/core/nbio/remove.odin b/tests/core/nbio/remove.odin new file mode 100644 index 000000000..063c2cf58 --- /dev/null +++ b/tests/core/nbio/remove.odin @@ -0,0 +1,247 @@ +package tests_nbio + +import "core:nbio" +import "core:net" +import "core:testing" +import "core:time" +import "core:log" + +// Removals are pretty complex. + +@(test) +immediate_remove_of_sendfile :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + sock, ep := open_next_available_local_port(t) + + // Server + { + nbio.accept_poly(sock, t, on_accept) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.accept.err, nil) + e(t, op.accept.client != 0) + + log.debugf("connection from: %v", op.accept.client_endpoint) + nbio.open_poly3(#file, t, op.accept.socket, op.accept.client, on_open) + } + + on_open :: proc(op: ^nbio.Operation, t: ^testing.T, server, client: net.TCP_Socket) { + ev(t, op.open.err, nil) + e(t, op.open.handle != 0) + + sendfile_op := nbio.sendfile_poly2(client, op.open.handle, t, server, on_sendfile) + + // oh no changed my mind. + nbio.remove(sendfile_op) + + nbio.close(op.open.handle) + nbio.close(client) + nbio.close(server) + } + + on_sendfile :: proc(op: ^nbio.Operation, t: ^testing.T, server: net.TCP_Socket) { + log.error("on_sendfile shouldn't be called") + } + } + + // Client + { + nbio.dial_poly(ep, t, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.dial.err, nil) + + buf := make([]byte, 128, context.temp_allocator) + nbio.recv_poly(op.dial.socket, {buf}, t, on_recv) + } + + on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.recv.err, nil) + + nbio.close(op.recv.socket.(net.TCP_Socket)) + } + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +immediate_remove_of_sendfile_without_stat :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + sock, ep := open_next_available_local_port(t) + + // Server + { + nbio.accept_poly(sock, t, on_accept) + + on_accept :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.accept.err, nil) + e(t, op.accept.client != 0) + + log.debugf("connection from: %v", op.accept.client_endpoint) + nbio.open_poly3(#file, t, op.accept.socket, op.accept.client, on_open) + } + + on_open :: proc(op: ^nbio.Operation, t: ^testing.T, server, client: net.TCP_Socket) { + ev(t, op.open.err, nil) + e(t, op.open.handle != 0) + + nbio.stat_poly3(op.open.handle, t, server, client, on_stat) + } + + on_stat :: proc(op: ^nbio.Operation, t: ^testing.T, server, client: net.TCP_Socket) { + ev(t, op.stat.err, nil) + + sendfile_op := nbio.sendfile_poly2(client, op.stat.handle, t, server, on_sendfile, nbytes=int(op.stat.size)) + + // oh no changed my mind. + nbio.remove(sendfile_op) + + nbio.timeout_poly3(time.Millisecond * 10, op.stat.handle, client, server, proc(op: ^nbio.Operation, p1: nbio.Handle, p2, p3: net.TCP_Socket){ + nbio.close(p1) + nbio.close(p2) + nbio.close(p3) + }) + } + + on_sendfile :: proc(op: ^nbio.Operation, t: ^testing.T, server: net.TCP_Socket) { + log.error("on_sendfile shouldn't be called") + } + } + + // Client + { + nbio.dial_poly(ep, t, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.dial.err, nil) + + buf := make([]byte, 128, context.temp_allocator) + nbio.recv_poly(op.dial.socket, {buf}, t, on_recv) + } + + on_recv :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.recv.err, nil) + + nbio.close(op.recv.socket.(net.TCP_Socket)) + } + } + + ev(t, nbio.run(), nil) + } +} + +// Open should free the temporary memory allocated for the path when removed. +// Can't really test that though, so should be checked manually that the internal callback is called but not the external. +@(test) +remove_open :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + open := nbio.open(#file, on_open) + nbio.remove(open) + + on_open :: proc(op: ^nbio.Operation) { + log.error("on_open shouldn't be called") + } + + ev(t, nbio.run(), nil) + } +} + +// Dial should close the socket when removed. +// Can't really test that though, so should be checked manually that the internal callback is called but not the external. +@(test) +remove_dial :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + sock, ep := open_next_available_local_port(t) + defer nbio.close(sock) + + dial := nbio.dial(ep, on_dial) + nbio.remove(dial) + + on_dial :: proc(op: ^nbio.Operation) { + log.error("on_dial shouldn't be called") + } + + ev(t, nbio.run(), nil) + } +} + +@(test) +remove_next_tick :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + nt := nbio.next_tick_poly(t, proc(op: ^nbio.Operation, t: ^testing.T) { + log.error("shouldn't be called") + }) + nbio.remove(nt) + + ev(t, nbio.run(), nil) + } +} + +@(test) +remove_timeout :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + hit: bool + timeout := nbio.timeout_poly(time.Second, &hit, proc(_: ^nbio.Operation, hit: ^bool) { + hit^ = true + }) + + nbio.remove(timeout) + + ev(t, nbio.run(), nil) + + e(t, !hit) + } +} + +@(test) +remove_multiple_poll :: proc(t: ^testing.T) { + if event_loop_guard(t) { + testing.set_fail_timeout(t, time.Minute) + + sock, ep := open_next_available_local_port(t) + defer nbio.close(sock) + + hit: bool + + first := nbio.poll(sock, .Receive, on_poll) + nbio.poll_poly2(sock, .Receive, t, &hit, on_poll2) + + on_poll :: proc(op: ^nbio.Operation) { + log.error("shouldn't be called") + } + + on_poll2 :: proc(op: ^nbio.Operation, t: ^testing.T, hit: ^bool) { + ev(t, op.poll.result, nbio.Poll_Result.Ready) + hit^ = true + } + + ev(t, nbio.tick(0), nil) + + nbio.remove(first) + + ev(t, nbio.tick(0), nil) + + nbio.dial_poly(ep, t, on_dial) + + on_dial :: proc(op: ^nbio.Operation, t: ^testing.T) { + ev(t, op.dial.err, nil) + } + + ev(t, nbio.run(), nil) + e(t, hit) + } +} diff --git a/tests/core/net/test_core_net.odin b/tests/core/net/test_core_net.odin index 9b3973a60..55fe6671d 100644 --- a/tests/core/net/test_core_net.odin +++ b/tests/core/net/test_core_net.odin @@ -10,8 +10,6 @@ A test suite for `core:net` */ -#+build !netbsd -#+build !openbsd #+feature dynamic-literals package test_core_net diff --git a/tests/core/normal.odin b/tests/core/normal.odin index e8b61fee8..d0889bf89 100644 --- a/tests/core/normal.odin +++ b/tests/core/normal.odin @@ -32,6 +32,7 @@ download_assets :: proc "contextless" () { @(require) import "math/noise" @(require) import "math/rand" @(require) import "mem" +@(require) import "nbio" @(require) import "net" @(require) import "odin" @(require) import "os" @@ -45,6 +46,7 @@ download_assets :: proc "contextless" () { @(require) import "sync" @(require) import "sync/chan" @(require) import "sys/posix" +@(require) import "sys/kqueue" @(require) import "sys/windows" @(require) import "text/i18n" @(require) import "text/match" diff --git a/tests/core/sys/kqueue/structs.odin b/tests/core/sys/kqueue/structs.odin new file mode 100644 index 000000000..edf1fdd1e --- /dev/null +++ b/tests/core/sys/kqueue/structs.odin @@ -0,0 +1,56 @@ +#+build darwin, freebsd, openbsd, netbsd +package tests_core_sys_kqueue + +import "core:strings" +import "core:testing" +import os "core:os/os2" + +@(test) +structs :: proc(t: ^testing.T) { + { + c_compiler := os.get_env("CC", context.temp_allocator) + if c_compiler == "" { + c_compiler = "clang" + } + + c_compilation, c_start_err := os.process_start({ + command = {c_compiler, #directory + "/structs/structs.c", "-o", #directory + "/structs/c_structs"}, + stdout = os.stdout, + stderr = os.stderr, + }) + testing.expect_value(t, c_start_err, nil) + + o_compilation, o_start_err := os.process_start({ + command = {ODIN_ROOT + "/odin", "build", #directory + "/structs", "-out:" + #directory + "/structs/odin_structs"}, + stdout = os.stdout, + stderr = os.stderr, + }) + testing.expect_value(t, o_start_err, nil) + + c_status, c_err := os.process_wait(c_compilation) + testing.expect_value(t, c_err, nil) + testing.expect_value(t, c_status.exit_code, 0) + + o_status, o_err := os.process_wait(o_compilation) + testing.expect_value(t, o_err, nil) + testing.expect_value(t, o_status.exit_code, 0) + } + + c_status, c_stdout, c_stderr, c_err := os.process_exec({command={#directory + "/structs/c_structs"}}, context.temp_allocator) + testing.expect_value(t, c_err, nil) + testing.expect_value(t, c_status.exit_code, 0) + testing.expect_value(t, string(c_stderr), "") + + o_status, o_stdout, o_stderr, o_err := os.process_exec({command={#directory + "/structs/odin_structs"}}, context.temp_allocator) + testing.expect_value(t, o_err, nil) + testing.expect_value(t, o_status.exit_code, 0) + testing.expect_value(t, string(o_stderr), "") + + testing.expect(t, strings.trim_space(string(c_stdout)) != "") + + testing.expect_value( + t, + strings.trim_space(string(o_stdout)), + strings.trim_space(string(c_stdout)), + ) +} diff --git a/tests/core/sys/kqueue/structs/structs.c b/tests/core/sys/kqueue/structs/structs.c new file mode 100644 index 000000000..e7620c994 --- /dev/null +++ b/tests/core/sys/kqueue/structs/structs.c @@ -0,0 +1,63 @@ +#include <stddef.h> +#include <stdio.h> +#include <sys/event.h> + +int main(int argc, char *argv[]) +{ + printf("kevent %zu %zu\n", sizeof(struct kevent), _Alignof(struct kevent)); + printf("kevent.ident %zu\n", offsetof(struct kevent, ident)); + printf("kevent.filter %zu\n", offsetof(struct kevent, filter)); + printf("kevent.flags %zu\n", offsetof(struct kevent, flags)); + printf("kevent.fflags %zu\n", offsetof(struct kevent, fflags)); + printf("kevent.data %zu\n", offsetof(struct kevent, data)); + printf("kevent.udata %zu\n", offsetof(struct kevent, udata)); + + printf("EV_ADD %d\n", EV_ADD); + printf("EV_DELETE %d\n", EV_DELETE); + printf("EV_ENABLE %d\n", EV_ENABLE); + printf("EV_DISABLE %d\n", EV_DISABLE); + printf("EV_ONESHOT %d\n", EV_ONESHOT); + printf("EV_CLEAR %d\n", EV_CLEAR); + printf("EV_RECEIPT %d\n", EV_RECEIPT); + printf("EV_DISPATCH %d\n", EV_DISPATCH); + printf("EV_ERROR %d\n", EV_ERROR); + printf("EV_EOF %d\n", EV_EOF); + + printf("EVFILT_READ %d\n", EVFILT_READ); + printf("EVFILT_WRITE %d\n", EVFILT_WRITE); + printf("EVFILT_AIO %d\n", EVFILT_AIO); + printf("EVFILT_VNODE %d\n", EVFILT_VNODE); + printf("EVFILT_PROC %d\n", EVFILT_PROC); + printf("EVFILT_SIGNAL %d\n", EVFILT_SIGNAL); + printf("EVFILT_TIMER %d\n", EVFILT_TIMER); + printf("EVFILT_USER %d\n", EVFILT_USER); + + printf("NOTE_SECONDS %u\n", NOTE_SECONDS); + printf("NOTE_USECONDS %u\n", NOTE_USECONDS); + printf("NOTE_NSECONDS %u\n", NOTE_NSECONDS); +#if defined(NOTE_ABSOLUTE) + printf("NOTE_ABSOLUTE %u\n", NOTE_ABSOLUTE); +#else + printf("NOTE_ABSOLUTE %u\n", NOTE_ABSTIME); +#endif + + printf("NOTE_LOWAT %u\n", NOTE_LOWAT); + + printf("NOTE_DELETE %u\n", NOTE_DELETE); + printf("NOTE_WRITE %u\n", NOTE_WRITE); + printf("NOTE_EXTEND %u\n", NOTE_EXTEND); + printf("NOTE_ATTRIB %u\n", NOTE_ATTRIB); + printf("NOTE_LINK %u\n", NOTE_LINK); + printf("NOTE_RENAME %u\n", NOTE_RENAME); + printf("NOTE_REVOKE %u\n", NOTE_REVOKE); + + printf("NOTE_EXIT %u\n", NOTE_EXIT); + printf("NOTE_FORK %u\n", NOTE_FORK); + printf("NOTE_EXEC %u\n", NOTE_EXEC); + + printf("NOTE_TRIGGER %u\n", NOTE_TRIGGER); + printf("NOTE_FFAND %u\n", NOTE_FFAND); + printf("NOTE_FFOR %u\n", NOTE_FFOR); + printf("NOTE_FFCOPY %u\n", NOTE_FFCOPY); + return 0; +} diff --git a/tests/core/sys/kqueue/structs/structs.odin b/tests/core/sys/kqueue/structs/structs.odin new file mode 100644 index 000000000..4886f63e4 --- /dev/null +++ b/tests/core/sys/kqueue/structs/structs.odin @@ -0,0 +1,58 @@ +package main + +import "core:fmt" +import "core:sys/kqueue" + +main :: proc() { + fmt.println("kevent", size_of(kqueue.KEvent), align_of(kqueue.KEvent)) + fmt.println("kevent.ident", offset_of(kqueue.KEvent, ident)) + fmt.println("kevent.filter", offset_of(kqueue.KEvent, filter)) + fmt.println("kevent.flags", offset_of(kqueue.KEvent, flags)) + fmt.println("kevent.fflags", offset_of(kqueue.KEvent, fflags)) + fmt.println("kevent.data", offset_of(kqueue.KEvent, data)) + fmt.println("kevent.udata", offset_of(kqueue.KEvent, udata)) + + fmt.println("EV_ADD", transmute(kqueue._Flags_Backing)kqueue.Flags{.Add}) + fmt.println("EV_DELETE", transmute(kqueue._Flags_Backing)kqueue.Flags{.Delete}) + fmt.println("EV_ENABLE", transmute(kqueue._Flags_Backing)kqueue.Flags{.Enable}) + fmt.println("EV_DISABLE", transmute(kqueue._Flags_Backing)kqueue.Flags{.Disable}) + fmt.println("EV_ONESHOT", transmute(kqueue._Flags_Backing)kqueue.Flags{.One_Shot}) + fmt.println("EV_CLEAR", transmute(kqueue._Flags_Backing)kqueue.Flags{.Clear}) + fmt.println("EV_RECEIPT", transmute(kqueue._Flags_Backing)kqueue.Flags{.Receipt}) + fmt.println("EV_DISPATCH", transmute(kqueue._Flags_Backing)kqueue.Flags{.Dispatch}) + fmt.println("EV_ERROR", transmute(kqueue._Flags_Backing)kqueue.Flags{.Error}) + fmt.println("EV_EOF", transmute(kqueue._Flags_Backing)kqueue.Flags{.EOF}) + + fmt.println("EVFILT_READ", int(kqueue.Filter.Read)) + fmt.println("EVFILT_WRITE", int(kqueue.Filter.Write)) + fmt.println("EVFILT_AIO", int(kqueue.Filter.AIO)) + fmt.println("EVFILT_VNODE", int(kqueue.Filter.VNode)) + fmt.println("EVFILT_PROC", int(kqueue.Filter.Proc)) + fmt.println("EVFILT_SIGNAL", int(kqueue.Filter.Signal)) + fmt.println("EVFILT_TIMER", int(kqueue.Filter.Timer)) + fmt.println("EVFILT_USER", int(kqueue.Filter.User)) + + fmt.println("NOTE_SECONDS", transmute(u32)kqueue.Timer_Flags{.Seconds}) + fmt.println("NOTE_USECONDS", transmute(u32)kqueue.Timer_Flags{.USeconds}) + fmt.println("NOTE_NSECONDS", transmute(u32)kqueue.TIMER_FLAGS_NSECONDS) + fmt.println("NOTE_ABSOLUTE", transmute(u32)kqueue.Timer_Flags{.Absolute}) + + fmt.println("NOTE_LOWAT", transmute(u32)kqueue.RW_Flags{.Low_Water_Mark}) + + fmt.println("NOTE_DELETE", transmute(u32)kqueue.VNode_Flags{.Delete}) + fmt.println("NOTE_WRITE", transmute(u32)kqueue.VNode_Flags{.Write}) + fmt.println("NOTE_EXTEND", transmute(u32)kqueue.VNode_Flags{.Extend}) + fmt.println("NOTE_ATTRIB", transmute(u32)kqueue.VNode_Flags{.Attrib}) + fmt.println("NOTE_LINK", transmute(u32)kqueue.VNode_Flags{.Link}) + fmt.println("NOTE_RENAME", transmute(u32)kqueue.VNode_Flags{.Rename}) + fmt.println("NOTE_REVOKE", transmute(u32)kqueue.VNode_Flags{.Revoke}) + + fmt.println("NOTE_EXIT", transmute(u32)kqueue.Proc_Flags{.Exit}) + fmt.println("NOTE_FORK", transmute(u32)kqueue.Proc_Flags{.Fork}) + fmt.println("NOTE_EXEC", transmute(u32)kqueue.Proc_Flags{.Exec}) + + fmt.println("NOTE_TRIGGER", transmute(u32)kqueue.User_Flags{.Trigger}) + fmt.println("NOTE_FFAND", transmute(u32)kqueue.User_Flags{.FFAnd}) + fmt.println("NOTE_FFOR", transmute(u32)kqueue.User_Flags{.FFOr}) + fmt.println("NOTE_FFCOPY", transmute(u32)kqueue.USER_FLAGS_COPY) +} |