Merge pull request #6124 from laytan/nbio

Add `core:nbio`
This commit is contained in:
gingerBill
2026-01-16 13:25:03 +00:00
committed by GitHub
88 changed files with 11901 additions and 483 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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
}
}

View File

@@ -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
}

195
core/nbio/doc.odin Normal file
View File

@@ -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 handles 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 operations 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

84
core/nbio/errors.odin Normal file
View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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

256
core/nbio/impl.odin Normal file
View File

@@ -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
}

1421
core/nbio/impl_linux.odin Normal file

File diff suppressed because it is too large Load Diff

217
core/nbio/impl_others.odin Normal file
View File

@@ -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) {
}

1403
core/nbio/impl_posix.odin Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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()))
}

View File

@@ -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

View File

@@ -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

1733
core/nbio/impl_windows.odin Normal file

File diff suppressed because it is too large Load Diff

436
core/nbio/nbio.odin Normal file
View File

@@ -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)
}

39
core/nbio/net.odin Normal file
View File

@@ -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

2473
core/nbio/ops.odin Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}
}
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
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, 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
}
}

24
core/net/dns_os.odin Normal file
View File

@@ -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)
}

12
core/net/dns_others.odin Normal file
View File

@@ -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
}

View File

@@ -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[:])
}
}

View File

@@ -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
}
}

View File

@@ -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.

View File

@@ -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,
}

View File

@@ -1,4 +1,4 @@
#+build darwin
#+build darwin, netbsd, openbsd
package net
/*

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
_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
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_TCP_NODELAY :: -1
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_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)

View File

@@ -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
}

105
core/net/socket_others.odin Normal file
View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,
_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
// bool: Whether other programs will be inhibited from binding the same endpoint as this socket.
Exclusive_Addr_Use = win.SO_EXCLUSIVEADDRUSE,
_SOCKET_OPTION_TCP_NODELAY :: win.TCP_NODELAY
// bool: When true, keepalive packets will be automatically be sent for this connection. TODO: verify this understanding
Keep_Alive = win.SO_KEEPALIVE,
_SOCKET_OPTION_USE_LOOPBACK :: -1
_SOCKET_OPTION_REUSE_PORT :: -1
_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE :: -1
_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: -1
// bool: When true, client connections will immediately be sent a TCP/IP RST response, rather than being accepted.
Conditional_Accept = win.SO_CONDITIONAL_ACCEPT,
_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: win.SO_EXCLUSIVEADDRUSE
_SOCKET_OPTION_CONDITIONAL_ACCEPT :: win.SO_CONDITIONAL_ACCEPT
_SOCKET_OPTION_DONT_LINGER :: win.SO_DONTLINGER
// 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,
}
_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)

View File

@@ -67,7 +67,7 @@ File_Flag :: enum {
Trunc,
Sparse,
Inheritable,
Non_Blocking,
Unbuffered_IO,
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
*/

View File

@@ -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,

View File

@@ -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 ---
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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, &params)
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

View File

@@ -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()
}

View File

@@ -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],
} \
)
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}
}
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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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 ---

View File

@@ -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})

View File

@@ -2,4 +2,5 @@
package all
@(require) import "core:sys/linux"
@(require) import "vendor:x11/xlib"
@(require) import "core:sys/linux/uring"
@(require) import "vendor:x11/xlib"

View File

@@ -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() {}
main :: proc() {}

View File

@@ -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)

100
tests/core/nbio/fs.odin Normal file
View File

@@ -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)
}
}
}

258
tests/core/nbio/nbio.odin Normal file
View File

@@ -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)
}
}
}

400
tests/core/nbio/net.odin Normal file
View File

@@ -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)
}
}

247
tests/core/nbio/remove.odin Normal file
View File

@@ -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)
}
}

View File

@@ -10,8 +10,6 @@
A test suite for `core:net`
*/
#+build !netbsd
#+build !openbsd
#+feature dynamic-literals
package test_core_net

View File

@@ -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"

View File

@@ -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)),
)
}

View File

@@ -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;
}

View File

@@ -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)
}