├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── examples
├── CatExample.java
├── HttpEchoServer.java
├── HttpHelloWorldServer.java
├── HttpReadmeFileServer.java
├── ParallelHttpEchoServer.java
└── TcpReverseProxyExample.java
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
├── c
│ ├── liburing_file_provider.c
│ ├── liburing_file_provider.h
│ ├── liburing_provider.c
│ ├── liburing_provider.h
│ ├── liburing_socket_provider.c
│ └── liburing_socket_provider.h
└── java
│ └── sh
│ └── blake
│ └── niouring
│ ├── AbstractIoUringChannel.java
│ ├── AbstractIoUringSocket.java
│ ├── IoUring.java
│ ├── IoUringFile.java
│ ├── IoUringServerSocket.java
│ ├── IoUringSocket.java
│ └── util
│ ├── ByteBufferUtil.java
│ ├── NativeLibraryLoader.java
│ ├── OsVersionCheck.java
│ └── ReferenceCounter.java
└── test
├── java
└── sh
│ └── blake
│ └── niouring
│ ├── IoUringFileTest.java
│ ├── IoUringSocketTest.java
│ └── TestBase.java
└── resources
└── test-file.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | .idea/
3 | .gradle/
4 | gradle.properties
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Blake Beaupain
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nio_uring
2 |
3 | `nio_uring` is an I/O library for Java that uses [io_uring](https://en.wikipedia.org/wiki/Io_uring) under the hood, which aims to be:
4 |
5 | * A simple and flexible API
6 | * Super fast and efficient
7 | * Truly zero-copy (the kernel addresses direct `ByteBuffer`s for I/O operations)
8 | * Slightly opinionated
9 |
10 | Feedback, suggestions, and contributions are most welcome!
11 |
12 | ## Requirements
13 | * Linux >= 5.1
14 | * Java >= 8
15 |
16 | For both of these, the higher the version the better - free performance!
17 |
18 | ## Maven Usage
19 |
20 | ```xml
21 |
22 | sh.blake.niouring
23 | nio_uring
24 | 0.1.4
25 |
26 | ```
27 |
28 | ## TCP Server Example
29 |
30 | Here's a basic HTTP server from `sh.blake.niouring.examples.HttpHelloWorldServer`. There are a few other examples in the same package.
31 |
32 | ```java
33 | public static void main(String[] args) {
34 | String response = "HTTP/1.1 200 OK\r\n\r\nHello, world!";
35 | ByteBuffer responseBuffer = ByteBufferUtil.wrapDirect(response);
36 |
37 | IoUringServerSocket serverSocket = new IoUringServerSocket(8080);
38 | serverSocket.onAccept((ring, socket) -> {
39 | // queue another accept request for the next client
40 | ring.queueAccept(serverSocket);
41 |
42 | // set up the read handler and queue a read operation
43 | socket.onRead(in -> ring.queueWrite(socket, responseBuffer.slice()));
44 | ring.queueRead(socket, ByteBuffer.allocateDirect(1024));
45 |
46 | // HTTP spec says the server should close when done
47 | socket.onWrite(out -> socket.close());
48 |
49 | // and some really basic error handling
50 | socket.onException(ex -> {
51 | ex.printStackTrace();
52 | socket.close();
53 | });
54 | });
55 |
56 | new IoUring()
57 | .onException(Exception::printStackTrace)
58 | .queueAccept(serverSocket) // queue an accept request, onAccept will be called when a socket connects
59 | .loop(); // process I/O events until interrupted
60 | }
61 | ```
62 |
63 | ## File Support
64 |
65 | A barebones `cat` implementation from `sh.blake.niouring.examples.CatExample`:
66 | ```java
67 | public static void main(String[] args) {
68 | IoUringFile file = new IoUringFile(args[0]);
69 | file.onRead(in -> {
70 | System.out.println(StandardCharsets.UTF_8.decode(in));
71 | file.close();
72 | });
73 | new IoUring()
74 | .queueRead(file, ByteBuffer.allocateDirect(8192))
75 | .execute(); // process at least one I/O event (blocking until complete)
76 | }
77 | ```
78 |
79 | There's also an example HTTP server that will respond with this README in the examples package!
80 |
81 | ## Performance Tuning
82 |
83 | Pretty much all performance tuning is done with one knob - the `ringSize` argument to the `IoUring` constructor, which has a default value of 512 if not provided. This value controls the number of outstanding I/O events (accepts, reads, and writes) at any given time. It is constrained by `memlock` limits (`ulimit -l`) which can be increased as necessary. Don't forget about file descriptor limits (`ulimit -n`) too!
84 |
85 | Beyond this, you will have to run multiple rings across multiple threads. See `sh.blake.niouring.examples.ParallelHttpEchoServer` for a simple starter using `java.util.concurrent` APIs.
86 |
87 | ## Caveats / Warnings
88 |
89 | ### Thread safety
90 |
91 | As of now, you should only call read/write/close operations from an `IoUring` handler (`onAccept`, `onRead`, `onWrite`, etc). This is because `nio_uring` uses `liburing` under the hood and its internal submission/completion system is shared and not thread safe. We understand this is an important feature and are working on an efficient way to wake up the main ring loop and have it wait for external threads to perform modifications before resuming.
92 |
93 | ### Runtime Exceptions
94 |
95 | `nio_uring` holds the opinion that checked exceptions, as a concept, were probably a mistake. Therefore, all exceptions produced by `nio_uring` will extend `java.lang.RuntimeException` and will always be forwarded to the appropriate exception handler if generated intentionally (see `socket.onException`).
96 |
97 | ### Direct buffers
98 |
99 | All `ByteBuffer` instances used with this library must be direct (e.g. allocated with `allocateDirect`). This is a hard requirement and is what enables zero-copy functionality.
100 |
101 | ### Multiple reads/writes
102 | Queuing multiple operations is fine, but try not to queue a read/write operation for _the same_ buffer to the _same_ socket more than once per ring execution, because the internal mapping system is not designed to handle this. The data will be read/written, but your handler will only be called for the first operation, and an `java.lang.IllegalStateException` exception with message "Buffer already removed" will be sent to the exception handler for the second.
103 |
104 | ## Building
105 |
106 | Set the `LIBURING_PATH` environment variable to the root of a fully compiled liburing directory.
107 |
108 | Then `./gradlew build` and you're off!
109 |
110 | ## License
111 |
112 | MIT. Have fun and make cool things!
113 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'c'
4 | id 'jacoco'
5 | id 'maven'
6 | id 'signing'
7 | id 'maven-publish'
8 | }
9 |
10 | group 'sh.blake.niouring'
11 | version '0.1.4'
12 |
13 | sourceCompatibility = 1.8
14 |
15 | task wrapper(type: Wrapper) {
16 | gradleVersion = '4.10'
17 | }
18 |
19 | repositories {
20 | mavenCentral()
21 | }
22 |
23 | dependencies {
24 | testCompile group: 'junit', name: 'junit', version: '4.12'
25 | implementation group: "org.eclipse.collections", name: "eclipse-collections-api", version: "11.1.0"
26 | implementation group: "org.eclipse.collections", name: "eclipse-collections", version: "11.1.0"
27 | }
28 |
29 | test {
30 | testLogging.showStandardStreams = true
31 | systemProperty "java.library.path", file("${buildDir}/libs/nio_uring/shared").absolutePath
32 | }
33 |
34 | jacoco {
35 | toolVersion = "0.8.6"
36 | }
37 |
38 | processResources {
39 | from "${buildDir}/libs/nio_uring/shared/"
40 | }
41 |
42 | uploadArchives {
43 | repositories {
44 | mavenDeployer {
45 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
46 | repository(url: "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") {
47 | authentication(userName: ossrhUsername, password: ossrhPassword)
48 | }
49 | snapshotRepository(url: "https://s01.oss.sonatype.org/content/repositories/snapshots/") {
50 | authentication(userName: ossrhUsername, password: ossrhPassword)
51 | }
52 | pom.project {
53 | name = "nio_uring"
54 | description = "I/O library for Java using io_uring under the hood"
55 | url = "https://github.com/bbeaupain/nio_uring"
56 | inceptionYear = "2022"
57 | packaging = "jar"
58 | licenses {
59 | license {
60 | name = "The MIT License"
61 | url = "https://opensource.org/licenses/MIT"
62 | distribution = "repo"
63 | }
64 | }
65 | developers {
66 | developer {
67 | name = "Blake Beaupain"
68 | email = "me@blake.sh"
69 | organization = "Blake Beaupain"
70 | organizationUrl = "https://www.blake.sh"
71 | }
72 | }
73 | scm {
74 | connection = "scm:git:git://github.com/bbeaupain/nio_uring.git"
75 | developerConnection = "scm:git:git://github.com/bbeaupain/nio_uring.git"
76 | url = "https://github.com/bbeaupain/nio_uring"
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | task sourcesJar(type: Jar, dependsOn: classes) {
84 | classifier = 'sources'
85 | from sourceSets.main.allSource
86 | }
87 |
88 | task javadocJar(type: Jar, dependsOn: javadoc) {
89 | classifier = 'javadoc'
90 | from javadoc.destinationDir
91 | }
92 |
93 | publishing {
94 | publications {
95 | mavenJava(MavenPublication) {
96 | from components.java
97 |
98 | artifact sourcesJar {
99 | classifier "sources"
100 | }
101 | }
102 | }
103 | }
104 |
105 | artifacts {
106 | archives sourcesJar
107 | archives javadocJar
108 | }
109 |
110 | signing {
111 | sign configurations.archives
112 | }
113 |
114 | model {
115 | binaries {
116 | all {
117 | if (toolChain in Gcc) {
118 | cCompiler.args "-O3"
119 | }
120 | }
121 | }
122 |
123 | repositories {
124 | libs(PrebuiltLibraries) {
125 | jdk {
126 | headers.srcDirs "${System.properties['java.home']}/include",
127 | "${System.properties['java.home']}/include/win32",
128 | "${System.properties['java.home']}/include/darwin",
129 | "${System.properties['java.home']}/include/linux"
130 | }
131 |
132 | liburing {
133 | headers.srcDirs "${System.getenv("LIBURING_PATH")}/src",
134 | "${System.getenv("LIBURING_PATH")}/src/include",
135 | "${System.getenv("LIBURING_PATH")}/src/include/liburing"
136 | binaries.withType(PrebuiltStaticLibraryBinary) {
137 | staticLibraryFile = file("${System.getenv("LIBURING_PATH")}/src/liburing.a")
138 | }
139 | }
140 | }
141 | }
142 |
143 | platforms {
144 | x64 { architecture "x86_64" }
145 | x86 { architecture "x86" }
146 | }
147 |
148 | components {
149 | nio_uring(NativeLibrarySpec) {
150 | sources {
151 | c {
152 | source {
153 | lib library: 'nio_uring', linkage: 'static'
154 | lib library: 'liburing', linkage: 'static'
155 | lib library: 'jdk', linkage: 'api'
156 | srcDir "src/main/c"
157 | include "**/*.c"
158 | }
159 | }
160 | }
161 | }
162 | }
163 | }
164 |
165 | build.dependsOn("nio_uringSharedLibrary")
166 | test.dependsOn("nio_uringSharedLibrary")
167 | processResources.dependsOn("nio_uringSharedLibrary")
--------------------------------------------------------------------------------
/examples/CatExample.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.examples;
2 |
3 | import sh.blake.niouring.IoUring;
4 | import sh.blake.niouring.IoUringFile;
5 |
6 | import java.nio.ByteBuffer;
7 | import java.nio.charset.StandardCharsets;
8 |
9 | public class CatExample {
10 | public static void main(String[] args) {
11 | IoUringFile file = new IoUringFile(args[0]);
12 | file.onRead(in -> {
13 | System.out.println(StandardCharsets.UTF_8.decode(in));
14 | file.close();
15 | });
16 | new IoUring()
17 | .queueRead(file, ByteBuffer.allocateDirect(8192))
18 | .execute(); // process at least one I/O event (blocking until complete)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/HttpEchoServer.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.examples;
2 |
3 | import sh.blake.niouring.IoUring;
4 | import sh.blake.niouring.IoUringServerSocket;
5 | import sh.blake.niouring.util.ByteBufferUtil;
6 |
7 | import java.nio.ByteBuffer;
8 |
9 | public class HttpEchoServer {
10 | private static final ByteBuffer RESPONSE_LINE_BUFFER = ByteBufferUtil.wrapDirect("HTTP/1.1 200 OK\r\n\r\n");
11 |
12 | public static void main(String[] args) {
13 | IoUringServerSocket serverSocket = new IoUringServerSocket(8080);
14 | serverSocket.onAccept((ring, socket) -> {
15 | ring.queueAccept(serverSocket);
16 |
17 | socket.onRead(in -> {
18 | ring.queueWrite(socket, RESPONSE_LINE_BUFFER.slice());
19 | ring.queueWrite(socket, in);
20 | });
21 | ring.queueRead(socket, ByteBuffer.allocateDirect(1024));
22 |
23 | socket.onWrite(out -> socket.close());
24 | socket.onException(ex -> socket.close());
25 | });
26 |
27 | new IoUring()
28 | .onException(Exception::printStackTrace)
29 | .queueAccept(serverSocket)
30 | .loop();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/HttpHelloWorldServer.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.examples;
2 |
3 | import sh.blake.niouring.IoUring;
4 | import sh.blake.niouring.IoUringServerSocket;
5 | import sh.blake.niouring.util.ByteBufferUtil;
6 |
7 | import java.nio.ByteBuffer;
8 |
9 | public class HttpHelloWorldServer {
10 | public static void main(String[] args) {
11 | String response = "HTTP/1.1 200 OK\r\n\r\nHello, world!";
12 | ByteBuffer responseBuffer = ByteBufferUtil.wrapDirect(response);
13 |
14 | IoUringServerSocket serverSocket = new IoUringServerSocket(8080);
15 | serverSocket.onAccept((ring, socket) -> {
16 | // queue another accept request for the next client
17 | ring.queueAccept(serverSocket);
18 |
19 | // set up the read handler and queue a read operation
20 | socket.onRead(in -> ring.queueWrite(socket, responseBuffer.slice()));
21 | ring.queueRead(socket, ByteBuffer.allocateDirect(1024));
22 |
23 | // HTTP spec says the server should close when done
24 | socket.onWrite(out -> socket.close());
25 |
26 | // and some really basic error handling
27 | socket.onException(ex -> {
28 | ex.printStackTrace();
29 | socket.close();
30 | });
31 | });
32 |
33 | new IoUring()
34 | .onException(Exception::printStackTrace)
35 | .queueAccept(serverSocket) // queue an accept request, onAccept will be called when a socket connects
36 | .loop(); // process I/O events until interrupted
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/HttpReadmeFileServer.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.examples;
2 |
3 | import sh.blake.niouring.IoUring;
4 | import sh.blake.niouring.IoUringFile;
5 | import sh.blake.niouring.IoUringServerSocket;
6 | import sh.blake.niouring.util.ByteBufferUtil;
7 |
8 | import java.nio.ByteBuffer;
9 |
10 | public class HttpReadmeFileServer {
11 | public static void main(String[] args) {
12 | IoUring ioUring = new IoUring().onException(Exception::printStackTrace);
13 |
14 | IoUringFile readmeFile = new IoUringFile("README.md");
15 | readmeFile.onRead(readmeBuffer -> {
16 | readmeFile.close();
17 |
18 | ByteBuffer responseLine = ByteBufferUtil.wrapDirect(
19 | "HTTP/1.1 200 OK\r\n" +
20 | "Content-Length: " + readmeBuffer.remaining() + "\r\n\r\n"
21 | );
22 |
23 | IoUringServerSocket serverSocket = new IoUringServerSocket(8080);
24 | serverSocket.onAccept((ring, socket) -> {
25 | ring.queueAccept(serverSocket);
26 |
27 | socket.onRead(in -> {
28 | ring.queueWrite(socket, responseLine.slice());
29 | ring.queueWrite(socket, readmeBuffer.slice());
30 | });
31 | ring.queueRead(socket, ByteBuffer.allocateDirect(1024));
32 |
33 | socket.onWrite(out -> socket.close());
34 | socket.onException(ex -> {
35 | ex.printStackTrace();
36 | socket.close();
37 | });
38 | });
39 |
40 | ioUring.queueAccept(serverSocket);
41 | });
42 |
43 | ioUring
44 | .queueRead(readmeFile, ByteBuffer.allocateDirect(8192))
45 | .loop();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/examples/ParallelHttpEchoServer.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.examples;
2 |
3 | import sh.blake.niouring.IoUring;
4 | import sh.blake.niouring.IoUringServerSocket;
5 | import sh.blake.niouring.util.ByteBufferUtil;
6 |
7 | import java.nio.ByteBuffer;
8 | import java.util.concurrent.ExecutorService;
9 | import java.util.concurrent.Executors;
10 |
11 | public class ParallelHttpEchoServer {
12 | private static final ByteBuffer RESPONSE_LINE_BUFFER = ByteBufferUtil.wrapDirect("HTTP/1.1 200 OK\r\n\r\n");
13 |
14 | public static void main(String[] args) {
15 | IoUringServerSocket serverSocket = new IoUringServerSocket(8080);
16 | serverSocket.onAccept((ring, socket) -> {
17 | ring.queueAccept(serverSocket);
18 |
19 | socket.onRead(in -> {
20 | ring.queueWrite(socket, RESPONSE_LINE_BUFFER.slice());
21 | ring.queueWrite(socket, in);
22 | });
23 | ring.queueRead(socket, ByteBuffer.allocateDirect(1024));
24 |
25 | socket.onWrite(out -> socket.close());
26 | socket.onException(ex -> socket.close());
27 | });
28 |
29 | int rings = Integer.parseInt(args[0]);
30 | int ringSize = Integer.parseInt(args[1]);
31 | ExecutorService threadPool = Executors.newFixedThreadPool(rings);
32 | for (int i = 0; i < rings; i++) {
33 | IoUring ring = new IoUring(ringSize)
34 | .onException(Exception::printStackTrace)
35 | .queueAccept(serverSocket);
36 |
37 | threadPool.execute(ring::loop);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/TcpReverseProxyExample.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.examples;
2 |
3 | import sh.blake.niouring.IoUring;
4 | import sh.blake.niouring.IoUringServerSocket;
5 | import sh.blake.niouring.IoUringSocket;
6 |
7 | import java.nio.ByteBuffer;
8 |
9 | public class TcpReverseProxyExample {
10 | public static void main(String[] args) {
11 | String listenAddress = "127.0.0.1"; int fromPort = 8080;
12 | String forwardAddress = "127.0.0.1"; int toPort = 8000;
13 |
14 | IoUringServerSocket serverSocket = new IoUringServerSocket(listenAddress, fromPort);
15 | serverSocket.onAccept((ring, listenSocket) -> {
16 | ring.queueAccept(serverSocket);
17 |
18 | IoUringSocket forwardSocket = new IoUringSocket(forwardAddress, toPort);
19 | forwardSocket.onConnect(r -> {
20 | ByteBuffer listenBuffer = ByteBuffer.allocateDirect(1024 * 100);
21 | ring.queueRead(listenSocket, listenBuffer);
22 | listenSocket.onRead(in -> ring.queueWrite(forwardSocket, (ByteBuffer) in.flip()));
23 | listenSocket.onWrite(out -> ring.queueRead(forwardSocket, (ByteBuffer) out.clear()));
24 | listenSocket.onException(Exception::printStackTrace);
25 | listenSocket.onClose(forwardSocket::close);
26 |
27 | ByteBuffer forwardBuffer = ByteBuffer.allocateDirect(1024 * 100);
28 | ring.queueRead(forwardSocket, forwardBuffer);
29 | forwardSocket.onRead(in -> ring.queueWrite(listenSocket, (ByteBuffer) in.flip()));
30 | forwardSocket.onWrite(out -> ring.queueRead(listenSocket, (ByteBuffer) out.clear()));
31 | forwardSocket.onException(Exception::printStackTrace);
32 | forwardSocket.onClose(listenSocket::close);
33 | });
34 |
35 | ring.queueConnect(forwardSocket);
36 | });
37 |
38 | new IoUring()
39 | .onException(Exception::printStackTrace)
40 | .queueAccept(serverSocket)
41 | .loop();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bbeaupain/nio_uring/d2dd6b8a7fd2d6227683b617a47a235e8975f9b7/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'nio_uring'
2 |
--------------------------------------------------------------------------------
/src/main/c/liburing_file_provider.c:
--------------------------------------------------------------------------------
1 | #include "liburing_file_provider.h"
2 | #include "liburing_provider.h"
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | JNIEXPORT jint JNICALL
12 | Java_sh_blake_niouring_IoUringFile_open(JNIEnv *env, jclass cls, jstring path) {
13 | char *file_path = (*env)->GetStringUTFChars(env, path, NULL);
14 | int32_t ret = open(file_path, O_RDWR);
15 | if (ret < 0) {
16 | ret = open(file_path, O_CREAT, 0666);
17 | if (ret < 0) {
18 | return throw_exception(env, "open", ret);
19 | }
20 | }
21 | (*env)->ReleaseStringUTFChars(env, path, file_path);
22 | return (int32_t) ret;
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/c/liburing_file_provider.h:
--------------------------------------------------------------------------------
1 | #ifndef _LIBURING_FILE_PROVIDER_DEFINED
2 | #define _LIBURING_FILE_PROVIDER_DEFINED
3 |
4 | #include
5 |
6 | JNIEXPORT jint JNICALL
7 | Java_sh_blake_niouring_IoUringFile_open(JNIEnv *env, jclass cls, jstring path);
8 |
9 | #endif
10 |
--------------------------------------------------------------------------------
/src/main/c/liburing_provider.c:
--------------------------------------------------------------------------------
1 | #include "liburing_provider.h"
2 | #include "liburing_socket_provider.h"
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | #define EVENT_TYPE_ACCEPT 0
16 | #define EVENT_TYPE_READ 1
17 | #define EVENT_TYPE_WRITE 2
18 | #define EVENT_TYPE_CONNECT 3
19 | #define EVENT_TYPE_CLOSE 4
20 |
21 | struct request {
22 | int32_t fd;
23 | int8_t event_type;
24 | int64_t buffer_addr;
25 | };
26 |
27 | struct accept_request {
28 | int32_t fd;
29 | int8_t event_type;
30 | struct sockaddr_in client_addr;
31 | socklen_t client_addr_len;
32 | };
33 |
34 | JNIEXPORT jlong JNICALL
35 | Java_sh_blake_niouring_IoUring_createCqes(JNIEnv *env, jclass cls, jint count) {
36 | struct io_uring_cqe **cqes = malloc(sizeof(struct io_uring_cqe *) * count);
37 | if (!cqes) {
38 | throw_out_of_memory_error(env);
39 | return -1;
40 | }
41 | return (jlong) cqes;
42 | }
43 |
44 | JNIEXPORT jlong JNICALL
45 | Java_sh_blake_niouring_IoUring_create(JNIEnv *env, jclass cls, jint maxEvents) {
46 | struct io_uring *ring = malloc(sizeof(struct io_uring));
47 | if (!ring) {
48 | throw_out_of_memory_error(env);
49 | return -1;
50 | }
51 |
52 | int32_t ret = io_uring_queue_init(maxEvents, ring, 0);
53 | if (ret < 0) {
54 | throw_exception(env, "io_uring_queue_init", ret);
55 | return -1;
56 | }
57 |
58 | return (uint64_t) ring;
59 | }
60 |
61 | JNIEXPORT void JNICALL
62 | Java_sh_blake_niouring_IoUring_close(JNIEnv *env, jclass cls, jlong ring_address) {
63 | struct io_uring *ring = (struct io_uring *) ring_address;
64 | io_uring_queue_exit(ring);
65 | }
66 |
67 | JNIEXPORT jint JNICALL
68 | Java_sh_blake_niouring_IoUring_submitAndGetCqes(JNIEnv *env, jclass cls, jlong ring_address, jobject byte_buffer, jlong cqes_address, jint cqes_size, jboolean should_wait) {
69 | struct io_uring *ring = (struct io_uring *) ring_address;
70 |
71 | int32_t ret = io_uring_submit(ring);
72 | if (ret < 0) {
73 | if (ret != -EBUSY) { // if busy, continue handling completions
74 | throw_exception(env, "io_uring_submit", ret);
75 | return -1;
76 | }
77 | }
78 |
79 | struct io_uring_cqe **cqes = (struct io_uring_cqe **) cqes_address;
80 | ret = io_uring_peek_batch_cqe(ring, cqes, cqes_size);
81 | if (ret == 0 && should_wait) {
82 | ret = io_uring_wait_cqe(ring, cqes);
83 | if (ret < 0) {
84 | throw_exception(env, "io_uring_wait_cqe", ret);
85 | return -1;
86 | }
87 | ret = 1;
88 | }
89 |
90 | char *buffer = (*env)->GetDirectBufferAddress(env, byte_buffer);
91 | if (buffer == NULL) {
92 | throw_exception(env, "invalid byte buffer (read)", -EINVAL);
93 | return -1;
94 | }
95 |
96 | long buf_capacity = (*env)->GetDirectBufferCapacity(env, byte_buffer);
97 |
98 | int32_t cqe_index = 0;
99 | int32_t buf_index = 0;
100 | while (cqe_index < ret) {
101 | struct io_uring_cqe *cqe = cqes[cqe_index];
102 | struct request *req = (struct request *) cqe->user_data;
103 |
104 | if (buf_index + 9 >= buf_capacity) {
105 | throw_buffer_overflow_exception(env);
106 | return -1;
107 | }
108 |
109 | buffer[buf_index++] = cqe->res >> 24;
110 | buffer[buf_index++] = cqe->res >> 16;
111 | buffer[buf_index++] = cqe->res >> 8;
112 | buffer[buf_index++] = cqe->res;
113 |
114 | buffer[buf_index++] = req->fd >> 24;
115 | buffer[buf_index++] = req->fd >> 16;
116 | buffer[buf_index++] = req->fd >> 8;
117 | buffer[buf_index++] = req->fd;
118 |
119 | buffer[buf_index++] = req->event_type;
120 |
121 | if (req->event_type == EVENT_TYPE_READ || req->event_type == EVENT_TYPE_WRITE) {
122 | if (buf_index + 8 >= buf_capacity) {
123 | throw_buffer_overflow_exception(env);
124 | return -1;
125 | }
126 |
127 | buffer[buf_index++] = req->buffer_addr >> 56;
128 | buffer[buf_index++] = req->buffer_addr >> 48;
129 | buffer[buf_index++] = req->buffer_addr >> 40;
130 | buffer[buf_index++] = req->buffer_addr >> 32;
131 | buffer[buf_index++] = req->buffer_addr >> 24;
132 | buffer[buf_index++] = req->buffer_addr >> 16;
133 | buffer[buf_index++] = req->buffer_addr >> 8;
134 | buffer[buf_index++] = req->buffer_addr;
135 | }
136 |
137 | cqe_index++;
138 | }
139 |
140 | return (int32_t) ret;
141 | }
142 |
143 | JNIEXPORT void JNICALL
144 | Java_sh_blake_niouring_IoUring_freeCqes(JNIEnv *env, jclass cls, jlong cqes_address) {
145 | struct io_uring_cqe **cqes = (struct io_uring_cqe **) cqes_address;
146 | free(cqes);
147 | }
148 |
149 | JNIEXPORT jstring JNICALL
150 | Java_sh_blake_niouring_IoUring_getCqeIpAddress(JNIEnv *env, jclass cls, jlong cqes_address, jint cqe_index) {
151 | struct io_uring_cqe **cqes = (struct io_uring_cqe **) cqes_address;
152 | struct io_uring_cqe *cqe = cqes[cqe_index];
153 | struct accept_request *req = (struct accept_request *) cqe->user_data;
154 | char *ip_address = inet_ntoa(req->client_addr.sin_addr);
155 | return (*env)->NewStringUTF(env, ip_address);
156 | }
157 |
158 | JNIEXPORT void JNICALL
159 | Java_sh_blake_niouring_IoUring_markCqeSeen(JNIEnv *env, jclass cls, jlong ring_address, jlong cqes_address, jint cqe_index) {
160 | struct io_uring *ring = (struct io_uring *) ring_address;
161 | struct io_uring_cqe **cqes = (struct io_uring_cqe **) cqes_address;
162 | struct io_uring_cqe *cqe = cqes[cqe_index];
163 | struct request *req = (struct request *) cqe->user_data;
164 |
165 | free(req);
166 | io_uring_cqe_seen(ring, cqe);
167 | }
168 |
169 | JNIEXPORT void JNICALL
170 | Java_sh_blake_niouring_IoUring_queueAccept(JNIEnv *env, jclass cls, jlong ring_address, jint server_socket_fd) {
171 | struct io_uring *ring = (struct io_uring *) ring_address;
172 |
173 | struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
174 | if (sqe == NULL) {
175 | throw_exception(env, "io_uring_get_sqe", -EBUSY);
176 | return;
177 | }
178 | sqe->cancel_flags = IORING_ASYNC_CANCEL_FD;
179 |
180 | struct accept_request *req = malloc(sizeof(*req));
181 | if (!req) {
182 | throw_out_of_memory_error(env);
183 | return;
184 | }
185 | memset(&req->client_addr, 0, sizeof(req->client_addr));
186 | req->client_addr_len = sizeof(req->client_addr);
187 | req->event_type = EVENT_TYPE_ACCEPT;
188 | req->fd = server_socket_fd;
189 |
190 | io_uring_prep_accept(sqe, server_socket_fd, (struct sockaddr *) &req->client_addr, &req->client_addr_len, 0);
191 | io_uring_sqe_set_data(sqe, req);
192 | }
193 |
194 | JNIEXPORT void JNICALL
195 | Java_sh_blake_niouring_IoUring_queueConnect(JNIEnv *env, jclass cls, jlong ring_address, jint socket_fd, jstring ip_address, jint port) {
196 | struct io_uring *ring = (struct io_uring *) ring_address;
197 |
198 | struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
199 | if (sqe == NULL) {
200 | throw_exception(env, "io_uring_get_sqe", -EBUSY);
201 | return;
202 | }
203 |
204 | struct accept_request *req = malloc(sizeof(*req));
205 | if (!req) {
206 | throw_out_of_memory_error(env);
207 | return;
208 | }
209 | const char *ip = (*env)->GetStringUTFChars(env, ip_address, NULL);
210 | memset(&req->client_addr, 0, sizeof(req->client_addr));
211 | req->client_addr.sin_addr.s_addr = inet_addr(ip);
212 | req->client_addr.sin_port = htons(port);
213 | req->client_addr.sin_family = AF_INET;
214 | req->client_addr_len = sizeof(req->client_addr);
215 | req->event_type = EVENT_TYPE_CONNECT;
216 | req->fd = socket_fd;
217 | (*env)->ReleaseStringUTFChars(env, ip_address, ip);
218 |
219 | io_uring_prep_connect(sqe, socket_fd, (struct sockaddr *) &req->client_addr, req->client_addr_len);
220 | io_uring_sqe_set_data(sqe, req);
221 | }
222 |
223 | JNIEXPORT jlong JNICALL
224 | Java_sh_blake_niouring_IoUring_queueRead(JNIEnv *env, jclass cls, jlong ring_address, jint fd, jobject byte_buffer, jint buffer_pos, jint buffer_len, jlong io_offset) {
225 | void *buffer = (*env)->GetDirectBufferAddress(env, byte_buffer);
226 | if (buffer == NULL) {
227 | throw_exception(env, "invalid byte buffer (read)", -EINVAL);
228 | return -1;
229 | }
230 |
231 | struct io_uring *ring = (struct io_uring *) ring_address;
232 | if (io_uring_sq_space_left(ring) <= 1) {
233 | throw_exception(env, "io_uring_sq_space_left", -EBUSY);
234 | return -1;
235 | }
236 |
237 | struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
238 | if (sqe == NULL) {
239 | throw_exception(env, "io_uring_get_sqe", -EBUSY);
240 | return -1;
241 | }
242 |
243 | sqe->cancel_flags = IORING_ASYNC_CANCEL_FD;
244 |
245 | struct request *req = malloc(sizeof(*req));
246 | if (!req) {
247 | throw_out_of_memory_error(env);
248 | return -1;
249 | }
250 | req->event_type = EVENT_TYPE_READ;
251 | req->buffer_addr = (int64_t) buffer;
252 | req->fd = fd;
253 |
254 | io_uring_prep_read(sqe, fd, buffer + buffer_pos, buffer_len, (uint64_t) io_offset);
255 | io_uring_sqe_set_data(sqe, req);
256 |
257 | return (uint64_t) buffer;
258 | }
259 |
260 | JNIEXPORT jlong JNICALL
261 | Java_sh_blake_niouring_IoUring_queueWrite(JNIEnv *env, jclass cls, jlong ring_address, jint fd, jobject byte_buffer, jint buffer_pos, jint buffer_len, jlong io_offset) {
262 | void *buffer = (*env)->GetDirectBufferAddress(env, byte_buffer);
263 | if (buffer == NULL) {
264 | throw_exception(env, "invalid byte buffer (write)", -EINVAL);
265 | return -1;
266 | }
267 |
268 | struct io_uring *ring = (struct io_uring *) ring_address;
269 | if (io_uring_sq_space_left(ring) <= 1) {
270 | throw_exception(env, "io_uring_sq_space_left", -EBUSY);
271 | return -1;
272 | }
273 |
274 | struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
275 | if (sqe == NULL) {
276 | throw_exception(env, "io_uring_get_sqe", -EBUSY);
277 | return -1;
278 | }
279 | sqe->cancel_flags = IORING_ASYNC_CANCEL_FD;
280 |
281 | struct request *req = malloc(sizeof(*req));
282 | if (!req) {
283 | throw_out_of_memory_error(env);
284 | return -1;
285 | }
286 | req->event_type = EVENT_TYPE_WRITE;
287 | req->buffer_addr = (int64_t) buffer;
288 | req->fd = fd;
289 |
290 | io_uring_prep_write(sqe, fd, buffer + buffer_pos, buffer_len, (uint64_t) io_offset);
291 | io_uring_sqe_set_data(sqe, req);
292 |
293 | return (uint64_t) buffer;
294 | }
295 |
296 | JNIEXPORT void JNICALL
297 | Java_sh_blake_niouring_IoUring_queueClose(JNIEnv *env, jclass cls, jlong ring_address, jint fd) {
298 | struct io_uring *ring = (struct io_uring *) ring_address;
299 | if (io_uring_sq_space_left(ring) <= 1) {
300 | throw_exception(env, "io_uring_sq_space_left", -EBUSY);
301 | return;
302 | }
303 |
304 | struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
305 | if (sqe == NULL) {
306 | throw_exception(env, "io_uring_get_sqe", -EBUSY);
307 | return;
308 | }
309 | sqe->cancel_flags = IORING_ASYNC_CANCEL_FD;
310 |
311 | struct request *req = malloc(sizeof(*req));
312 | if (!req) {
313 | throw_out_of_memory_error(env);
314 | return;
315 | }
316 | req->event_type = EVENT_TYPE_CLOSE;
317 | req->fd = fd;
318 |
319 | io_uring_prep_close(sqe, fd);
320 | io_uring_sqe_set_data(sqe, req);
321 | }
322 |
323 | JNIEXPORT void JNICALL
324 | Java_sh_blake_niouring_AbstractIoUringChannel_close(JNIEnv *env, jclass cls, jint fd) {
325 | shutdown(fd, SHUT_WR);
326 | close(fd);
327 | }
328 |
329 | int32_t throw_exception(JNIEnv *env, char *cause, int32_t ret) {
330 | char error_msg[1024];
331 | snprintf(error_msg, sizeof(error_msg), "%s - %s", cause, strerror(-ret));
332 | return (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/RuntimeException"), (const char *) &error_msg);
333 | }
334 |
335 | int32_t throw_out_of_memory_error(JNIEnv *env) {
336 | return (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/OutOfMemoryError"), "Out of heap space");
337 | }
338 |
339 | int32_t throw_buffer_overflow_exception(JNIEnv *env) {
340 | return (*env)->ThrowNew(env, (*env)->FindClass(env, "java/nio/BufferOverflowException"), "Buffer overflow");
341 | }
342 |
--------------------------------------------------------------------------------
/src/main/c/liburing_provider.h:
--------------------------------------------------------------------------------
1 | #ifndef _LIBURING_PROVIDER_DEFINED
2 | #define _LIBURING_PROVIDER_DEFINED
3 |
4 | #include
5 | #include
6 |
7 | JNIEXPORT jlong JNICALL
8 | Java_sh_blake_niouring_IoUring_createCqes(JNIEnv *env, jclass cls, jint count);
9 |
10 | JNIEXPORT void JNICALL
11 | Java_sh_blake_niouring_IoUring_freeCqes(JNIEnv *env, jclass cls, jlong cqes_address);
12 |
13 | JNIEXPORT jlong JNICALL
14 | Java_sh_blake_niouring_IoUring_create(JNIEnv *env, jclass cls, jint maxEvents);
15 |
16 | JNIEXPORT void JNICALL
17 | Java_sh_blake_niouring_IoUring_close(JNIEnv *env, jclass cls, jlong ring_address);
18 |
19 | JNIEXPORT jint JNICALL
20 | Java_sh_blake_niouring_IoUring_submitAndGetCqes(JNIEnv *env, jclass cls, jlong ring_address, jobject byte_buffer, jlong cqes_address, jint cqes_size, jboolean should_wait);
21 |
22 | JNIEXPORT jstring JNICALL
23 | Java_sh_blake_niouring_IoUring_getCqeIpAddress(JNIEnv *env, jclass cls, jlong cqes_address, jint cqe_index);
24 |
25 | JNIEXPORT void JNICALL
26 | Java_sh_blake_niouring_IoUring_markCqeSeen(JNIEnv *env, jclass cls, jlong ring_address, jlong cqes_address, jint cqe_index);
27 |
28 | JNIEXPORT void JNICALL
29 | Java_sh_blake_niouring_IoUring_queueAccept(JNIEnv *env, jclass cls, jlong ring_address, jint server_socket_fd);
30 |
31 | JNIEXPORT void JNICALL
32 | Java_sh_blake_niouring_IoUring_queueConnect(JNIEnv *env, jclass cls, jlong ring_address, jint socket_fd, jstring ip_address, jint port);
33 |
34 | JNIEXPORT jlong JNICALL
35 | Java_sh_blake_niouring_IoUring_queueRead(JNIEnv *env, jclass cls, jlong ring_address, jint fd, jobject byte_buffer, jint buffer_pos, jint buffer_len, jlong io_offset);
36 |
37 | JNIEXPORT jlong JNICALL
38 | Java_sh_blake_niouring_IoUring_queueWrite(JNIEnv *env, jclass cls, jlong ring_address, jint fd, jobject byte_buffer, jint buffer_pos, jint buffer_len, jlong io_offset);
39 |
40 | JNIEXPORT void JNICALL
41 | Java_sh_blake_niouring_IoUring_queueClose(JNIEnv *env, jclass cls, jlong ring_address, jint fd);
42 |
43 | JNIEXPORT void JNICALL
44 | Java_sh_blake_niouring_AbstractIoUringChannel_close(JNIEnv *env, jclass cls, jint fd);
45 |
46 | int32_t throw_exception(JNIEnv *env, char *cause, int32_t ret);
47 |
48 | int32_t throw_out_of_memory_error(JNIEnv *env);
49 |
50 | #endif
51 |
--------------------------------------------------------------------------------
/src/main/c/liburing_socket_provider.c:
--------------------------------------------------------------------------------
1 | #include "liburing_socket_provider.h"
2 | #include "liburing_provider.h"
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | JNIEXPORT jint JNICALL
15 | Java_sh_blake_niouring_AbstractIoUringSocket_create(JNIEnv *env, jclass cls) {
16 | int32_t val = 1;
17 |
18 | int32_t fd = socket(PF_INET, SOCK_STREAM, 0);
19 | if (fd == -1) {
20 | throw_exception(env, "socket", fd);
21 | return -1;
22 | }
23 |
24 | int32_t ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int));
25 | if (ret < 0) {
26 | throw_exception(env, "setsockopt", ret);
27 | return -1;
28 | }
29 |
30 | ret = setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(val));
31 | if (ret == -1) {
32 | throw_exception(env, "setsockopt", ret);
33 | return -1;
34 | }
35 |
36 | return (uint32_t) fd;
37 | }
38 |
39 | JNIEXPORT void JNICALL
40 | Java_sh_blake_niouring_IoUringServerSocket_bind(JNIEnv *env, jclass cls, jlong server_socket_fd, jstring ip_address, jint port, jint backlog) {
41 | char *ip = (*env)->GetStringUTFChars(env, ip_address, NULL);
42 |
43 | struct sockaddr_in srv_addr;
44 | memset(&srv_addr, 0, sizeof(srv_addr));
45 | srv_addr.sin_family = AF_INET;
46 | srv_addr.sin_port = htons(port);
47 | srv_addr.sin_addr.s_addr = inet_addr(ip);
48 |
49 | (*env)->ReleaseStringUTFChars(env, ip_address, ip);
50 |
51 | int32_t ret = bind(server_socket_fd, (const struct sockaddr *) &srv_addr, sizeof(srv_addr));
52 | if (ret < 0) {
53 | throw_exception(env, "bind", ret);
54 | return;
55 | }
56 |
57 | ret = listen(server_socket_fd, backlog);
58 | if (ret < 0) {
59 | throw_exception(env, "io_uring_get_sqe", -16);
60 | return;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/c/liburing_socket_provider.h:
--------------------------------------------------------------------------------
1 | #ifndef _LIBURING_SOCKET_PROVIDER_DEFINED
2 | #define _LIBURING_SOCKET_PROVIDER_DEFINED
3 |
4 | #include
5 | #include
6 |
7 | JNIEXPORT jint JNICALL
8 | Java_sh_blake_niouring_AbstractIoUringSocket_create(JNIEnv *env, jclass cls);
9 |
10 | JNIEXPORT void JNICALL
11 | Java_sh_blake_niouring_IoUringServerSocket_bind(JNIEnv *env, jclass cls, jlong server_socket_fd, jstring host, jint port, jint backlog);
12 |
13 | int32_t throw_buffer_overflow_exception(JNIEnv *env);
14 |
15 | #endif
16 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/AbstractIoUringChannel.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import org.eclipse.collections.impl.map.mutable.primitive.LongObjectHashMap;
4 | import sh.blake.niouring.util.NativeLibraryLoader;
5 | import sh.blake.niouring.util.ReferenceCounter;
6 |
7 | import java.nio.ByteBuffer;
8 | import java.util.function.Consumer;
9 |
10 | /**
11 | * The type {@code AbstractIoUringSocket}.
12 | */
13 | public abstract class AbstractIoUringChannel {
14 | private final int fd;
15 | private final LongObjectHashMap> readBufferMap = new LongObjectHashMap<>();
16 | private final LongObjectHashMap> writeBufferMap = new LongObjectHashMap<>();
17 | private boolean closed = false;
18 | private Consumer readHandler;
19 | private Consumer writeHandler;
20 | private Consumer exceptionHandler;
21 | private Runnable closeHandler;
22 |
23 | /**
24 | * Instantiates a new {@code AbstractIoUringSocket}.
25 | *
26 | * @param fd the fd
27 | */
28 | AbstractIoUringChannel(int fd) {
29 | this.fd = fd;
30 | }
31 |
32 | protected void handleReadCompletion(ByteBuffer buffer, int bytesRead) {
33 | if (bytesRead < 0) {
34 | close();
35 | return;
36 | }
37 | buffer.position(buffer.position() + bytesRead);
38 | if (readHandler() != null) {
39 | readHandler().accept(buffer);
40 | }
41 | }
42 |
43 | protected void handleWriteCompletion(ByteBuffer buffer, int bytesWritten) {
44 | if (bytesWritten < 0) {
45 | close();
46 | return;
47 | }
48 | buffer.position(buffer.position() + bytesWritten);
49 | if (writeHandler() != null) {
50 | writeHandler().accept(buffer);
51 | }
52 | }
53 |
54 | /**
55 | * Closes the socket.
56 | */
57 | public void close() {
58 | if (closed) {
59 | return;
60 | }
61 | AbstractIoUringChannel.close(fd);
62 | closed = true;
63 | if (closeHandler != null) {
64 | closeHandler.run();
65 | }
66 | }
67 |
68 | /**
69 | * Gets the file descriptor.
70 | *
71 | * @return the long
72 | */
73 | int fd() {
74 | return fd;
75 | }
76 |
77 | /**
78 | * Checks if a write operation is currently pending.
79 | *
80 | * @return whether write is pending
81 | */
82 | public boolean isWritePending() {
83 | return !writeBufferMap.isEmpty();
84 | }
85 |
86 | /**
87 | * Checks if a read operation is currently pending.
88 | *
89 | * @return whether read is pending
90 | */
91 | public boolean isReadPending() {
92 | return !readBufferMap.isEmpty();
93 | }
94 |
95 | /**
96 | * Gets the read handler.
97 | *
98 | * @return the read handler
99 | */
100 | Consumer readHandler() {
101 | return readHandler;
102 | }
103 |
104 | /**
105 | * Sets the handler to be called when a read operation completes.
106 | *
107 | * @param readHandler the read handler
108 | * @return this instance
109 | */
110 | public AbstractIoUringChannel onRead(Consumer readHandler) {
111 | this.readHandler = readHandler;
112 | return this;
113 | }
114 |
115 | /**
116 | * Gets the write handler.
117 | *
118 | * @return the write handler
119 | */
120 | Consumer writeHandler() {
121 | return writeHandler;
122 | }
123 |
124 | /**
125 | * Sets the handler to be called when a write operation completes.
126 | *
127 | * @param writeHandler the write handler
128 | * @return this instance
129 | */
130 | public AbstractIoUringChannel onWrite(Consumer writeHandler) {
131 | this.writeHandler = writeHandler;
132 | return this;
133 | }
134 |
135 | /**
136 | * Gets the exception handler.
137 | *
138 | * @return the exception handler
139 | */
140 | Consumer exceptionHandler() {
141 | return exceptionHandler;
142 | }
143 |
144 | /**
145 | * Gets read buffer map.
146 | *
147 | * @return the read buffer map
148 | */
149 | LongObjectHashMap> readBufferMap() {
150 | return readBufferMap;
151 | }
152 |
153 | /**
154 | * Gets write buffer map.
155 | *
156 | * @return the write buffer map
157 | */
158 | LongObjectHashMap> writeBufferMap() {
159 | return writeBufferMap;
160 | }
161 |
162 | /**
163 | * Sets the handler to be called when an exception is caught while handling I/O for the socket.
164 | *
165 | * @param exceptionHandler the exception handler
166 | * @return this instance
167 | */
168 | public AbstractIoUringChannel onException(Consumer exceptionHandler) {
169 | this.exceptionHandler = exceptionHandler;
170 | return this;
171 | }
172 |
173 | Runnable closeHandler() {
174 | return closeHandler;
175 | }
176 |
177 | /**
178 | * Sets the handler to be called when the channel is closed.
179 | * @param closeHandler The close handler
180 | * @return this instance
181 | */
182 | public AbstractIoUringChannel onClose(Runnable closeHandler) {
183 | this.closeHandler = closeHandler;
184 | return this;
185 | }
186 |
187 | /**
188 | * Check if the channel is closed.
189 | *
190 | * @return true if the channel has been closed
191 | */
192 | public boolean isClosed() {
193 | return closed;
194 | }
195 |
196 | void setClosed(boolean closed) {
197 | this.closed = closed;
198 | }
199 |
200 | /**
201 | * Check if the channel is open.
202 | * @return true if the channel has not been closed
203 | */
204 | public boolean isOpen() {
205 | return !closed;
206 | }
207 |
208 | private static native void close(int fd);
209 |
210 | static {
211 | NativeLibraryLoader.load();
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/AbstractIoUringSocket.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import sh.blake.niouring.util.NativeLibraryLoader;
4 |
5 | /**
6 | * An {@link AbstractIoUringChannel} representing a network socket.
7 | */
8 | public class AbstractIoUringSocket extends AbstractIoUringChannel {
9 | private final String ipAddress;
10 | private final int port;
11 |
12 | /**
13 | * Creates a new {@code AbstractIoUringSocket} instance.
14 | * @param fd The file descriptor
15 | * @param ipAddress The IP address
16 | * @param port The port
17 | */
18 | AbstractIoUringSocket(int fd, String ipAddress, int port) {
19 | super(fd);
20 | this.ipAddress = ipAddress;
21 | this.port = port;
22 | }
23 |
24 | public String ipAddress() {
25 | return ipAddress;
26 | }
27 |
28 | public int port() {
29 | return port;
30 | }
31 |
32 | static native int create();
33 |
34 | static {
35 | NativeLibraryLoader.load();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/IoUring.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import org.eclipse.collections.impl.map.mutable.primitive.IntObjectHashMap;
4 | import sh.blake.niouring.util.ReferenceCounter;
5 | import sh.blake.niouring.util.NativeLibraryLoader;
6 |
7 | import java.nio.ByteBuffer;
8 | import java.util.function.Consumer;
9 |
10 | /**
11 | * Primary interface for creating and working with an {@code io_uring}.
12 | */
13 | public class IoUring {
14 | private static final int DEFAULT_MAX_EVENTS = 1024;
15 | private static final int EVENT_TYPE_ACCEPT = 0;
16 | private static final int EVENT_TYPE_READ = 1;
17 | private static final int EVENT_TYPE_WRITE = 2;
18 | private static final int EVENT_TYPE_CONNECT = 3;
19 | private static final int EVENT_TYPE_CLOSE = 4;
20 |
21 | private final long ring;
22 | private final int ringSize;
23 | private final IntObjectHashMap fdToSocket = new IntObjectHashMap<>();
24 | private Consumer exceptionHandler;
25 | private boolean closed = false;
26 | private final long cqes;
27 | private final ByteBuffer resultBuffer;
28 |
29 | /**
30 | * Instantiates a new {@code IoUring} with {@code DEFAULT_MAX_EVENTS}.
31 | */
32 | public IoUring() {
33 | this(DEFAULT_MAX_EVENTS);
34 | }
35 |
36 | /**
37 | * Instantiates a new Io uring.
38 | *
39 | * @param ringSize the max events
40 | */
41 | public IoUring(int ringSize) {
42 | this.ringSize = ringSize;
43 | this.ring = IoUring.create(ringSize);
44 | this.cqes = IoUring.createCqes(ringSize);
45 | this.resultBuffer = ByteBuffer.allocateDirect(ringSize * 17);
46 | }
47 |
48 | /**
49 | * Closes the io_uring.
50 | */
51 | public void close() {
52 | if (closed) {
53 | throw new IllegalStateException("io_uring closed");
54 | }
55 | closed = true;
56 | IoUring.close(ring);
57 | IoUring.freeCqes(cqes);
58 | }
59 |
60 | /**
61 | * Takes over the current thread with a loop calling {@code execute()}, until closed.
62 | */
63 | public void loop() {
64 | while (!closed) {
65 | execute();
66 | }
67 | }
68 |
69 | /**
70 | * Submits all queued I/O operations to the kernel and waits an unlimited amount of time for any to complete.
71 | */
72 | public int execute() {
73 | return doExecute(true);
74 | }
75 |
76 | /**
77 | * Submits all queued I/O operations to the kernel and handles any pending completion events, returning immediately
78 | * if none are present.
79 | */
80 | public int executeNow() {
81 | return doExecute(false);
82 | }
83 |
84 | private int doExecute(boolean shouldWait) {
85 | if (closed) {
86 | throw new IllegalStateException("io_uring closed");
87 | }
88 | try {
89 | int count = IoUring.submitAndGetCqes(ring, resultBuffer, cqes, ringSize, shouldWait);
90 | for (int i = 0; i < count && i < ringSize; i++) {
91 | try {
92 | handleEventCompletion(cqes, resultBuffer, i);
93 | } finally {
94 | IoUring.markCqeSeen(ring, cqes, i);
95 | }
96 | }
97 | return count;
98 | } catch (Exception ex) {
99 | if (exceptionHandler != null) {
100 | exceptionHandler.accept(ex);
101 | }
102 | } finally {
103 | resultBuffer.clear();
104 | }
105 | return -1;
106 | }
107 |
108 | private void handleEventCompletion(long cqes, ByteBuffer results, int i) {
109 | int result = results.getInt();
110 | int fd = results.getInt();
111 | int eventType = results.get();
112 |
113 | if (eventType == EVENT_TYPE_ACCEPT) {
114 | IoUringServerSocket serverSocket = (IoUringServerSocket) fdToSocket.get(fd);
115 | String ipAddress = IoUring.getCqeIpAddress(cqes, i);
116 | IoUringSocket socket = serverSocket.handleAcceptCompletion(this, serverSocket, result, ipAddress);
117 | if (socket != null) {
118 | fdToSocket.put(socket.fd(), socket);
119 | }
120 | } else {
121 | AbstractIoUringChannel channel = fdToSocket.get(fd);
122 | if (channel == null || channel.isClosed()) {
123 | return;
124 | }
125 | try {
126 | if (eventType == EVENT_TYPE_CONNECT) {
127 | ((IoUringSocket) channel).handleConnectCompletion(this, result);
128 | } else if (eventType == EVENT_TYPE_READ) {
129 | long bufferAddress = results.getLong();
130 | ReferenceCounter refCounter = channel.readBufferMap().get(bufferAddress);
131 | ByteBuffer buffer = refCounter.ref();
132 | if (buffer == null) {
133 | throw new IllegalStateException("Buffer already removed");
134 | }
135 | if (refCounter.deincrementReferenceCount() == 0) {
136 | channel.readBufferMap().remove(bufferAddress);
137 | }
138 | channel.handleReadCompletion(buffer, result);
139 | } else if (eventType == EVENT_TYPE_WRITE) {
140 | long bufferAddress = results.getLong();
141 | ReferenceCounter refCounter = channel.writeBufferMap().get(bufferAddress);
142 | ByteBuffer buffer = refCounter.ref();
143 | if (buffer == null) {
144 | throw new IllegalStateException("Buffer already removed");
145 | }
146 | if (refCounter.deincrementReferenceCount() == 0) {
147 | channel.writeBufferMap().remove(bufferAddress);
148 | }
149 | channel.handleWriteCompletion(buffer, result);
150 | } else if (eventType == EVENT_TYPE_CLOSE) {
151 | channel.setClosed(true);
152 | channel.closeHandler().run();
153 | }
154 | } catch (Exception ex) {
155 | if (channel.exceptionHandler() != null) {
156 | channel.exceptionHandler().accept(ex);
157 | }
158 | } finally {
159 | if (channel.isClosed() && channel.equals(fdToSocket.get(fd))) {
160 | deregister(channel);
161 | }
162 | }
163 | }
164 | }
165 |
166 | /**
167 | * Queues a {@link IoUringServerSocket} for an accept operation on the next ring execution.
168 | *
169 | * @param serverSocket the server socket
170 | * @return this instance
171 | */
172 | public IoUring queueAccept(IoUringServerSocket serverSocket) {
173 | fdToSocket.put(serverSocket.fd(), serverSocket);
174 | IoUring.queueAccept(ring, serverSocket.fd());
175 | return this;
176 | }
177 |
178 | /**
179 | * Queues a {@link IoUringServerSocket} for a connect operation on the next ring execution.
180 | *
181 | * @param socket the socket channel
182 | * @return this instance
183 | */
184 | public IoUring queueConnect(IoUringSocket socket) {
185 | fdToSocket.put(socket.fd(), socket);
186 | IoUring.queueConnect(ring, socket.fd(), socket.ipAddress(), socket.port());
187 | return this;
188 | }
189 |
190 | /**
191 | * Queues {@link IoUringSocket} for a read operation on the next ring execution.
192 | *
193 | * @param channel the channel
194 | * @param buffer the buffer to read into
195 | * @return this instance
196 | */
197 | public IoUring queueRead(AbstractIoUringChannel channel, ByteBuffer buffer) {
198 | return queueRead(channel, buffer, 0L);
199 | }
200 |
201 | /**
202 | * Queues {@link IoUringSocket} for a read operation on the next ring execution.
203 | *
204 | * @param channel the channel
205 | * @param buffer the buffer to read into
206 | * @param offset the offset into the file/source of the read; Casted to u64
207 | * @return this instance
208 | */
209 | public IoUring queueRead(AbstractIoUringChannel channel, ByteBuffer buffer, long offset) {
210 | if (!buffer.isDirect()) {
211 | throw new IllegalArgumentException("Buffer must be direct");
212 | }
213 | fdToSocket.put(channel.fd(), channel);
214 | long bufferAddress = IoUring.queueRead(ring, channel.fd(), buffer, buffer.position(), buffer.limit() - buffer.position(), offset);
215 | ReferenceCounter refCounter = channel.readBufferMap().get(bufferAddress);
216 | if (refCounter == null) {
217 | refCounter = new ReferenceCounter<>(buffer);
218 | channel.readBufferMap().put(bufferAddress, refCounter);
219 | }
220 | refCounter.incrementReferenceCount();
221 | return this;
222 | }
223 |
224 | /**
225 | * Queues {@link IoUringSocket} for a write operation on the next ring execution.
226 | *
227 | * @param channel the channel
228 | * @return this instance
229 | */
230 | public IoUring queueWrite(AbstractIoUringChannel channel, ByteBuffer buffer) {
231 | return queueWrite(channel, buffer, 0L);
232 | }
233 |
234 | /**
235 | * Queues {@link IoUringSocket} for a write operation on the next ring execution.
236 | *
237 | * @param channel the channel
238 | * @param offset the offset into the file/source of the write; Casted to u64
239 | * @return this instance
240 | */
241 | public IoUring queueWrite(AbstractIoUringChannel channel, ByteBuffer buffer, long offset) {
242 | if (!buffer.isDirect()) {
243 | throw new IllegalArgumentException("Buffer must be direct");
244 | }
245 | fdToSocket.put(channel.fd(), channel);
246 | long bufferAddress = IoUring.queueWrite(ring, channel.fd(), buffer, buffer.position(), buffer.limit() - buffer.position(), offset);
247 | ReferenceCounter refCounter = channel.writeBufferMap().get(bufferAddress);
248 | if (refCounter == null) {
249 | refCounter = new ReferenceCounter<>(buffer);
250 | channel.writeBufferMap().put(bufferAddress, refCounter);
251 | }
252 | refCounter.incrementReferenceCount();
253 | return this;
254 | }
255 |
256 | public IoUring queueClose(AbstractIoUringChannel channel) {
257 | IoUring.queueClose(ring, channel.fd());
258 | return this;
259 | }
260 |
261 | /**
262 | * Gets the exception handler.
263 | *
264 | * @return the exception handler
265 | */
266 | Consumer exceptionHandler() {
267 | return exceptionHandler;
268 | }
269 |
270 | /**
271 | * Sets the handler that is called when an {@code Exception} is caught during execution.
272 | *
273 | * @param exceptionHandler the exception handler
274 | */
275 | public IoUring onException(Consumer exceptionHandler) {
276 | this.exceptionHandler = exceptionHandler;
277 | return this;
278 | }
279 |
280 | /**
281 | * Deregister this channel from the ring.
282 | *
283 | * @param channel the channel
284 | */
285 | void deregister(AbstractIoUringChannel channel) {
286 | fdToSocket.remove(channel.fd());
287 | }
288 |
289 | private static native long create(int maxEvents);
290 | private static native void close(long ring);
291 | private static native long createCqes(int count);
292 | private static native void freeCqes(long cqes);
293 | private static native int submitAndGetCqes(long ring, ByteBuffer buffer, long cqes, int cqesSize, boolean shouldWait);
294 | private static native String getCqeIpAddress(long cqes, int cqeIndex);
295 | private static native void markCqeSeen(long ring, long cqes, int cqeIndex);
296 | private static native void queueAccept(long ring, int serverSocketFd);
297 | private static native void queueConnect(long ring, int socketFd, String ipAddress, int port);
298 | private static native long queueRead(long ring, int channelFd, ByteBuffer buffer, int bufferPos, int bufferLen, long offset);
299 | private static native long queueWrite(long ring, int channelFd, ByteBuffer buffer, int bufferPos, int bufferLen, long offset);
300 | private static native void queueClose(long ring, int channelFd);
301 |
302 | static {
303 | NativeLibraryLoader.load();
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/IoUringFile.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import sh.blake.niouring.util.NativeLibraryLoader;
4 |
5 | /**
6 | * An {@link AbstractIoUringChannel} implementation for file operations.
7 | */
8 | public class IoUringFile extends AbstractIoUringChannel {
9 |
10 | /**
11 | * Instantiates a new {@code IoUringFile}.
12 | *
13 | * @param path The path to the file
14 | */
15 | public IoUringFile(String path) {
16 | super(IoUringFile.open(path));
17 | }
18 |
19 | private static native int open(String path);
20 |
21 | static {
22 | NativeLibraryLoader.load();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/IoUringServerSocket.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import sh.blake.niouring.util.NativeLibraryLoader;
4 |
5 | import java.nio.ByteBuffer;
6 | import java.util.function.BiConsumer;
7 | import java.util.function.Consumer;
8 |
9 | /**
10 | * A {@code ServerSocket} analog for working with an {@code io_uring}.
11 | */
12 | public class IoUringServerSocket extends AbstractIoUringSocket {
13 | private static final int DEFAULT_BACKLOG = 65535;
14 |
15 | private BiConsumer acceptHandler;
16 |
17 | /**
18 | * Instantiates a new {@code IoUringServerSocket}.
19 | *
20 | * @param address The address to bind to
21 | * @param port The port to bind to
22 | * @param backlog The backlog size
23 | */
24 | public IoUringServerSocket(String address, int port, int backlog) {
25 | super(AbstractIoUringSocket.create(), address, port);
26 | IoUringServerSocket.bind(fd(), address, port, backlog);
27 | }
28 |
29 | /**
30 | * Instantiates a new {@code IoUringServerSocket} with a default backlog size of {@code DEFAULT_BACKLOG}.
31 | *
32 | * @param address The address to bind to
33 | * @param port The port to bind to
34 | */
35 | public IoUringServerSocket(String address, int port) {
36 | this(address, port, DEFAULT_BACKLOG);
37 | }
38 |
39 | /**
40 | * Instantiates a new {@code IoUringServerSocket} bound to "127.0.0.1" on the specified port with the default
41 | * backlog size of {@code DEFAULT_BACKLOG}.
42 | *
43 | * @param port The port to bind to
44 | */
45 | public IoUringServerSocket(int port) {
46 | this("127.0.0.1", port, DEFAULT_BACKLOG);
47 | }
48 |
49 | IoUringSocket handleAcceptCompletion(IoUring ioUring, IoUringServerSocket serverSocket, int channelFd, String ipAddress) {
50 | if (channelFd < 0) {
51 | return null;
52 | }
53 | IoUringSocket channel = new IoUringSocket(channelFd, ipAddress, serverSocket.port());
54 | if (serverSocket.acceptHandler() != null) {
55 | serverSocket.acceptHandler().accept(ioUring, channel);
56 | }
57 | return channel;
58 | }
59 |
60 | @Override
61 | public IoUringServerSocket onRead(Consumer buffer) {
62 | throw new UnsupportedOperationException("Server socket cannot read");
63 | }
64 |
65 | @Override
66 | public IoUringServerSocket onWrite(Consumer buffer) {
67 | throw new UnsupportedOperationException("Server socket cannot write");
68 | }
69 |
70 | /**
71 | * Gets the accept handler.
72 | *
73 | * @return the accept handler
74 | */
75 | BiConsumer acceptHandler() {
76 | return acceptHandler;
77 | }
78 |
79 | /**
80 | * Sets the accept handler.
81 | *
82 | * @param acceptHandler the accept handler
83 | * @return this instance
84 | */
85 | public IoUringServerSocket onAccept(BiConsumer acceptHandler) {
86 | this.acceptHandler = acceptHandler;
87 | return this;
88 | }
89 |
90 | private static native void bind(long fd, String host, int port, int backlog);
91 |
92 | static {
93 | NativeLibraryLoader.load();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/IoUringSocket.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import java.util.function.Consumer;
4 |
5 | /**
6 | * A {@code Socket} analog for working with an {@code io_uring}.
7 | */
8 | public class IoUringSocket extends AbstractIoUringSocket {
9 | private Consumer connectHandler;
10 |
11 | /**
12 | * Instantiates a new {@code IoUringSocket}.
13 | *
14 | * @param ipAddress the ip address
15 | * @param port the port
16 | */
17 | public IoUringSocket(String ipAddress, int port) {
18 | super(AbstractIoUringSocket.create(), ipAddress, port);
19 | }
20 |
21 | /**
22 | * Instantiates a new {@code IoUringSocket}.
23 | *
24 | * @param fd the fd
25 | * @param ipAddress the ip address
26 | * @param port the port
27 | */
28 | IoUringSocket(int fd, String ipAddress, int port) {
29 | super(fd, ipAddress, port);
30 | }
31 |
32 | void handleConnectCompletion(IoUring ioUring, int result) {
33 | if (result != 0) {
34 | // TODO: better error messages, users don't have access to errno
35 | throw new RuntimeException("Connection result was: " + result);
36 | }
37 | if (connectHandler != null) {
38 | connectHandler.accept(ioUring);
39 | }
40 | }
41 |
42 | Consumer connectHandler() {
43 | return connectHandler;
44 | }
45 |
46 | /**
47 | * Set the connect handler.
48 | *
49 | * @return this instance
50 | */
51 | public IoUringSocket onConnect(Consumer connectHandler) {
52 | this.connectHandler = connectHandler;
53 | return this;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/util/ByteBufferUtil.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.util;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.charset.StandardCharsets;
5 |
6 | /**
7 | * Utility methods for byte buffers.
8 | */
9 | public class ByteBufferUtil {
10 |
11 | /**
12 | * Wrap a direct byte buffer.
13 | *
14 | * @param data the data
15 | * @return the byte buffer
16 | */
17 | public static ByteBuffer wrapDirect(byte[] data) {
18 | return (ByteBuffer) ByteBuffer.allocateDirect(data.length).put(data).flip();
19 | }
20 |
21 | /**
22 | * Wrap direct byte buffer.
23 | *
24 | * @param utf8 the utf8 string
25 | * @return the byte buffer
26 | */
27 | public static ByteBuffer wrapDirect(String utf8) {
28 | byte[] data = utf8.getBytes(StandardCharsets.UTF_8);
29 | return (ByteBuffer) ByteBuffer.allocateDirect(data.length).put(data).flip();
30 | }
31 |
32 | /**
33 | * Copies a non-direct buffer to a new direct byte buffer, returning the argued buffer immediately if it is already
34 | * direct.
35 | *
36 | * @param buffer the buffer
37 | * @return the direct byte buffer
38 | */
39 | public static ByteBuffer wrapDirect(ByteBuffer buffer) {
40 | if (buffer.isDirect()) {
41 | return buffer;
42 | }
43 | buffer.flip();
44 | return (ByteBuffer) ByteBuffer.allocateDirect(buffer.remaining()).put(buffer).flip();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/util/NativeLibraryLoader.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.util;
2 |
3 | import java.io.File;
4 | import java.io.FileOutputStream;
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 |
8 | public class NativeLibraryLoader {
9 | private static boolean loadAttempted = false;
10 |
11 | public static synchronized void load() {
12 | OsVersionCheck.verifySystemRequirements();
13 | try {
14 | if (loadAttempted) {
15 | return;
16 | }
17 | loadAttempted = true;
18 |
19 | try (InputStream inputStream = NativeLibraryLoader.class.getResourceAsStream("/libnio_uring.so")) {
20 | if (inputStream == null) {
21 | throw new IOException("Native library not found");
22 | }
23 | File tempFile = File.createTempFile("libnio_uring", ".tmp");
24 |
25 | byte[] buffer = new byte[8192];
26 | try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) {
27 | while (inputStream.available() > 0) {
28 | int bytesRead = inputStream.read(buffer);
29 | fileOutputStream.write(buffer, 0, bytesRead);
30 | }
31 | }
32 |
33 | System.load(tempFile.getAbsolutePath());
34 | }
35 | } catch (IOException ex) {
36 | ex.printStackTrace();
37 | System.loadLibrary("nio_uring");
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/util/OsVersionCheck.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.util;
2 |
3 | /**
4 | * Operating system version check utility.
5 | */
6 | public class OsVersionCheck {
7 | private static final String OS = "Linux";
8 | private static final int MAJOR_VERSION = 5;
9 | private static final int MINOR_VERSION = 1;
10 |
11 | /**
12 | * Verifies that the host system is Linux 5.1 or greater.
13 | */
14 | public static void verifySystemRequirements() {
15 | String os = System.getProperty("os.name");
16 | if (!os.contains(OS)) {
17 | failed();
18 | }
19 |
20 | String version = System.getProperty("os.version");
21 | String[] versionTokens = version.split("\\.");
22 |
23 | int versionMajor = Integer.parseInt(versionTokens[0]);
24 | if (versionMajor < MAJOR_VERSION) {
25 | failed();
26 | }
27 |
28 | int versionMinor = Integer.parseInt(versionTokens[1]);
29 | if (versionMinor < MINOR_VERSION && versionMajor == MAJOR_VERSION) {
30 | failed();
31 | }
32 | }
33 |
34 | private static void failed() {
35 | throw new RuntimeException("Linux >= 5.1 required");
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/sh/blake/niouring/util/ReferenceCounter.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring.util;
2 |
3 | public class ReferenceCounter {
4 | private final T ref;
5 | private int referenceCount = 0;
6 |
7 | public ReferenceCounter(T ref) {
8 | this.ref = ref;
9 | }
10 |
11 | public T ref() {
12 | return ref;
13 | }
14 |
15 | public int incrementReferenceCount() {
16 | return ++referenceCount;
17 | }
18 |
19 | public int deincrementReferenceCount() {
20 | return --referenceCount;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/java/sh/blake/niouring/IoUringFileTest.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | import java.nio.ByteBuffer;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.concurrent.atomic.AtomicBoolean;
9 | import java.util.concurrent.atomic.AtomicInteger;
10 |
11 | public class IoUringFileTest extends TestBase {
12 |
13 | @Test
14 | public void open_and_read_readme_should_succeed() {
15 | String fileName = "src/test/resources/test-file.txt";
16 | AtomicBoolean readSuccessfully = new AtomicBoolean(false);
17 |
18 | IoUringFile file = new IoUringFile(fileName);
19 | file.onRead(in -> {
20 | in.flip();
21 | String fileStr = StandardCharsets.UTF_8.decode(in).toString();
22 | if (fileStr.startsWith("Hello, world!")) {
23 | readSuccessfully.set(true);
24 | }
25 | });
26 |
27 | ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 10);
28 | IoUring ioUring = new IoUring(TEST_RING_SIZE)
29 | .onException(Exception::printStackTrace)
30 | .queueRead(file, buffer);
31 |
32 | attemptUntil(ioUring::execute, readSuccessfully::get);
33 |
34 | Assert.assertTrue("File read successfully", readSuccessfully.get());
35 | }
36 |
37 | @Test
38 | public void seek_read_readme_should_succeed() {
39 | String fileName = "src/test/resources/test-file.txt";
40 | AtomicInteger readCounter = new AtomicInteger(0);
41 | AtomicBoolean seekSuccessfully = new AtomicBoolean(false);
42 | int chunkSize = 7;
43 | ByteBuffer buffer = ByteBuffer.allocateDirect(chunkSize);
44 |
45 | IoUring ioUring = new IoUring(TEST_RING_SIZE)
46 | .onException(Exception::printStackTrace);
47 |
48 | IoUringFile file = new IoUringFile(fileName);
49 | file.onRead(in -> {
50 | in.flip();
51 | int count = readCounter.incrementAndGet();
52 | String fileStr = StandardCharsets.UTF_8.decode(in).toString();
53 |
54 | if (count == 1) {
55 | in.clear(); // reuse the buffer provided
56 | ioUring.queueRead(file, in, chunkSize); // read with offset
57 | } else if (fileStr.startsWith("world!")) {
58 | seekSuccessfully.set(true);
59 | }
60 | });
61 |
62 | ioUring.queueRead(file, buffer);
63 | attemptUntil(ioUring::execute, seekSuccessfully::get);
64 |
65 | Assert.assertTrue("File seek successfully", seekSuccessfully.get());
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/test/java/sh/blake/niouring/IoUringSocketTest.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 | import sh.blake.niouring.util.ByteBufferUtil;
6 |
7 | import java.nio.ByteBuffer;
8 | import java.nio.charset.StandardCharsets;
9 | import java.util.concurrent.atomic.AtomicBoolean;
10 |
11 | public class IoUringSocketTest extends TestBase {
12 |
13 | @Test
14 | public void test_create_server_and_connect_should_succeed() {
15 | int port = randomPort();
16 |
17 | AtomicBoolean accepted = new AtomicBoolean(false);
18 | IoUringServerSocket serverSocket = new IoUringServerSocket(port);
19 | serverSocket.onAccept((ring, socket) -> {
20 | accepted.set(true);
21 | serverSocket.close();
22 | });
23 | serverSocket.onException(Exception::printStackTrace);
24 |
25 | AtomicBoolean connected = new AtomicBoolean(false);
26 | IoUringSocket socket = new IoUringSocket("127.0.0.1", port);
27 | socket.onException(Exception::printStackTrace);
28 | socket.onConnect(ring -> {
29 | connected.set(true);
30 | socket.close();
31 | });
32 |
33 | IoUring ioUring = new IoUring(TEST_RING_SIZE)
34 | .onException(Exception::printStackTrace)
35 | .queueAccept(serverSocket)
36 | .queueConnect(socket);
37 |
38 | attemptUntil(ioUring::execute, () -> accepted.get() && connected.get());
39 |
40 | ioUring.close();
41 |
42 | Assert.assertTrue("Server accepted connection", accepted.get());
43 | Assert.assertTrue("Client connected", connected.get());
44 | }
45 |
46 | @Test
47 | public void test_create_server_and_connect_with_wrong_port_should_produce_exception() {
48 | int port = randomPort();
49 |
50 | AtomicBoolean accepted = new AtomicBoolean(false);
51 | AtomicBoolean exceptionProduced = new AtomicBoolean(false);
52 |
53 | IoUringServerSocket serverSocket = new IoUringServerSocket(port);
54 | serverSocket.onAccept((ring, socket) -> accepted.set(true));
55 | serverSocket.onException(Exception::printStackTrace);
56 |
57 | IoUringSocket socket = new IoUringSocket("127.0.0.1", port + 1);
58 | socket.onException(ex -> exceptionProduced.set(true));
59 |
60 | IoUring ioUring = new IoUring(TEST_RING_SIZE)
61 | .onException(Exception::printStackTrace)
62 | .queueAccept(serverSocket)
63 | .queueConnect(socket);
64 |
65 | attemptUntil(ioUring::execute, exceptionProduced::get);
66 |
67 | ioUring.close();
68 | serverSocket.close();
69 | socket.close();
70 |
71 | Assert.assertFalse("Server accepted connection", accepted.get());
72 | Assert.assertTrue("Client to connect (wrong port)", exceptionProduced.get());
73 | }
74 |
75 | @Test
76 | public void test_create_server_connect_and_send_data_should_be_received() {
77 | int port = randomPort();
78 | String message = "Test over port " + randomPort();
79 |
80 | AtomicBoolean serverAccepted = new AtomicBoolean(false);
81 | AtomicBoolean serverSent = new AtomicBoolean(false);
82 | AtomicBoolean clientConnected = new AtomicBoolean(false);
83 | AtomicBoolean clientReceived = new AtomicBoolean(false);
84 |
85 | IoUringServerSocket serverSocket = new IoUringServerSocket(port);
86 | serverSocket.onException(Exception::printStackTrace);
87 | serverSocket.onAccept((ring, socket) -> {
88 | ByteBuffer testBuffer = ByteBufferUtil.wrapDirect(message);
89 | socket.onWrite(out -> socket.close());
90 | ring.queueWrite(socket, testBuffer);
91 | serverAccepted.set(true);
92 | serverSent.set(true);
93 | });
94 |
95 | IoUringSocket socket = new IoUringSocket("127.0.0.1", port);
96 | socket.onException(Exception::printStackTrace);
97 | socket.onConnect(ring -> {
98 | ring.queueRead(socket, ByteBuffer.allocateDirect(32));
99 | clientConnected.set(true);
100 | });
101 | socket.onRead(in -> {
102 | in.flip();
103 | String payload = StandardCharsets.UTF_8.decode(in).toString();
104 | if (payload.equals(message)) {
105 | clientReceived.set(true);
106 | }
107 | socket.close();
108 | });
109 |
110 | IoUring ioUring = new IoUring(TEST_RING_SIZE)
111 | .onException(Exception::printStackTrace)
112 | .queueAccept(serverSocket)
113 | .queueConnect(socket);
114 |
115 | attemptUntil(ioUring::execute, () ->
116 | serverAccepted.get() && clientConnected.get() && serverSent.get() && clientReceived.get());
117 |
118 | ioUring.close();
119 |
120 | Assert.assertTrue("Server accepted connection", serverAccepted.get());
121 | Assert.assertTrue("Server sent data", serverSent.get());
122 | Assert.assertTrue("Client connected", clientConnected.get());
123 | Assert.assertTrue("Client received data", clientReceived.get());
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/test/java/sh/blake/niouring/TestBase.java:
--------------------------------------------------------------------------------
1 | package sh.blake.niouring;
2 |
3 | import java.util.Random;
4 | import java.util.function.Supplier;
5 |
6 | public class TestBase {
7 | public static final int TEST_RING_SIZE = 8;
8 | public static final int MAX_ATTEMPTS = 10;
9 |
10 | public int randomPort() {
11 | return new Random(System.currentTimeMillis()).nextInt(64510) + 1024;
12 | }
13 |
14 | void attemptUntil(Runnable runnable, Supplier condition) {
15 | for (int i = 0; i < MAX_ATTEMPTS; i++) {
16 | if (condition.get()) {
17 | return;
18 | }
19 | runnable.run();
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/resources/test-file.txt:
--------------------------------------------------------------------------------
1 | Hello, world!
2 |
3 | Do not change this file unless you want to fix broken unit tests!
--------------------------------------------------------------------------------