rusty-ring 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # rusty-ring
2
+
3
+ ## Purpose
4
+
5
+ Rust-based io_uring bindings for Python via PyO3. Replaces the external `liburing` (Cython) dependency with bindings built on the `io-uring` crate from tokio-rs.
6
+
7
+ ## Layout
8
+
9
+ ```
10
+ src/ # Rust source code
11
+ python/rusty_ring/ # Python type stubs (maturin mixed layout)
12
+ tests/ # Python tests
13
+ pyproject.toml # Package metadata (maturin build backend)
14
+ Cargo.toml # Rust package config
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ just dev # Build debug extension via maturin develop
21
+ just dev-release # Build release extension
22
+ just test # Run Python tests (builds first)
23
+ just test-rust # Run Rust tests
24
+ just check # Run all checks
25
+ ```
26
+
27
+ ## Notes
28
+
29
+ - Uses maturin (not hatchling) as build backend
30
+ - Not managed by copier/bonfire template
31
+ - `src/` contains Rust code, not Python — Python stubs live in `python/`
@@ -0,0 +1,197 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "bitflags"
13
+ version = "2.11.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
16
+
17
+ [[package]]
18
+ name = "cfg-if"
19
+ version = "1.0.4"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
22
+
23
+ [[package]]
24
+ name = "heck"
25
+ version = "0.5.0"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
28
+
29
+ [[package]]
30
+ name = "indoc"
31
+ version = "2.0.7"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
34
+ dependencies = [
35
+ "rustversion",
36
+ ]
37
+
38
+ [[package]]
39
+ name = "io-uring"
40
+ version = "0.7.11"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344"
43
+ dependencies = [
44
+ "bitflags",
45
+ "cfg-if",
46
+ "libc",
47
+ ]
48
+
49
+ [[package]]
50
+ name = "libc"
51
+ version = "0.2.182"
52
+ source = "registry+https://github.com/rust-lang/crates.io-index"
53
+ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
54
+
55
+ [[package]]
56
+ name = "memoffset"
57
+ version = "0.9.1"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
60
+ dependencies = [
61
+ "autocfg",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "once_cell"
66
+ version = "1.21.3"
67
+ source = "registry+https://github.com/rust-lang/crates.io-index"
68
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
69
+
70
+ [[package]]
71
+ name = "portable-atomic"
72
+ version = "1.13.1"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
75
+
76
+ [[package]]
77
+ name = "proc-macro2"
78
+ version = "1.0.106"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
81
+ dependencies = [
82
+ "unicode-ident",
83
+ ]
84
+
85
+ [[package]]
86
+ name = "pyo3"
87
+ version = "0.27.2"
88
+ source = "registry+https://github.com/rust-lang/crates.io-index"
89
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
90
+ dependencies = [
91
+ "indoc",
92
+ "libc",
93
+ "memoffset",
94
+ "once_cell",
95
+ "portable-atomic",
96
+ "pyo3-build-config",
97
+ "pyo3-ffi",
98
+ "pyo3-macros",
99
+ "unindent",
100
+ ]
101
+
102
+ [[package]]
103
+ name = "pyo3-build-config"
104
+ version = "0.27.2"
105
+ source = "registry+https://github.com/rust-lang/crates.io-index"
106
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
107
+ dependencies = [
108
+ "target-lexicon",
109
+ ]
110
+
111
+ [[package]]
112
+ name = "pyo3-ffi"
113
+ version = "0.27.2"
114
+ source = "registry+https://github.com/rust-lang/crates.io-index"
115
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
116
+ dependencies = [
117
+ "libc",
118
+ "pyo3-build-config",
119
+ ]
120
+
121
+ [[package]]
122
+ name = "pyo3-macros"
123
+ version = "0.27.2"
124
+ source = "registry+https://github.com/rust-lang/crates.io-index"
125
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
126
+ dependencies = [
127
+ "proc-macro2",
128
+ "pyo3-macros-backend",
129
+ "quote",
130
+ "syn",
131
+ ]
132
+
133
+ [[package]]
134
+ name = "pyo3-macros-backend"
135
+ version = "0.27.2"
136
+ source = "registry+https://github.com/rust-lang/crates.io-index"
137
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
138
+ dependencies = [
139
+ "heck",
140
+ "proc-macro2",
141
+ "pyo3-build-config",
142
+ "quote",
143
+ "syn",
144
+ ]
145
+
146
+ [[package]]
147
+ name = "quote"
148
+ version = "1.0.44"
149
+ source = "registry+https://github.com/rust-lang/crates.io-index"
150
+ checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
151
+ dependencies = [
152
+ "proc-macro2",
153
+ ]
154
+
155
+ [[package]]
156
+ name = "rustversion"
157
+ version = "1.0.22"
158
+ source = "registry+https://github.com/rust-lang/crates.io-index"
159
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
160
+
161
+ [[package]]
162
+ name = "rusty-ring"
163
+ version = "0.1.0"
164
+ dependencies = [
165
+ "io-uring",
166
+ "libc",
167
+ "pyo3",
168
+ ]
169
+
170
+ [[package]]
171
+ name = "syn"
172
+ version = "2.0.117"
173
+ source = "registry+https://github.com/rust-lang/crates.io-index"
174
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
175
+ dependencies = [
176
+ "proc-macro2",
177
+ "quote",
178
+ "unicode-ident",
179
+ ]
180
+
181
+ [[package]]
182
+ name = "target-lexicon"
183
+ version = "0.13.5"
184
+ source = "registry+https://github.com/rust-lang/crates.io-index"
185
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
186
+
187
+ [[package]]
188
+ name = "unicode-ident"
189
+ version = "1.0.24"
190
+ source = "registry+https://github.com/rust-lang/crates.io-index"
191
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
192
+
193
+ [[package]]
194
+ name = "unindent"
195
+ version = "0.2.4"
196
+ source = "registry+https://github.com/rust-lang/crates.io-index"
197
+ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "rusty-ring"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ readme = "README.md"
6
+
7
+ [lib]
8
+ name = "rusty_ring"
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ pyo3 = { version = "0.27", features = ["extension-module"] }
13
+ io-uring = "0.7"
14
+ libc = "0.2"
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: rusty-ring
3
+ Version: 0.2.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: POSIX :: Linux
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Classifier: Programming Language :: Rust
11
+ Classifier: Topic :: System :: Networking
12
+ Classifier: Typing :: Typed
13
+ Summary: Rust-based io_uring bindings via PyO3
14
+ Keywords: io_uring,rust,pyo3,async,io,linux
15
+ Author-email: Otto Sellerstam <ottosellerstam@gmail.com>
16
+ License: MIT
17
+ Requires-Python: >=3.14
18
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
19
+ Project-URL: Homepage, https://github.com/otto-sellerstam/one-ring
20
+ Project-URL: Issues, https://github.com/otto-sellerstam/one-ring/issues
21
+ Project-URL: Repository, https://github.com/otto-sellerstam/one-ring
22
+
23
+ # rusty-ring
24
+
25
+ Rust-based `io_uring` bindings for Python via PyO3, built on the [`io-uring`](https://crates.io/crates/io-uring) crate from `tokio-rs`.
26
+
@@ -0,0 +1,3 @@
1
+ # rusty-ring
2
+
3
+ Rust-based `io_uring` bindings for Python via PyO3, built on the [`io-uring`](https://crates.io/crates/io-uring) crate from `tokio-rs`.
@@ -0,0 +1,34 @@
1
+ # List available recipes
2
+ default:
3
+ @just --list
4
+
5
+ # Build the Rust extension (debug, for development)
6
+ dev:
7
+ uv run maturin develop
8
+
9
+ # Build the Rust extension (release)
10
+ dev-release:
11
+ uv run maturin develop --release
12
+
13
+ # Run tests (builds first)
14
+ test: dev
15
+ uv run pytest tests/
16
+
17
+ # Run clippy linter (fail on warnings)
18
+ clippy:
19
+ cargo clippy -- -D warnings
20
+
21
+ # Format Rust code
22
+ fmt:
23
+ cargo fmt
24
+
25
+ # Check Rust formatting
26
+ fmt-check:
27
+ cargo fmt -- --check
28
+
29
+ # Run Rust tests
30
+ test-rust:
31
+ cargo test
32
+
33
+ # Run all checks
34
+ check: clippy fmt-check test-rust test
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "rusty-ring"
3
+ version = "0.2.0"
4
+ description = "Rust-based io_uring bindings via PyO3"
5
+ requires-python = ">=3.14"
6
+ license = { text = "MIT" }
7
+ authors = [
8
+ { name = "Otto Sellerstam", email = "ottosellerstam@gmail.com" },
9
+ ]
10
+ readme = "README.md"
11
+ keywords = ["io_uring", "rust", "pyo3", "async", "io", "linux"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Programming Language :: Rust",
20
+ "Topic :: System :: Networking",
21
+ "Typing :: Typed",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/otto-sellerstam/one-ring"
26
+ Repository = "https://github.com/otto-sellerstam/one-ring"
27
+ Issues = "https://github.com/otto-sellerstam/one-ring/issues"
28
+
29
+ [build-system]
30
+ requires = ["maturin>=1.8,<2.0"]
31
+ build-backend = "maturin"
32
+
33
+ [tool.maturin]
34
+ features = ["pyo3/extension-module"]
35
+ python-source = "python"
36
+ module-name = "rusty_ring._rusty_ring"
@@ -0,0 +1,121 @@
1
+ from rusty_ring._rusty_ring import (
2
+ AF_INET,
3
+ AF_INET6,
4
+ AF_UNIX,
5
+ AT_EMPTY_PATH,
6
+ AT_FDCWD,
7
+ AT_SYMLINK_NOFOLLOW,
8
+ IPPROTO_TCP,
9
+ MSG_DONTWAIT,
10
+ MSG_NOSIGNAL,
11
+ O_APPEND,
12
+ O_CLOEXEC,
13
+ O_CREAT,
14
+ O_NONBLOCK,
15
+ O_RDONLY,
16
+ O_RDWR,
17
+ O_TRUNC,
18
+ O_WRONLY,
19
+ S_IFDIR,
20
+ S_IFIFO,
21
+ S_IFLNK,
22
+ S_IFMT,
23
+ S_IFREG,
24
+ S_IFSOCK,
25
+ S_IRGRP,
26
+ S_IROTH,
27
+ S_IRUSR,
28
+ S_IWGRP,
29
+ S_IWOTH,
30
+ S_IWUSR,
31
+ S_IXGRP,
32
+ S_IXOTH,
33
+ S_IXUSR,
34
+ SFD_CLOEXEC,
35
+ SFD_NONBLOCK,
36
+ SIGHUP,
37
+ SIGINT,
38
+ SIGTERM,
39
+ SO_KEEPALIVE,
40
+ SO_REUSEADDR,
41
+ SO_REUSEPORT,
42
+ SOCK_CLOEXEC,
43
+ SOCK_DGRAM,
44
+ SOCK_NONBLOCK,
45
+ SOCK_STREAM,
46
+ SOL_SOCKET,
47
+ STATX_ALL,
48
+ STATX_ATIME,
49
+ STATX_CTIME,
50
+ STATX_INO,
51
+ STATX_MODE,
52
+ STATX_MTIME,
53
+ STATX_SIZE,
54
+ STATX_TYPE,
55
+ TCP_NODELAY,
56
+ CompletionEvent,
57
+ Ring,
58
+ SockAddr,
59
+ StatxBuffer,
60
+ )
61
+
62
+ __all__ = [
63
+ "AF_INET",
64
+ "AF_INET6",
65
+ "AF_UNIX",
66
+ "AT_EMPTY_PATH",
67
+ "AT_FDCWD",
68
+ "AT_SYMLINK_NOFOLLOW",
69
+ "IPPROTO_TCP",
70
+ "MSG_DONTWAIT",
71
+ "MSG_NOSIGNAL",
72
+ "O_APPEND",
73
+ "O_CLOEXEC",
74
+ "O_CREAT",
75
+ "O_NONBLOCK",
76
+ "O_RDONLY",
77
+ "O_RDWR",
78
+ "O_TRUNC",
79
+ "O_WRONLY",
80
+ "SFD_CLOEXEC",
81
+ "SFD_NONBLOCK",
82
+ "SIGHUP",
83
+ "SIGINT",
84
+ "SIGTERM",
85
+ "SOCK_CLOEXEC",
86
+ "SOCK_DGRAM",
87
+ "SOCK_NONBLOCK",
88
+ "SOCK_STREAM",
89
+ "SOL_SOCKET",
90
+ "SO_KEEPALIVE",
91
+ "SO_REUSEADDR",
92
+ "SO_REUSEPORT",
93
+ "STATX_ALL",
94
+ "STATX_ATIME",
95
+ "STATX_CTIME",
96
+ "STATX_INO",
97
+ "STATX_MODE",
98
+ "STATX_MTIME",
99
+ "STATX_SIZE",
100
+ "STATX_TYPE",
101
+ "S_IFDIR",
102
+ "S_IFIFO",
103
+ "S_IFLNK",
104
+ "S_IFMT",
105
+ "S_IFREG",
106
+ "S_IFSOCK",
107
+ "S_IRGRP",
108
+ "S_IROTH",
109
+ "S_IRUSR",
110
+ "S_IWGRP",
111
+ "S_IWOTH",
112
+ "S_IWUSR",
113
+ "S_IXGRP",
114
+ "S_IXOTH",
115
+ "S_IXUSR",
116
+ "TCP_NODELAY",
117
+ "CompletionEvent",
118
+ "Ring",
119
+ "SockAddr",
120
+ "StatxBuffer",
121
+ ]
@@ -0,0 +1,169 @@
1
+ import types
2
+ from typing import Self
3
+
4
+ class SockAddr:
5
+ @staticmethod
6
+ def v4(ip: str, port: int) -> SockAddr: ...
7
+ @staticmethod
8
+ def v6(ip: str, port: int) -> SockAddr: ...
9
+
10
+ class CompletionEvent:
11
+ @property
12
+ def user_data(self) -> int: ...
13
+ @property
14
+ def res(self) -> int: ...
15
+ @property
16
+ def flags(self) -> int: ...
17
+
18
+ class Ring:
19
+ def __init__(self, depth: int = 32) -> None: ...
20
+ def __enter__(self) -> Self: ...
21
+ def __exit__(
22
+ self,
23
+ exc_type: type[BaseException] | None,
24
+ exc_val: BaseException | None,
25
+ exc_tb: types.TracebackType | None,
26
+ ) -> bool: ...
27
+ def submit(self) -> int: ...
28
+ def peek(self) -> CompletionEvent | None: ...
29
+ def wait(self) -> CompletionEvent: ...
30
+ def prep_nop(self, user_data: int) -> None: ...
31
+ def prep_timeout(self, user_data: int, sec: int, nsec: int) -> None: ...
32
+ def prep_close(
33
+ self,
34
+ user_data: int,
35
+ fd: int,
36
+ ) -> None: ...
37
+ def prep_cancel(
38
+ self, user_data: int, target_user_data: int, flags: int = 0
39
+ ) -> None: ...
40
+ def prep_read(
41
+ self, user_data: int, fd: int, buf: bytearray, nbytes: int, offset: int
42
+ ) -> None: ...
43
+ def prep_write(self, user_data: int, fd: int, buf: bytes, offset: int) -> None: ...
44
+ def prep_openat(
45
+ self, user_data: int, path: str, flags: int, mode: int, dir_fd: int
46
+ ) -> None: ...
47
+ def prep_statx(
48
+ self,
49
+ user_data: int,
50
+ path: str,
51
+ buf: StatxBuffer,
52
+ flags: int,
53
+ mask: int,
54
+ dir_fd: int,
55
+ ) -> None: ...
56
+ def prep_socket(
57
+ self,
58
+ user_data: int,
59
+ domain: int,
60
+ sock_type: int,
61
+ protocol: int = 0,
62
+ flags: int = 0,
63
+ ) -> None: ...
64
+ def prep_socket_setopt(
65
+ self,
66
+ user_data: int,
67
+ fd: int,
68
+ ) -> None: ...
69
+ def prep_socket_bind(
70
+ self, user_data: int, fd: int, sock_addr: SockAddr
71
+ ) -> None: ...
72
+ def prep_socket_listen(self, user_data: int, fd: int, backlog: int) -> None: ...
73
+ def prep_socket_accept(self, user_data: int, fd: int) -> None: ...
74
+ def prep_socket_recv(
75
+ self, user_data: int, fd: int, buf: bytearray, flags: int = 0
76
+ ) -> None: ...
77
+ def prep_socket_send(
78
+ self, user_data: int, fd: int, buf: bytes, flags: int = 0
79
+ ) -> None: ...
80
+ def prep_socket_connect(
81
+ self, user_data: int, fd: int, sock_addr: SockAddr
82
+ ) -> None: ...
83
+
84
+ class StatxBuffer:
85
+ def __init__(self) -> None: ...
86
+ @property
87
+ def size(self) -> int: ...
88
+ @property
89
+ def mtime_sec(self) -> int: ...
90
+ @property
91
+ def ino(self) -> int: ...
92
+ @property
93
+ def mode(self) -> int: ...
94
+
95
+ # File open flags
96
+ O_RDONLY: int
97
+ O_WRONLY: int
98
+ O_RDWR: int
99
+ O_CREAT: int
100
+ O_TRUNC: int
101
+ O_APPEND: int
102
+ O_NONBLOCK: int
103
+ O_CLOEXEC: int
104
+
105
+ # File mode bits (permissions)
106
+ S_IRUSR: int
107
+ S_IWUSR: int
108
+ S_IXUSR: int
109
+ S_IRGRP: int
110
+ S_IWGRP: int
111
+ S_IXGRP: int
112
+ S_IROTH: int
113
+ S_IWOTH: int
114
+ S_IXOTH: int
115
+
116
+ # File type bits
117
+ S_IFREG: int
118
+ S_IFDIR: int
119
+ S_IFLNK: int
120
+ S_IFSOCK: int
121
+ S_IFIFO: int
122
+ S_IFMT: int
123
+
124
+ # AT flags (path resolution)
125
+ AT_FDCWD: int
126
+ AT_EMPTY_PATH: int
127
+ AT_SYMLINK_NOFOLLOW: int
128
+
129
+ # Statx mask
130
+ STATX_TYPE: int
131
+ STATX_MODE: int
132
+ STATX_INO: int
133
+ STATX_SIZE: int
134
+ STATX_MTIME: int
135
+ STATX_ATIME: int
136
+ STATX_CTIME: int
137
+ STATX_ALL: int
138
+
139
+ # Socket: address families
140
+ AF_INET: int
141
+ AF_INET6: int
142
+ AF_UNIX: int
143
+
144
+ # Socket: types
145
+ SOCK_STREAM: int
146
+ SOCK_DGRAM: int
147
+ SOCK_NONBLOCK: int
148
+ SOCK_CLOEXEC: int
149
+
150
+ # Socket: options
151
+ SOL_SOCKET: int
152
+ SO_REUSEADDR: int
153
+ SO_REUSEPORT: int
154
+ SO_KEEPALIVE: int
155
+ IPPROTO_TCP: int
156
+ TCP_NODELAY: int
157
+
158
+ # Socket: send/recv flags
159
+ MSG_NOSIGNAL: int
160
+ MSG_DONTWAIT: int
161
+
162
+ # Signals
163
+ SIGINT: int
164
+ SIGTERM: int
165
+ SIGHUP: int
166
+
167
+ # Signalfd flags
168
+ SFD_NONBLOCK: int
169
+ SFD_CLOEXEC: int
File without changes
@@ -0,0 +1,678 @@
1
+ use io_uring::{IoUring, opcode, types};
2
+ use pyo3::exceptions::{PyRuntimeError, PyValueError};
3
+ use pyo3::prelude::*;
4
+ use pyo3::types::{PyByteArray, PyBytes};
5
+ use std::collections::HashMap;
6
+ use std::ffi::CString;
7
+ use std::net::{Ipv4Addr, Ipv6Addr};
8
+ use std::os::unix::io::RawFd;
9
+
10
+ /// A completed io_uring operation.
11
+ #[pyclass(frozen)]
12
+ #[derive(Clone, Debug)]
13
+ struct CompletionEvent {
14
+ #[pyo3(get)]
15
+ user_data: u64,
16
+ #[pyo3(get)]
17
+ res: i32,
18
+ #[pyo3(get)]
19
+ flags: u32,
20
+ }
21
+
22
+ #[pymethods]
23
+ impl CompletionEvent {
24
+ fn __repr__(&self) -> String {
25
+ format!(
26
+ "CompletionEvent(user_data={}, res={}, flags={})",
27
+ self.user_data, self.res, self.flags
28
+ )
29
+ }
30
+ }
31
+
32
+ #[allow(dead_code)]
33
+ struct StatxRequest {
34
+ path: CString,
35
+ statxbuf: Py<StatxBuffer>,
36
+ }
37
+
38
+ /// Owns an io_uring instance and exposes prep/submit/complete operations.
39
+ ///
40
+ /// Usage from Python:
41
+ /// ```python
42
+ /// with Ring(depth=32) as ring:
43
+ /// ring.prep_nop(user_data=1)
44
+ /// ring.submit()
45
+ /// cqe = ring.wait()
46
+ /// ```
47
+ ///
48
+ #[pyclass]
49
+ struct Ring {
50
+ ring: Option<IoUring>,
51
+ depth: u32,
52
+
53
+ /// Buffers that are currently owned by the kernel (between submit and CQE).
54
+ /// Keyed by `user_data` so they can be released when the CQE arrives.
55
+ ///
56
+ /// The kernel holds raw pointers into these buffers. They must be kept
57
+ /// alive and un-resized until the corresponding CQE is consumed.
58
+ /// TODO: Consolidate into 1.
59
+ pinned_mutable_buffers: HashMap<u64, Py<PyByteArray>>,
60
+
61
+ pinned_immutable_buffers: HashMap<u64, Py<PyBytes>>,
62
+
63
+ /// CStrings for paths passed to openat.
64
+ pinned_paths: HashMap<u64, CString>,
65
+
66
+ /// Timespecs for timeouts.
67
+ pinned_timespecs: HashMap<u64, types::Timespec>,
68
+
69
+ /// Addresses for sockets.
70
+ pinned_sockaddr: HashMap<u64, SockAddrInner>,
71
+
72
+ /// Socket option values. Boxed for pointer stability across HashMap resizes.
73
+ pinned_sockopts: HashMap<u64, Box<i32>>,
74
+
75
+ // Statx buffers
76
+ pinned_statx_buffers: HashMap<u64, StatxRequest>,
77
+ }
78
+
79
+ impl Ring {
80
+ fn uring_mut(&mut self) -> PyResult<&mut IoUring> {
81
+ self.ring
82
+ .as_mut()
83
+ .ok_or_else(|| PyRuntimeError::new_err("Ring not initialised (use as context manager)"))
84
+ }
85
+
86
+ /// Push an entry onto the SQ. Panics if SQ is full.
87
+ fn push_entry(&mut self, entry: io_uring::squeue::Entry) -> PyResult<()> {
88
+ let ring = self.uring_mut()?;
89
+ // SAFETY: we trust that the caller has set up the entry correctly and
90
+ // that any buffers referenced are pinned in `pinned_buffers`.
91
+ unsafe {
92
+ ring.submission()
93
+ .push(&entry)
94
+ .map_err(|_| PyRuntimeError::new_err("Submission queue is full"))?;
95
+ }
96
+ Ok(())
97
+ }
98
+
99
+ /// Release any pinned resources associated with a completed user_data.
100
+ fn release_pinned(&mut self, user_data: u64) {
101
+ self.pinned_mutable_buffers.remove(&user_data);
102
+ self.pinned_immutable_buffers.remove(&user_data);
103
+ self.pinned_paths.remove(&user_data);
104
+ self.pinned_sockaddr.remove(&user_data);
105
+ self.pinned_timespecs.remove(&user_data);
106
+ self.pinned_sockopts.remove(&user_data);
107
+ self.pinned_statx_buffers.remove(&user_data);
108
+ }
109
+
110
+ fn cqe_to_event(&mut self, cqe: &io_uring::cqueue::Entry) -> CompletionEvent {
111
+ let user_data = cqe.user_data();
112
+ self.release_pinned(user_data);
113
+ CompletionEvent {
114
+ user_data,
115
+ res: cqe.result(),
116
+ flags: cqe.flags(),
117
+ }
118
+ }
119
+ }
120
+
121
+ #[pymethods]
122
+ impl Ring {
123
+ #[new]
124
+ #[pyo3(signature = (depth = 32))]
125
+ fn new(depth: u32) -> Self {
126
+ Ring {
127
+ ring: None,
128
+ depth,
129
+ pinned_mutable_buffers: HashMap::new(),
130
+ pinned_immutable_buffers: HashMap::new(),
131
+ pinned_paths: HashMap::new(),
132
+ pinned_timespecs: HashMap::new(),
133
+ pinned_sockaddr: HashMap::new(),
134
+ pinned_sockopts: HashMap::new(),
135
+ pinned_statx_buffers: HashMap::new(),
136
+ }
137
+ }
138
+
139
+ /// Python CM protocol.
140
+ fn __enter__(mut slf: PyRefMut<'_, Self>) -> PyResult<PyRefMut<'_, Self>> {
141
+ let ring = IoUring::new(slf.depth)
142
+ .map_err(|e| PyRuntimeError::new_err(format!("io_uring_setup failed: {e}")))?;
143
+ slf.ring = Some(ring);
144
+ Ok(slf)
145
+ }
146
+
147
+ #[pyo3(signature = (_exc_type=None, _exc_val=None, _exc_tb=None))]
148
+ fn __exit__(
149
+ &mut self,
150
+ _exc_type: Option<&Bound<'_, PyAny>>,
151
+ _exc_val: Option<&Bound<'_, PyAny>>,
152
+ _exc_tb: Option<&Bound<'_, PyAny>>,
153
+ ) -> PyResult<bool> {
154
+ self.pinned_mutable_buffers.clear();
155
+ self.pinned_immutable_buffers.clear();
156
+ self.pinned_paths.clear();
157
+ self.pinned_sockaddr.clear();
158
+ self.pinned_timespecs.clear();
159
+ self.pinned_sockopts.clear();
160
+ self.pinned_statx_buffers.clear();
161
+ self.ring = None; // Drop triggers internal io_uring cleanup
162
+ Ok(false)
163
+ }
164
+
165
+ /// Submit all queued SQEs to the kernel. Returns number submitted.
166
+ fn submit(&mut self) -> PyResult<u32> {
167
+ let n = self
168
+ .uring_mut()?
169
+ .submit()
170
+ .map_err(|e| PyRuntimeError::new_err(format!("io_uring_submit failed: {e}")))?;
171
+ Ok(n as u32)
172
+ }
173
+
174
+ /// Non-blocking peek.
175
+ fn peek(&mut self) -> PyResult<Option<CompletionEvent>> {
176
+ let ring = self.uring_mut()?;
177
+ let cq = ring.completion();
178
+ let cqe = cq.into_iter().next();
179
+ match cqe {
180
+ Some(cqe) => Ok(Some(self.cqe_to_event(&cqe))),
181
+ None => Ok(None),
182
+ }
183
+ }
184
+
185
+ /// Blocking wait for at least one CQE and return it.
186
+ fn wait(&mut self, py: Python<'_>) -> PyResult<CompletionEvent> {
187
+ let ring = self.uring_mut()?;
188
+ py.detach(|| ring.submit_and_wait(1))
189
+ .map_err(|e| PyRuntimeError::new_err(format!("io_uring_wait failed: {e}")))?;
190
+ let cqe = ring
191
+ .completion()
192
+ .next()
193
+ .ok_or_else(|| PyRuntimeError::new_err("No CQE after wait"))?;
194
+ Ok(self.cqe_to_event(&cqe))
195
+ }
196
+
197
+ /// Submit a no-op.
198
+ fn prep_nop(&mut self, user_data: u64) -> PyResult<()> {
199
+ let entry = opcode::Nop::new().build().user_data(user_data);
200
+ self.push_entry(entry)
201
+ }
202
+
203
+ /// Submit a timeout (sleep).
204
+ fn prep_timeout(&mut self, user_data: u64, sec: u64, nsec: u32) -> PyResult<()> {
205
+ let timespec = types::Timespec::new().sec(sec).nsec(nsec);
206
+ self.pinned_timespecs.insert(user_data, timespec);
207
+ let ts = self.pinned_timespecs.get(&user_data).unwrap();
208
+
209
+ let entry = opcode::Timeout::new(ts).build().user_data(user_data);
210
+
211
+ self.push_entry(entry)
212
+ }
213
+
214
+ /// Prep a read into `buf`.
215
+ /// The `buf` (a Python `bytearray`) is pinned until the CQE is consumed.
216
+ /// **Do not resize `buf` between prep and consuming the CQE.**
217
+ #[pyo3(signature = (user_data, fd, buf, nbytes, offset))]
218
+ fn prep_read(
219
+ &mut self,
220
+ _py: Python<'_>,
221
+ user_data: u64,
222
+ fd: RawFd,
223
+ buf: Bound<'_, PyByteArray>,
224
+ nbytes: u32,
225
+ offset: u64,
226
+ ) -> PyResult<()> {
227
+ let ptr = buf.data();
228
+ let len = nbytes.min(buf.len() as u32);
229
+
230
+ let entry = opcode::Read::new(types::Fd(fd), ptr.cast(), len)
231
+ .offset(offset)
232
+ .build()
233
+ .user_data(user_data);
234
+
235
+ self.pinned_mutable_buffers.insert(user_data, buf.unbind());
236
+ self.push_entry(entry)
237
+ }
238
+
239
+ // Prepares statx for metadata extraction.
240
+ fn prep_statx(
241
+ &mut self,
242
+ user_data: u64,
243
+ path: &str,
244
+ buf: Bound<'_, StatxBuffer>,
245
+ flags: i32,
246
+ mask: u32,
247
+ dir_fd: RawFd,
248
+ ) -> PyResult<()> {
249
+ let c_path =
250
+ CString::new(path).map_err(|_| PyRuntimeError::new_err("Path contains null byte"))?;
251
+ let path_ptr = c_path.as_ptr();
252
+
253
+ let mut guard = buf.borrow_mut();
254
+ let statxbuf_ptr = &mut *guard.inner as *mut libc::statx as *mut io_uring::types::statx;
255
+
256
+ let entry = opcode::Statx::new(types::Fd(dir_fd), path_ptr, statxbuf_ptr)
257
+ .flags(flags)
258
+ .mask(mask)
259
+ .build()
260
+ .user_data(user_data);
261
+
262
+ drop(guard);
263
+
264
+ self.pinned_statx_buffers.insert(
265
+ user_data,
266
+ StatxRequest {
267
+ path: c_path,
268
+ statxbuf: buf.unbind(),
269
+ },
270
+ );
271
+ self.push_entry(entry)
272
+ }
273
+
274
+ /// Prep a file write.
275
+ #[pyo3(signature = (user_data, fd, buf, offset))]
276
+ fn prep_write(
277
+ &mut self,
278
+ _py: Python<'_>,
279
+ user_data: u64,
280
+ fd: RawFd,
281
+ buf: Bound<'_, PyBytes>,
282
+ offset: u64,
283
+ ) -> PyResult<()> {
284
+ let data = buf.as_bytes();
285
+ let ptr = data.as_ptr();
286
+ let len = data.len() as u32;
287
+
288
+ let entry = opcode::Write::new(types::Fd(fd), ptr.cast(), len)
289
+ .offset(offset)
290
+ .build()
291
+ .user_data(user_data);
292
+
293
+ self.pinned_immutable_buffers
294
+ .insert(user_data, buf.unbind());
295
+ self.push_entry(entry)
296
+ }
297
+
298
+ /// Prep a file open.
299
+ #[pyo3(signature = (user_data, path, flags, mode, dir_fd))]
300
+ fn prep_openat(
301
+ &mut self,
302
+ user_data: u64,
303
+ path: &str,
304
+ flags: i32,
305
+ mode: u32,
306
+ dir_fd: RawFd,
307
+ ) -> PyResult<()> {
308
+ let c_path =
309
+ CString::new(path).map_err(|_| PyRuntimeError::new_err("Path contains null byte"))?;
310
+ let ptr = c_path.as_ptr();
311
+
312
+ let entry = opcode::OpenAt::new(types::Fd(dir_fd), ptr)
313
+ .flags(flags)
314
+ .mode(mode)
315
+ .build()
316
+ .user_data(user_data);
317
+
318
+ // Pin the CString so the pointer stays valid until CQE
319
+ self.pinned_paths.insert(user_data, c_path);
320
+ self.push_entry(entry)
321
+ }
322
+
323
+ /// Prep a file/socket close.
324
+ fn prep_close(&mut self, user_data: u64, fd: RawFd) -> PyResult<()> {
325
+ let entry = opcode::Close::new(types::Fd(fd))
326
+ .build()
327
+ .user_data(user_data);
328
+ self.push_entry(entry)
329
+ }
330
+
331
+ /// Prep a cancellation of another in-flight operation.
332
+ #[pyo3(signature = (user_data, target_user_data, flags = 0))]
333
+ fn prep_cancel(&mut self, user_data: u64, target_user_data: u64, flags: i32) -> PyResult<()> {
334
+ let entry = opcode::AsyncCancel::new(target_user_data)
335
+ .build()
336
+ .user_data(user_data);
337
+ // TODO: use flags once io-uring crate exposes cancel flags
338
+ let _ = flags;
339
+ self.push_entry(entry)
340
+ }
341
+
342
+ /// Prep a socket creation.
343
+ #[pyo3(signature = (user_data, domain, sock_type, protocol = 0, flags = 0))]
344
+ fn prep_socket(
345
+ &mut self,
346
+ user_data: u64,
347
+ domain: i32,
348
+ sock_type: i32,
349
+ protocol: i32,
350
+ flags: u32,
351
+ ) -> PyResult<()> {
352
+ let entry = opcode::Socket::new(domain, sock_type, protocol)
353
+ .build()
354
+ .user_data(user_data);
355
+ let _ = flags; // TODO: pass flags if opcode supports it
356
+ self.push_entry(entry)
357
+ }
358
+
359
+ /// Prep a recv from a connected socket into `buf`.
360
+ #[pyo3(signature = (user_data, fd, buf, flags = 0))]
361
+ fn prep_socket_recv(
362
+ &mut self,
363
+ _py: Python<'_>,
364
+ user_data: u64,
365
+ fd: RawFd,
366
+ buf: Bound<'_, PyByteArray>,
367
+ flags: u32,
368
+ ) -> PyResult<()> {
369
+ let ptr = buf.data();
370
+ let len = buf.len() as u32;
371
+
372
+ let entry = opcode::Recv::new(types::Fd(fd), ptr.cast(), len)
373
+ .flags(flags as i32)
374
+ .build()
375
+ .user_data(user_data);
376
+
377
+ self.pinned_mutable_buffers.insert(user_data, buf.unbind());
378
+ self.push_entry(entry)
379
+ }
380
+
381
+ /// Prep a send to a connected socket.
382
+ #[pyo3(signature = (user_data, fd, buf, flags = 0))]
383
+ fn prep_socket_send(
384
+ &mut self,
385
+ _py: Python<'_>,
386
+ user_data: u64,
387
+ fd: RawFd,
388
+ buf: Bound<'_, PyBytes>,
389
+ flags: u32,
390
+ ) -> PyResult<()> {
391
+ let data = buf.as_bytes();
392
+ let ptr = data.as_ptr();
393
+ let len = data.len() as u32;
394
+
395
+ let entry = opcode::Send::new(types::Fd(fd), ptr.cast(), len)
396
+ .flags(flags as i32)
397
+ .build()
398
+ .user_data(user_data);
399
+
400
+ self.pinned_immutable_buffers
401
+ .insert(user_data, buf.unbind());
402
+ self.push_entry(entry)
403
+ }
404
+
405
+ /// Preps to bind to a socket.
406
+ fn prep_socket_bind(
407
+ &mut self,
408
+ _py: Python<'_>,
409
+ user_data: u64,
410
+ fd: RawFd,
411
+ sock_addr: SockAddr,
412
+ ) -> PyResult<()> {
413
+ self.pinned_sockaddr.insert(user_data, sock_addr.inner);
414
+
415
+ let stored = self.pinned_sockaddr.get(&user_data).unwrap();
416
+ let (ptr, len) = stored.as_ptr_and_len();
417
+
418
+ let entry = opcode::Bind::new(types::Fd(fd), ptr, len)
419
+ .build()
420
+ .user_data(user_data);
421
+
422
+ self.push_entry(entry)
423
+ }
424
+
425
+ /// Prepares a socket to listen. Marks it as passive.
426
+ fn prep_socket_listen(
427
+ &mut self,
428
+ _py: Python<'_>,
429
+ user_data: u64,
430
+ fd: RawFd,
431
+ backlog: i32,
432
+ ) -> PyResult<()> {
433
+ let entry = opcode::Listen::new(types::Fd(fd), backlog)
434
+ .build()
435
+ .user_data(user_data);
436
+
437
+ self.push_entry(entry)
438
+ }
439
+
440
+ /// Prepares a socket to accept an incoming connection.
441
+ /// TODO: Add sockaddr for kernel to fill, for logging who connected.
442
+ fn prep_socket_accept(&mut self, _py: Python<'_>, user_data: u64, fd: RawFd) -> PyResult<()> {
443
+ let entry = opcode::Accept::new(types::Fd(fd), std::ptr::null_mut(), std::ptr::null_mut())
444
+ .build()
445
+ .user_data(user_data);
446
+
447
+ self.push_entry(entry)
448
+ }
449
+
450
+ /// Connects to a socket from a client.
451
+ fn prep_socket_connect(
452
+ &mut self,
453
+ _py: Python<'_>,
454
+ user_data: u64,
455
+ fd: RawFd,
456
+ sock_addr: SockAddr,
457
+ ) -> PyResult<()> {
458
+ self.pinned_sockaddr.insert(user_data, sock_addr.inner);
459
+
460
+ let stored = self.pinned_sockaddr.get(&user_data).unwrap();
461
+ let (ptr, len) = stored.as_ptr_and_len();
462
+
463
+ let entry = opcode::Connect::new(types::Fd(fd), ptr, len)
464
+ .build()
465
+ .user_data(user_data);
466
+
467
+ self.push_entry(entry)
468
+ }
469
+
470
+ /// Set socket options.
471
+ fn prep_socket_setopt(&mut self, user_data: u64, fd: RawFd) -> PyResult<()> {
472
+ // TODO: Hardcoded for now.
473
+ let optval = Box::new(1i32); // SO_REUSEADDR value
474
+ self.pinned_sockopts.insert(user_data, optval);
475
+ let pinned = self.pinned_sockopts.get(&user_data).unwrap();
476
+
477
+ let entry = opcode::SetSockOpt::new(
478
+ types::Fd(fd),
479
+ libc::SOL_SOCKET as u32,
480
+ libc::SO_REUSEADDR as u32,
481
+ pinned.as_ref() as *const i32 as *const libc::c_void,
482
+ std::mem::size_of::<i32>() as u32,
483
+ )
484
+ .build()
485
+ .user_data(user_data);
486
+
487
+ self.push_entry(entry)
488
+ }
489
+ }
490
+
491
+ //TODO: Move to another module.
492
+ #[derive(Clone)]
493
+ enum SockAddrInner {
494
+ V4(libc::sockaddr_in),
495
+ V6(libc::sockaddr_in6),
496
+ }
497
+
498
+ impl SockAddrInner {
499
+ /// Returns address as pointer as well as its length.
500
+ fn as_ptr_and_len(&self) -> (*const libc::sockaddr, u32) {
501
+ match &self {
502
+ SockAddrInner::V4(addr) => (
503
+ addr as *const libc::sockaddr_in as *const libc::sockaddr,
504
+ std::mem::size_of::<libc::sockaddr_in>() as u32,
505
+ ),
506
+ SockAddrInner::V6(addr) => (
507
+ addr as *const libc::sockaddr_in6 as *const libc::sockaddr,
508
+ std::mem::size_of::<libc::sockaddr_in6>() as u32,
509
+ ),
510
+ }
511
+ }
512
+ }
513
+
514
+ #[pyclass]
515
+ #[derive(Clone)]
516
+ struct SockAddr {
517
+ inner: SockAddrInner,
518
+ }
519
+
520
+ #[pymethods]
521
+ impl SockAddr {
522
+ #[staticmethod]
523
+ fn v4(ip: &str, port: u16) -> PyResult<Self> {
524
+ let addr_parsed: Ipv4Addr = ip
525
+ .parse()
526
+ .map_err(|e| PyValueError::new_err(format!("Invalid IPv4 address: {e}")))?;
527
+
528
+ let mut addr: libc::sockaddr_in = unsafe { std::mem::zeroed() };
529
+ addr.sin_family = libc::AF_INET as u16;
530
+ addr.sin_addr.s_addr = u32::from_ne_bytes(addr_parsed.octets());
531
+ addr.sin_port = port.to_be();
532
+ Ok(SockAddr {
533
+ inner: SockAddrInner::V4(addr),
534
+ })
535
+ }
536
+
537
+ #[staticmethod]
538
+ fn v6(ip: &str, port: u16) -> PyResult<Self> {
539
+ let addr_parsed: Ipv6Addr = ip
540
+ .parse()
541
+ .map_err(|e| PyValueError::new_err(format!("Invalid IPv6 address: {e}")))?;
542
+
543
+ let mut addr: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
544
+ addr.sin6_family = libc::AF_INET6 as u16;
545
+ addr.sin6_addr.s6_addr = addr_parsed.octets();
546
+ addr.sin6_port = port.to_be();
547
+ Ok(SockAddr {
548
+ inner: SockAddrInner::V6(addr),
549
+ })
550
+ }
551
+ }
552
+
553
+ #[pyclass]
554
+ #[derive(Clone, Debug)]
555
+ struct StatxBuffer {
556
+ inner: Box<libc::statx>,
557
+ }
558
+
559
+ #[pymethods]
560
+ impl StatxBuffer {
561
+ #[new]
562
+ fn new() -> Self {
563
+ StatxBuffer {
564
+ inner: Box::new(unsafe { std::mem::zeroed() }),
565
+ }
566
+ }
567
+
568
+ #[getter]
569
+ fn size(&self) -> u64 {
570
+ self.inner.stx_size
571
+ }
572
+
573
+ #[getter]
574
+ fn mtime_sec(&self) -> i64 {
575
+ self.inner.stx_mtime.tv_sec
576
+ }
577
+
578
+ #[getter]
579
+ fn ino(&self) -> u64 {
580
+ self.inner.stx_ino
581
+ }
582
+
583
+ #[getter]
584
+ fn mode(&self) -> u32 {
585
+ self.inner.stx_mode as u32
586
+ }
587
+ }
588
+
589
+ fn register_constants(m: &Bound<'_, PyModule>) -> PyResult<()> {
590
+ // File open flags
591
+ m.add("O_RDONLY", libc::O_RDONLY)?;
592
+ m.add("O_WRONLY", libc::O_WRONLY)?;
593
+ m.add("O_RDWR", libc::O_RDWR)?;
594
+ m.add("O_CREAT", libc::O_CREAT)?;
595
+ m.add("O_TRUNC", libc::O_TRUNC)?;
596
+ m.add("O_APPEND", libc::O_APPEND)?;
597
+ m.add("O_NONBLOCK", libc::O_NONBLOCK)?;
598
+ m.add("O_CLOEXEC", libc::O_CLOEXEC)?;
599
+
600
+ // File mode bits (permissions)
601
+ m.add("S_IRUSR", libc::S_IRUSR)?;
602
+ m.add("S_IWUSR", libc::S_IWUSR)?;
603
+ m.add("S_IXUSR", libc::S_IXUSR)?;
604
+ m.add("S_IRGRP", libc::S_IRGRP)?;
605
+ m.add("S_IWGRP", libc::S_IWGRP)?;
606
+ m.add("S_IXGRP", libc::S_IXGRP)?;
607
+ m.add("S_IROTH", libc::S_IROTH)?;
608
+ m.add("S_IWOTH", libc::S_IWOTH)?;
609
+ m.add("S_IXOTH", libc::S_IXOTH)?;
610
+
611
+ // File type bits (from st_mode / stx_mode)
612
+ m.add("S_IFREG", libc::S_IFREG)?;
613
+ m.add("S_IFDIR", libc::S_IFDIR)?;
614
+ m.add("S_IFLNK", libc::S_IFLNK)?;
615
+ m.add("S_IFSOCK", libc::S_IFSOCK)?;
616
+ m.add("S_IFIFO", libc::S_IFIFO)?;
617
+ m.add("S_IFMT", libc::S_IFMT)?;
618
+
619
+ // AT flags (path resolution)
620
+ m.add("AT_FDCWD", libc::AT_FDCWD)?;
621
+ m.add("AT_EMPTY_PATH", libc::AT_EMPTY_PATH)?;
622
+ m.add("AT_SYMLINK_NOFOLLOW", libc::AT_SYMLINK_NOFOLLOW)?;
623
+
624
+ // Statx mask (which fields to populate)
625
+ m.add("STATX_TYPE", libc::STATX_TYPE)?;
626
+ m.add("STATX_MODE", libc::STATX_MODE)?;
627
+ m.add("STATX_INO", libc::STATX_INO)?;
628
+ m.add("STATX_SIZE", libc::STATX_SIZE)?;
629
+ m.add("STATX_MTIME", libc::STATX_MTIME)?;
630
+ m.add("STATX_ATIME", libc::STATX_ATIME)?;
631
+ m.add("STATX_CTIME", libc::STATX_CTIME)?;
632
+ m.add("STATX_ALL", libc::STATX_ALL)?;
633
+
634
+ // Socket: address families
635
+ m.add("AF_INET", libc::AF_INET)?;
636
+ m.add("AF_INET6", libc::AF_INET6)?;
637
+ m.add("AF_UNIX", libc::AF_UNIX)?;
638
+
639
+ // Socket: types
640
+ m.add("SOCK_STREAM", libc::SOCK_STREAM)?;
641
+ m.add("SOCK_DGRAM", libc::SOCK_DGRAM)?;
642
+ m.add("SOCK_NONBLOCK", libc::SOCK_NONBLOCK)?;
643
+ m.add("SOCK_CLOEXEC", libc::SOCK_CLOEXEC)?;
644
+
645
+ // Socket: protocol levels and options
646
+ m.add("SOL_SOCKET", libc::SOL_SOCKET)?;
647
+ m.add("SO_REUSEADDR", libc::SO_REUSEADDR)?;
648
+ m.add("SO_REUSEPORT", libc::SO_REUSEPORT)?;
649
+ m.add("SO_KEEPALIVE", libc::SO_KEEPALIVE)?;
650
+ m.add("IPPROTO_TCP", libc::IPPROTO_TCP)?;
651
+ m.add("TCP_NODELAY", libc::TCP_NODELAY)?;
652
+
653
+ // Socket: send/recv flags
654
+ m.add("MSG_NOSIGNAL", libc::MSG_NOSIGNAL)?;
655
+ m.add("MSG_DONTWAIT", libc::MSG_DONTWAIT)?;
656
+
657
+ // Signals
658
+ m.add("SIGINT", libc::SIGINT)?;
659
+ m.add("SIGTERM", libc::SIGTERM)?;
660
+ m.add("SIGHUP", libc::SIGHUP)?;
661
+
662
+ // Signalfd flags
663
+ m.add("SFD_NONBLOCK", libc::SFD_NONBLOCK)?;
664
+ m.add("SFD_CLOEXEC", libc::SFD_CLOEXEC)?;
665
+
666
+ Ok(())
667
+ }
668
+
669
+ #[pymodule]
670
+ fn _rusty_ring(m: &Bound<'_, PyModule>) -> PyResult<()> {
671
+ m.add_class::<Ring>()?;
672
+ m.add_class::<CompletionEvent>()?;
673
+ m.add_class::<SockAddr>()?;
674
+ m.add_class::<StatxBuffer>()?;
675
+
676
+ register_constants(m)?;
677
+ Ok(())
678
+ }
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ println!("Hello, world!");
3
+ }
@@ -0,0 +1,77 @@
1
+ import threading
2
+ import time
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from typing import TYPE_CHECKING
5
+
6
+ from one_ring_loop.log import get_logger
7
+ from rusty_ring import Ring
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ SERVER_MESSAGE = b"A new client connected!"
15
+
16
+
17
+ class TestRing:
18
+ def test_ring_context_manager(self) -> None:
19
+ with Ring(32) as ring:
20
+ assert isinstance(ring, Ring)
21
+
22
+ def test_file_open_write_read(self, tmp_file_path: Path) -> None:
23
+ file_content = b"Hello! :)"
24
+
25
+ with Ring(32) as ring:
26
+ ring.prep_openat(
27
+ user_data=0, path=str(tmp_file_path), flags=66, mode=432, dir_fd=-100
28
+ )
29
+ ring.submit()
30
+ open_event = ring.wait()
31
+ logger.info("Got open event", io_event=open_event)
32
+ fd = open_event.res
33
+
34
+ write_buf = bytes(file_content)
35
+ ring.prep_write(0, fd=fd, buf=write_buf, offset=0)
36
+ ring.submit()
37
+ write_event = ring.wait()
38
+ logger.info("Got write event", io_event=write_event)
39
+
40
+ read_buf = bytearray(9)
41
+ ring.prep_read(0, fd=fd, buf=read_buf, nbytes=9, offset=0)
42
+ ring.submit()
43
+ read_event = ring.wait()
44
+ logger.info("Got read event", io_event=read_event)
45
+ assert bytes(read_buf) == file_content
46
+
47
+ def test_timeout(self, timing) -> None:
48
+ with Ring(32) as ring:
49
+ sleep_for_sec = 1
50
+ sleep_for_nsec = int(5e8) # 0.5 seconds.
51
+ ring.prep_timeout(0, sleep_for_sec, sleep_for_nsec)
52
+ ring.submit()
53
+
54
+ timing.start()
55
+ ring.wait()
56
+ timing.assert_elapsed_between(1.5, 1.6, msg="Should sleep for 2 secounds")
57
+
58
+ def test_wait_does_not_hold_gil(self) -> None:
59
+ flag = threading.Event()
60
+
61
+ def background() -> None:
62
+ logger.info("Sleeping for 0.5 seconds")
63
+ time.sleep(0.5)
64
+ logger.info("Setting flag")
65
+ flag.set()
66
+
67
+ with Ring(32) as ring, ThreadPoolExecutor(max_workers=2) as executor:
68
+ ring.prep_timeout(0, sec=1, nsec=0)
69
+ ring.submit()
70
+
71
+ logger.info("Submitting background function")
72
+ executor.submit(background)
73
+ logger.info("Waiting on ring timeout")
74
+ ring.wait()
75
+ logger.info("Finished waiting")
76
+
77
+ assert flag.is_set()