├── .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! --------------------------------------------------------------------------------