diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml new file mode 100644 index 0000000..fa884c2 --- /dev/null +++ b/.github/workflows/scala.yml @@ -0,0 +1,36 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Scala CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install libuv + run: sudo apt-get install libuv1-dev + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'sbt' + - name: Run tests + run: sbt test + # Optional: This step uploads information to the GitHub dependency graph and unblocking Dependabot alerts for the repository + - name: Upload dependency graph + uses: scalacenter/sbt-dependency-submission@ab086b50c947c9774b70f39fc7f6e20ca2706c91 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4106306 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/project/target +/project/project +sbt-launch.jar +src/test/resources/write-test.txt diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..db004c6 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,2 @@ +version = "3.7.3" +runner.dialect = scala3 \ No newline at end of file diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..425d166 --- /dev/null +++ b/build.sbt @@ -0,0 +1,32 @@ +scalaVersion := "3.3.1" + +enablePlugins(ScalaNativePlugin) +enablePlugins(ScalaNativeJUnitPlugin) + +organization := "quelgar.github.com" + +name := "scala-uv" + +// set to Debug for compilation details (Info is default) +logLevel := Level.Info + +// import to add Scala Native options +import scala.scalanative.build._ + +// defaults set with common options shown +nativeConfig ~= { c => + c.withLTO(LTO.none) // thin + .withMode(Mode.debug) // releaseFast + .withGC(GC.immix) // commix +} + +scalacOptions ++= Seq( + "-new-syntax", + "-no-indent", + "-Wvalue-discard", + "-Wunused:all", + "-Werror", + "-deprecation" +) + +// Test / nativeLinkingOptions += "-luv" diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..b19d4e1 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..6913a59 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") diff --git a/src/main/resources/scala-native/errors.c b/src/main/resources/scala-native/errors.c new file mode 100644 index 0000000..d3acf03 --- /dev/null +++ b/src/main/resources/scala-native/errors.c @@ -0,0 +1,9 @@ +#include + +#define XX(error_name, message) \ + int uv_scala_errorcode_##error_name() \ + { \ + return UV_##error_name; \ + } +UV_ERRNO_MAP(XX) +#undef XX diff --git a/src/main/resources/scala-native/helpers.c b/src/main/resources/scala-native/helpers.c new file mode 100644 index 0000000..9157168 --- /dev/null +++ b/src/main/resources/scala-native/helpers.c @@ -0,0 +1,75 @@ +#include +#include + +void uv_scala_buf_init(char *base, unsigned int len, uv_buf_t *buffer) +{ + uv_buf_t buf = uv_buf_init(base, len); + buffer->base = buf.base; + buffer->len = buf.len; +} + +void *uv_scala_buf_base(const uv_buf_t *buffer) +{ + return buffer->base; +} + +size_t uv_scala_buf_len(const uv_buf_t *buffer) +{ + return buffer->len; +} + +size_t uv_scala_buf_struct_size() +{ + return sizeof(uv_buf_t); +} + +size_t uv_scala_mutex_t_size() +{ + return sizeof(uv_mutex_t); +} + +uv_stream_t *uv_scala_connect_stream_handle(const uv_connect_t *req) +{ + return req->handle; +} + +uv_stream_t *uv_scala_shutdown_stream_handle(const uv_shutdown_t *req) +{ + return req->handle; +} + +uv_stream_t *uv_scala_write_stream_handle(const uv_write_t *req) +{ + return req->handle; +} + +uv_stream_t *uv_scala_send_stream_handle(const uv_write_t *req) +{ + return req->send_handle; +} + +size_t uv_scala_sizeof_sockaddr_in() +{ + return sizeof(struct sockaddr_in); +} + +void uv_scala_init_sockaddr_in(int address, int port, struct sockaddr_in *addr) +{ + addr->sin_family = AF_INET; + addr->sin_addr.s_addr = htonl(address); + addr->sin_port = htons(port); +} + +size_t uv_scala_sizeof_sockaddr_in6() +{ + return sizeof(struct sockaddr_in6); +} + +void uv_scala_init_sockaddr_in6(const char *address, int port, unsigned int flow_info, unsigned int scope_id, struct sockaddr_in6 *addr) +{ + addr->sin6_family = AF_INET6; + memcpy(&(addr->sin6_addr), address, sizeof(struct in6_addr)); + addr->sin6_port = htons(port); + addr->sin6_flowinfo = htonl(flow_info); + addr->sin6_scope_id = htonl(scope_id); +} diff --git a/src/main/resources/scala-native/platform_specific.c b/src/main/resources/scala-native/platform_specific.c new file mode 100644 index 0000000..f5bd292 --- /dev/null +++ b/src/main/resources/scala-native/platform_specific.c @@ -0,0 +1,7 @@ + + +#ifdef _WIN32 + +#else + +#endif \ No newline at end of file diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala new file mode 100644 index 0000000..e6ecba8 --- /dev/null +++ b/src/main/scala/Main.scala @@ -0,0 +1,70 @@ +import scalauv.* + +import scala.scalanative.* +import unsafe.* +import unsigned.* +import LibUv.* +import UvUtils.* + +final class Test { + + private var done = false + + def callback: AsyncCallback = { (handle: AsyncHandle) => + println("Callback!!") + done = true + uv_close(handle, null) + } + + def run() = { + + withZone { + + val loop = uv_default_loop() + + val asyncHandle = UvUtils.zoneAllocateHandle(HandleType.UV_ASYNC) + uv_async_init(loop, asyncHandle, callback).checkErrorThrowIO() + + uv_async_send(asyncHandle).checkErrorThrowIO() + + println(s"Test before, done = $done") + uv_run(loop, RunMode.DEFAULT).checkErrorThrowIO() + println(s"Test after, done = $done") + } + + } + +} + +object Test + +object Main { + + private var done = false + + private val callback: AsyncCallback = { (handle: AsyncHandle) => + println("Callback!!") + done = true + uv_close(handle, null) + } + + def main(args: Array[String]): Unit = { + + withZone { + val loop = uv_default_loop() + + val asyncHandle = UvUtils.zoneAllocateHandle(HandleType.UV_ASYNC) + uv_async_init(loop, asyncHandle, callback).checkErrorThrowIO() + + uv_async_send(asyncHandle).checkErrorThrowIO() + + println(s"Main before, done = $done") + uv_run(loop, RunMode.DEFAULT).checkErrorThrowIO() + println(s"Main after, done = $done") + } + + // val test = new Test + // test.run() + } + +} diff --git a/src/main/scala/scalauv/Buffer.scala b/src/main/scala/scalauv/Buffer.scala new file mode 100644 index 0000000..89e0895 --- /dev/null +++ b/src/main/scala/scalauv/Buffer.scala @@ -0,0 +1,109 @@ +package scalauv + +import scalanative.unsafe.* +import scalanative.unsigned.* +import java.nio +import java.nio.charset.StandardCharsets +import scala.scalanative.libc.stdlib + +opaque type Buffer = Ptr[Byte] + +extension (buffer: Buffer) { + + def base: Ptr[Byte] = helpers.uv_scala_buf_base(buffer) + def length: Int = helpers.uv_scala_buf_len(buffer).toInt + + def apply(index: Int): Byte = base(index) + + def asNio: nio.ByteBuffer = ??? + + def asUtf8String(max: Int): String = + new String(asArray(max), StandardCharsets.UTF_8) + + def asArray(max: Int): Array[Byte] = { + val a = Array.ofDim[Byte](max) + for i <- 0 until max do { + a(i) = base(i) + } + a + } + + def foreachByte(max: Int)(f: Byte => Unit): Unit = { + for i <- 0 until max do { + f(base(i)) + } + } + + def +(index: Int): Buffer = + buffer + (index.toLong * Buffer.structureSize.toLong) + + inline def toNative: Ptr[Byte] = buffer + + inline def init(base: Ptr[Byte], size: CSize): Unit = + helpers.uv_scala_buf_init(base, size.toUInt, buffer) + + inline def mallocInit(size: CSize): Unit = + helpers.uv_scala_buf_init(stdlib.malloc(size), size.toUInt, buffer) + + inline def free(): Unit = stdlib.free(buffer) +} + +object Buffer { + + given Tag[Buffer] = Tag.Ptr[Byte](summon[Tag[Byte]]) + + val structureSize: CSize = helpers.uv_scala_buf_struct_size() + + inline def unsafeFromNative(ptr: Ptr[Byte]): Buffer = ptr + + inline def stackAllocate( + ptr: Ptr[Byte], + size: CUnsignedInt + ): Buffer = { + val uvBuf = stackalloc[Byte](structureSize) + helpers.uv_scala_buf_init(ptr, size, uvBuf) + uvBuf + } + + inline def stackAllocate( + array: Array[Byte], + index: Int = 0 + ): Buffer = { + val uvBuf = stackalloc[Byte](structureSize) + helpers.uv_scala_buf_init( + array.at(index), + (array.length - index).toUInt, + uvBuf + ) + uvBuf + } + + def zoneAllocate(array: Array[Byte], index: Int = 0)(using + Zone + ): Buffer = { + val uvBuf = alloc[Byte](structureSize) + helpers.uv_scala_buf_init( + array.at(index), + (array.length - index).toUInt, + uvBuf + ) + uvBuf + } + + def malloc(base: Ptr[Byte], size: CSize): Buffer = { + val uvBuf = stdlib.malloc(structureSize.toULong) + helpers.uv_scala_buf_init(base, size.toUInt, uvBuf) + uvBuf + } + + def malloc(array: Array[Byte], index: Int = 0): Buffer = { + val uvBuf = stdlib.malloc(structureSize.toULong) + helpers.uv_scala_buf_init( + array.at(index), + (array.length - index).toUInt, + uvBuf + ) + uvBuf + } + +} diff --git a/src/main/scala/scalauv/LibUv.scala b/src/main/scala/scalauv/LibUv.scala new file mode 100644 index 0000000..a38c678 --- /dev/null +++ b/src/main/scala/scalauv/LibUv.scala @@ -0,0 +1,504 @@ +package scalauv + +import scala.scalanative.unsafe.* + +@link("uv") +@extern +object LibUv { + + type Loop = Ptr[Byte] + + type Handle = Ptr[Byte] + + type StreamHandle = Ptr[Byte] + + type TcpHandle = Handle + + type FileHandle = CInt + + type AsyncHandle = Handle + + type TimerHandle = Handle + + type PipeHandle = Handle + + type CloseCallback = CFuncPtr1[Handle, Unit] + + type AsyncCallback = CFuncPtr1[AsyncHandle, Unit] + + type TimerCallback = CFuncPtr1[TimerHandle, Unit] + + type FsCallback = CFuncPtr1[Req, Unit] + + type AllocCallback = CFuncPtr3[Handle, CSize, Buffer, Unit] + + type StreamReadCallback = CFuncPtr3[StreamHandle, CSSize, Buffer, Unit] + + type StreamWriteCallback = CFuncPtr2[WriteReq, CInt, Unit] + + type ConnectCallback = CFuncPtr2[ConnectReq, CInt, Unit] + + type ShutdownCallback = CFuncPtr2[Req, CInt, Unit] + + type ConnectionCallback = CFuncPtr2[StreamHandle, ErrorCode, Unit] + + type Req = Ptr[Byte] + + type ConnectReq = Req + + type ShutdownReq = Req + + type WriteReq = Req + + type FileReq = Req + + type ErrorCode = CInt + + type RequestType = CInt + + type HandleType = CInt + + type RunMode = CInt + + type Thread = Ptr[Byte] + + type ThreadCallback = CFuncPtr1[Thread, Unit] + + type ThreadLocalKey = Ptr[Byte] + + type OnceOnly = Ptr[Byte] + + type Mutex = Ptr[Byte] + + type ReadWriteLock = Ptr[Byte] + + type Semaphore = Ptr[Byte] + + type Condition = Ptr[Byte] + + type Barrier = Ptr[Byte] + + type SocketHandle = CInt + + // ========================================================= + // Basics + + def uv_strerror_r(err: CInt, buf: CString, buflen: CSize): CString = extern + + def uv_err_name_r(err: CInt, buf: CString, buflen: CSize): CString = extern + + def uv_translate_sys_error(sys_errno: CInt): CInt = extern + + def uv_default_loop(): Loop = extern + + def uv_run(loop: Loop, runMode: RunMode): ErrorCode = extern + + def uv_loop_close(loop: Loop): ErrorCode = extern + + def uv_loop_size(): CSize = extern + + def uv_loop_init(loop: Loop): ErrorCode = extern + +// def uv_loop_configure(loop: Loop, option: CInt, value: CInt): ErrorCode = +// extern + + def uv_ip4_addr( + ip: CString, + port: CInt, + addr: SocketAddressIp4 + ): ErrorCode = + extern + + def uv_ip6_addr( + ip: CString, + port: CInt, + addr: SocketAddressIp6 + ): ErrorCode = + extern + + def uv_ip4_name( + src: Ptr[SocketAddressIp4], + dst: CString, + size: CSize + ): ErrorCode = extern + + def uv_ip6_name( + src: Ptr[SocketAddressIp6], + dst: CString, + size: CSize + ): ErrorCode = extern + + // ========================================================= + // Handles + + def uv_handle_size(handle_type: HandleType): CSize = extern + + def uv_handle_get_loop(handle: Handle): Loop = extern + + def uv_handle_get_data(handle: Handle): Ptr[Byte] = extern + + def uv_handle_set_data(handle: Handle, data: Ptr[Byte]): Unit = extern + + def uv_handle_get_type(handle: Handle): HandleType = extern + + def uv_handle_type_name(handleType: HandleType): CString = extern + + def uv_is_active(handle: Handle): CInt = extern + + def uv_is_closing(handle: Handle): CInt = extern + + def uv_close(handle: Handle, callback: CloseCallback): Unit = extern + + def uv_ref(handle: Handle): Unit = extern + + def uv_unref(handle: Handle): Unit = extern + + def uv_has_ref(handle: Handle): CInt = extern + + // ========================================================= + // Requests + + def uv_req_size(req_type: RequestType): CSize = extern + + def uv_req_get_data(req: Req): Ptr[Byte] = extern + + def uv_req_set_data(req: Req, data: Ptr[Byte]): Unit = extern + + def uv_req_get_type(req: Req): RequestType = extern + + def uv_req_type_name(reqType: RequestType): CString = extern + + // ========================================================= + // Async & Timers + + def uv_async_init( + loop: Loop, + handle: AsyncHandle, + callback: AsyncCallback + ): ErrorCode = extern + + def uv_async_send(handle: AsyncHandle): ErrorCode = extern + + def uv_timer_init(loop: Loop, handle: TimerHandle): ErrorCode = extern + + def uv_timer_start( + handle: TimerHandle, + callback: TimerCallback, + timeoutMillis: CUnsignedLong, + repeatMillis: CUnsignedLong + ): ErrorCode = extern + + // ========================================================= + // Files + + def uv_fs_open( + loop: Loop, + req: FileReq, + path: CString, + flags: CInt, + mode: CInt, + callback: FsCallback + ): FileHandle = extern + + def uv_fs_close( + loop: Loop, + req: FileReq, + file: FileHandle, + callback: FsCallback + ): ErrorCode = extern + + def uv_fs_req_cleanup(req: FileReq): Unit = extern + + def uv_fs_read( + loop: Loop, + req: FileReq, + file: FileHandle, + bufs: Buffer, + numberOfBufs: CUnsignedInt, + offset: Long, + cb: FsCallback + ): CInt = extern + + def uv_fs_write( + loop: Loop, + req: FileReq, + file: FileHandle, + bufs: Buffer, + numberOfBufs: CUnsignedInt, + offset: Long, + cb: FsCallback + ): CInt = extern + + def uv_fs_access( + loop: Loop, + req: FileReq, + path: CString, + mode: CInt, + cb: FsCallback + ): CInt = extern + + // ========================================================= + // Streams + + def uv_shutdown( + req: ShutdownReq, + handle: StreamHandle, + cb: ShutdownCallback + ): ErrorCode = extern + + def uv_listen( + stream: StreamHandle, + backlog: CInt, + cb: ConnectionCallback + ): ErrorCode = extern + + def uv_accept( + server: StreamHandle, + client: StreamHandle + ): ErrorCode = extern + + def uv_read_start( + stream: StreamHandle, + alloc_cb: AllocCallback, + read_cb: StreamReadCallback + ): ErrorCode = extern + + def uv_read_stop(stream: StreamHandle): ErrorCode = extern + + def uv_write( + req: WriteReq, + handle: StreamHandle, + bufs: Buffer, + numberOfBufs: CUnsignedInt, + cb: StreamWriteCallback + ): ErrorCode = extern + + def uv_write2( + req: WriteReq, + handle: StreamHandle, + bufs: Buffer, + numberOfBufs: CUnsignedInt, + sendHandle: StreamHandle, + cb: StreamWriteCallback + ): ErrorCode = extern + + def uv_try_write( + handle: StreamHandle, + bufs: Buffer, + numberOfBufs: CUnsignedInt + ): CInt = extern + + def uv_try_write2( + handle: StreamHandle, + bufs: Buffer, + numberOfBufs: CUnsignedInt, + sendHandle: StreamHandle + ): CInt = extern + + def uv_is_readable(handle: StreamHandle): CInt = extern + + def uv_is_writable(handle: StreamHandle): CInt = extern + + def uv_stream_set_blocking(handle: StreamHandle, blocking: CInt): CInt = + extern + + def uv_stream_get_write_queue_size(handle: StreamHandle): CSize = extern + + // ========================================================= + // TCP + + def uv_tcp_init(loop: Loop, handle: TcpHandle): ErrorCode = extern + + def uv_tcp_init_ex( + loop: Loop, + handle: TcpHandle, + flags: CUnsignedInt + ): ErrorCode = extern + + def uv_tcp_open(handle: TcpHandle, sock: CInt): ErrorCode = extern + + def uv_tcp_no_delay(handle: TcpHandle, enable: CInt): ErrorCode = extern + + def uv_tcp_keepalive( + handle: TcpHandle, + enable: CInt, + delay: CUnsignedInt + ): ErrorCode = extern + + def uv_tcp_simultaneous_accepts(handle: TcpHandle, enable: CInt): ErrorCode = + extern + + def uv_tcp_bind( + handle: TcpHandle, + addr: SocketAddressIp4, + flags: CUnsignedInt + ): ErrorCode = extern + + def uv_tcp_getsockname( + handle: TcpHandle, + name: SocketAddressIp4, + namelen: Ptr[CInt] + ): ErrorCode = extern + + def uv_tcp_getpeername( + handle: TcpHandle, + name: SocketAddressIp4, + namelen: Ptr[CInt] + ): ErrorCode = extern + + def uv_tcp_connect( + req: Req, + handle: TcpHandle, + addr: SocketAddressIp4, + cb: ConnectCallback + ): ErrorCode = extern + + def uv_tcp_close_reset(handle: TcpHandle, cb: CloseCallback): ErrorCode = + extern + + def uv_socketpair( + socketType: CInt, + protocol: CInt, + socketVector: Ptr[CArray[SocketHandle, Nat._2]], + flags0: CInt, + flags1: CInt + ): ErrorCode = extern + + // ========================================================= + // Pipes + + def uv_pipe_init( + loop: Loop, + handle: PipeHandle, + ipc: CInt + ): ErrorCode = extern + + def uv_pipe_open( + handle: PipeHandle, + file: FileHandle + ): ErrorCode = extern + + def uv_pipe_bind( + handle: PipeHandle, + name: CString + ): ErrorCode = extern + + def uv_pipe_bind2( + handle: PipeHandle, + name: CString, + nameLength: CSize, + flags: CUnsignedInt + ): ErrorCode = extern + + def uv_pipe_connect( + req: ConnectReq, + handle: PipeHandle, + name: CString, + cb: ConnectCallback + ): Unit = extern + + def uv_pipe_connect2( + req: ConnectReq, + handle: PipeHandle, + name: CString, + nameLength: CSize, + flags: CUnsignedInt, + cb: ConnectCallback + ): Unit = extern + + def uv_pipe_getsockname( + handle: PipeHandle, + buffer: CString, + nameLength: Ptr[CSize] + ): ErrorCode = extern + + def uv_pipe_getpeername( + handle: PipeHandle, + buffer: CString, + nameLength: Ptr[CSize] + ): ErrorCode = extern + + def uv_pipe_pending_instances( + handle: PipeHandle, + count: CInt + ): Unit = extern + + def uv_pipe_pending_count(handle: PipeHandle): CSize = extern + + def uv_pipe_pending_type(handle: PipeHandle): HandleType = extern + + def uv_pipe_chmod(handle: PipeHandle, flags: CInt): ErrorCode = extern + + def uv_pipe( + fileHandles: Ptr[FileHandle], + readFlags: CInt, + writeFlags: CInt + ): ErrorCode = extern + + // ========================================================= + // Mutexes + + def uv_mutex_init(mutex: Mutex): ErrorCode = extern + + def uv_mutex_init_recursive(mutex: Mutex): ErrorCode = extern + + def uv_mutex_destroy(mutex: Mutex): Unit = extern + + def uv_mutex_lock(mutex: Mutex): Unit = extern + + def uv_mutex_trylock(mutex: Mutex): ErrorCode = extern + + def uv_mutex_unlock(mutex: Mutex): Unit = extern + +} + +@extern +private[scalauv] object helpers { + + def uv_scala_buf_init( + base: Ptr[CChar], + len: CUnsignedInt, + buffer: Ptr[Byte] + ): Unit = extern + + def uv_scala_buf_base(buffer: Ptr[Byte]): Ptr[Byte] = extern + + def uv_scala_buf_len(buffer: Ptr[Byte]): CSize = extern + + def uv_scala_buf_struct_size(): CSize = extern + + def uv_scala_mutex_t_size(): CSize = extern + + def uv_scala_connect_stream_handle( + req: LibUv.ConnectReq + ): LibUv.StreamHandle = + extern + + def uv_scala_shutdown_stream_handle(req: LibUv.Req): LibUv.StreamHandle = + extern + + def uv_scala_write_stream_handle(req: LibUv.Req): LibUv.StreamHandle = extern + + def uv_scala_send_stream_handle(req: LibUv.Req): LibUv.StreamHandle = extern + + def uv_scala_sizeof_sockaddr_in(): CSize = extern + + def uv_scala_init_sockaddr_in( + address: CInt, + port: CInt, + socketAddress: SocketAddressIp4 + ): Unit = + extern + + def uv_scala_sizeof_sockaddr_in6(): CSize = extern + + def uv_scala_init_sockaddr_in6( + address: Ptr[Byte], + port: CInt, + flowInfo: CUnsignedInt, + scopeId: CUnsignedInt, + socketAddress: SocketAddressIp6 + ): Unit = + extern + +} diff --git a/src/main/scala/scalauv/UvConstants.scala b/src/main/scala/scalauv/UvConstants.scala new file mode 100644 index 0000000..553a879 --- /dev/null +++ b/src/main/scala/scalauv/UvConstants.scala @@ -0,0 +1,80 @@ +package scalauv + +import LibUv.* +import scala.scalanative.unsafe.* + +type UvBufferSize = Nat.Digit2[Nat._1, Nat._6] + +object HandleType { + val UV_UNKNOWN_HANDLE: HandleType = 0 + val UV_ASYNC: HandleType = 1 + val UV_CHECK: HandleType = 2 + val UV_FS_EVENT: HandleType = 3 + val UV_FS_POLL: HandleType = 4 + val UV_HANDLE: HandleType = 5 + val UV_IDLE: HandleType = 6 + val UV_NAMED_PIPE: HandleType = 7 + val UV_POLL: HandleType = 8 + val UV_PREPARE: HandleType = 9 + val UV_PROCESS: HandleType = 10 + val UV_STREAM: HandleType = 11 + val UV_TCP: HandleType = 12 + val UV_TIMER: HandleType = 13 + val UV_TTY: HandleType = 14 + val UV_UDP: HandleType = 15 + val UV_SIGNAL: HandleType = 16 + val UV_FILE: HandleType = 17 + val UV_HANDLE_TYPE_MAX: HandleType = 18 +} + +object RunMode { + val DEFAULT: RunMode = 0 + val ONCE: RunMode = 1 + val NOWAIT: RunMode = 2 +} + +object RequestType { + val UNKNOWN_REQ: RequestType = 0 + val REQ: RequestType = 1 + val CONNECT: RequestType = 2 + val WRITE: RequestType = 3 + val SHUTDOWN: RequestType = 4 + val UDP_SEND: RequestType = 5 + val FS: RequestType = 6 + val WORK: RequestType = 7 + val GETADDRINFO: RequestType = 8 + val GETNAMEINFO: RequestType = 9 + val REQ_TYPE_MAX: RequestType = 10 +} + +object FileOpenFlags { + val O_RDONLY = 0 + val O_WRONLY = 1 + val O_RDWR = 2 + + val O_CREAT = 0x200 + val O_EXCL = 0x800 + val O_TRUNC = 0x400 + + val O_APPEND = 0x08 + val O_DSYNC = 0x400000 + val O_SYNC = 0x80 +} + +object CreateMode { + + val S_IRUSR = 0x100 + val S_IWUSR = 0x80 + + val None = 0 + +} + +object AccessCheckMode { + + val F_OK = 0 + val R_OK = 4 + val W_OK = 2 + val X_OK = 1 + +} diff --git a/src/main/scala/scalauv/UvUtils.scala b/src/main/scala/scalauv/UvUtils.scala new file mode 100644 index 0000000..75a4155 --- /dev/null +++ b/src/main/scala/scalauv/UvUtils.scala @@ -0,0 +1,435 @@ +package scalauv + +import LibUv.* +import scala.scalanative.unsafe.* +import scala.scalanative.libc.* +import scala.scalanative.unsigned.* +import java.io.IOException +import scala.util.boundary +import java.nio.charset.StandardCharsets +import java.nio.charset.Charset + +inline def withZone[A](f: Zone ?=> A): A = Zone(z => f(using z)) + +def mallocCString( + s: String, + charset: Charset = StandardCharsets.UTF_8 +): CString = { + if s.isEmpty() then c"" + else { + val bytes = s.getBytes(charset) + val size = bytes.length.toUInt + val cString = stdlib.malloc(size + 1.toUInt) + string.memcpy(cString, bytes.at(0), size) + !(cString + size) = 0.toByte + cString + } +} + +/** Utility to help deal with errors returned by the libuv API. Constructor + * methods like `attempt` can be used the check the libuv return value, + * constructing a `Failure` case if it is < 0 and `Success` otherwise. As with + * `Either`, the `flatMap` does not continue if there is a failure. It is also + * possible to register callbacks to be run if there is a failure, using + * methods like `onFail`. + */ +enum Uv[+A] { + + case Success(onFailActions: Vector[Uv.Error => Unit], result: A) extends Uv[A] + case Failure(error: Uv.Error) extends Uv[Nothing] + + def errorMessage: Option[String] = this match { + case Success(_, _) => + None + case Failure(error) => + Some(error.message) + } + + def map[B](f: A => B): Uv[B] = this match { + case Success(onFailActions, result) => + Success(onFailActions, f(result)) + case Failure(error) => + Failure(error) + } + + def flatMap[B](f: A => Uv[B]): Uv[B] = this match { + case Success(onFailActions, result) => + f(result) match { + case Success(onFailActions2, result2) => + Success(onFailActions2 ++ onFailActions, result2) + case Failure(error) => + onFailActions.foreach(_(error)) + Failure(error) + } + case Failure(error) => + Failure(error) + } + + def foreach(f: A => Unit): Unit = this match { + case Success(_, result) => + f(result) + case Failure(_) => + () + } + + def as[B](b: B): Uv[B] = this match { + case Success(onFailActions, _) => + Success(onFailActions, b) + case Failure(error) => + Failure(error) + } + + def mapErrorMessage(f: String => String): Uv[A] = this match { + case s @ Success(_, _) => + s + case Failure(error) => + Failure(error.copy(message = f(error.message))) + } + + def onFailError(f: Uv.Error => Unit): Uv[A] = this match { + case Success(onFailActions, result) => + Success(f +: onFailActions, result) + case Failure(error) => + f(error) + this + } + + def onFail(f: => Unit): Uv[A] = onFailError(_ => f) + + def toEither: Either[Uv.Error, A] = this match { + case Success(_, result) => + Right(result) + case Failure(error) => + Left(error) + } + + def eitherMessage: Either[String, A] = this match { + case Success(_, result) => + Right(result) + case Failure(error) => + Left(error.message) + } + + def fold[B](onFail: Uv.Error => B, onSuccess: A => B): B = this match { + case Success(_, result) => + onSuccess(result) + case Failure(error) => + onFail(error) + } + + def foreachFailure(f: Uv.Error => Unit): Unit = this match { + case Success(_, _) => + () + case Failure(error) => + f(error) + } + + def isSuccess: Boolean = this match { + case Success(_, _) => + true + case Failure(_) => + false + } + + def isFailure: Boolean = !isSuccess + + def toOption: Option[A] = this match { + case Success(_, result) => + Some(result) + case Failure(_) => + None + } + +} + +object Uv { + + final case class Error(errorCode: ErrorCode, message: String) { + + def errorName: String = UvUtils.errorName(errorCode) + + } + + object Error { + + def forCode(errorCode: ErrorCode): Error = + Error(errorCode, UvUtils.errorMessage(errorCode)) + + } + + def succeed[A](a: A): Uv[A] = Success(Vector.empty, a) + + def unit: Uv[Unit] = succeed(()) + + def fail(errorCode: ErrorCode): Uv[Nothing] = Failure( + Error.forCode(errorCode) + ) + + def checkError[A](errorCode: ErrorCode, a: A): Uv[A] = + if errorCode < 0 then Failure(Error.forCode(errorCode)) + else succeed(a) + + def attempt(errorCode: ErrorCode): Uv[ErrorCode] = + if errorCode < 0 then fail(errorCode) + else succeed(errorCode) + + def onFailError(f: Error => Unit): Uv[Unit] = Success(Vector(f), ()) + + def onFail(f: => Unit): Uv[Unit] = onFailError(_ => f) + +} + +object UvUtils { + + /** Allocate the specified type of request on the stack. + * + * *Note:* this is generally only safe to use for synchronous calls. + * + * @param requestType + * The type of request to allocate. + * @return + * The new request. + */ + inline def stackAllocateRequest(requestType: RequestType): Req = + stackalloc[Byte](uv_req_size(requestType)) + + inline def zoneAllocateRequest(requestType: RequestType)(using Zone): Req = + alloc[Byte](uv_req_size(requestType)) + + inline def mallocRequest(requestType: RequestType): Req = + stdlib.malloc(uv_req_size(requestType)).asInstanceOf[Req] + + object FsReq { + + inline def use[A](inline f: Req => A): A = { + val req = stackAllocateRequest(RequestType.FS) + try f(req) + finally uv_fs_req_cleanup(req) + } + + } + + inline def stackAllocateHandle(handleType: HandleType): Handle = + stackalloc[Byte](uv_handle_size(handleType)) + + inline def zoneAllocateHandle(handleType: HandleType)(using Zone): Handle = + alloc[Byte](uv_handle_size(handleType)) + + inline def mallocHandle(handleType: HandleType): Handle = + stdlib.malloc(uv_handle_size(handleType)).asInstanceOf[Handle] + + private val ErrorCodeNameMax: CSize = 80.toUInt + + def errorName(errorCode: CInt): String = { + val cString = stackalloc[Byte](ErrorCodeNameMax) + LibUv.uv_err_name_r(errorCode, cString, ErrorCodeNameMax) + fromCString(cString) + } + + private val ErrorCodeMessageMex: CSize = 200.toUInt + + def errorMessage(errorCode: ErrorCode): String = withZone { + val cString = alloc[Byte](ErrorCodeMessageMex) + LibUv.uv_strerror_r(errorCode, cString, ErrorCodeMessageMex) + fromCString(cString) + } + + def errorNameAndMessage(errorCode: LibUv.ErrorCode): String = withZone { + val cString = alloc[Byte](ErrorCodeMessageMex) + LibUv.uv_err_name_r(errorCode, cString, ErrorCodeMessageMex) + val name = fromCString(cString) + LibUv.uv_strerror_r(errorCode, cString, ErrorCodeMessageMex) + val message = fromCString(cString) + s"UV error $name: $message" + } + + def checkError[A](result: ErrorCode)( + handleError: CInt => A + ): Either[A, CInt] = + if result < 0 then Left(handleError(result)) else Right(result) + + def checkErrorThrow(result: ErrorCode)(f: String => Exception): CInt = + if result < 0 then throw f(errorNameAndMessage(result)) else result + + def checkErrorThrowIO(result: ErrorCode): CInt = + checkErrorThrow(result)(new IOException(_)) + + def checkErrorEofThrow( + result: ErrorCode + )(f: String => Exception): Option[CInt] = + result match { + case code if code == ErrorCodes.EOF => None + case error if error < 0 => throw f(errorNameAndMessage(error)) + case success => Some(success) + } + + def withMutex[A](mutex: Mutex)(f: => A): A = { + uv_mutex_lock(mutex) + try f + finally uv_mutex_unlock(mutex) + } + +} + +extension (uvResult: CInt) { + + def checkError[A](handleError: CInt => A): Either[A, CInt] = + UvUtils.checkError(uvResult)(handleError) + + def checkErrorThrow(f: String => Exception): CInt = + UvUtils.checkErrorThrow(uvResult)(f) + + def checkErrorThrowIO(): CInt = + UvUtils.checkErrorThrowIO(uvResult) + + def checkErrorMessage: Either[String, CInt] = + checkError(UvUtils.errorMessage) + + def checkCustomErrorMessage(f: String => String): Either[String, CInt] = + checkError(code => f(UvUtils.errorMessage(code))) + + def attempt: Uv[ErrorCode] = Uv.attempt(uvResult) + + def ifSuccess[A](a: => A): Uv[A] = + if uvResult < 0 then Uv.fail(uvResult) else Uv.succeed(a) + + def onFail(f: => Unit): CInt = { + if uvResult < 0 then f + uvResult + } + + def onFailMessage(f: String => Unit): CInt = { + if uvResult < 0 then f(UvUtils.errorMessage(uvResult)) + uvResult + } + +} + +final class IOVector(val nativeBuffers: Buffer, numberOfBuffers: Int) { + + inline def apply(index: Int): Buffer = { + nativeBuffers + index + } + + inline def foreachBuffer(f: Buffer => Unit): Unit = + for index <- 0 until numberOfBuffers do { + f(apply(index)) + } + + def foreachBufferMax(max: Int)(f: Buffer => Unit): Unit = { + var remaining = max + boundary { + for index <- 0 until numberOfBuffers do { + val buf = apply(index) + val length = scala.math.min(buf.length, remaining) + f(buf) + remaining -= length + assert(remaining >= 0) + if remaining == 0 then boundary.break() + } + } + } + + inline def nativeNumBuffers: CUnsignedInt = numberOfBuffers.toUInt + +} + +object IOVector { + + inline def stackAllocateForBuffer( + ptr: Ptr[Byte], + size: CUnsignedInt + ): IOVector = { + val buffer = Buffer.stackAllocate(ptr, size) + IOVector(buffer, 1) + } + + inline def stackAllocateForBuffers( + buffers: Seq[(Ptr[Byte], CUnsignedInt)] + ): IOVector = { + val uvBufs = + stackalloc[Byte](buffers.size.toUInt * Buffer.structureSize) + .asInstanceOf[Buffer] + buffers.zipWithIndex.foreach { case ((ptr, size), index) => + (uvBufs + index).init(ptr, size) + } + IOVector(uvBufs, buffers.size) + } + + def zoneAllocate(bufferSizes: Int*)(using Zone): IOVector = { + val uvBufs = + alloc[Byte](bufferSizes.size.toUInt * Buffer.structureSize) + .asInstanceOf[Buffer] + bufferSizes.zipWithIndex.foreach { case (size, index) => + val base = alloc[Byte](size) + (uvBufs + index).init(base, size.toUInt) + } + IOVector(uvBufs, bufferSizes.size) + } + +} + +type Ip4Address = Int + +object Ip4Address { + + inline def apply(a: Int, b: Int, c: Int, d: Int): Ip4Address = { + val aShifted = a << 24 + val bShifted = b << 16 + val cShifted = c << 8 + val dShifted = d + aShifted | bShifted | cShifted | dShifted + } + + val Unspecified: Ip4Address = apply(0, 0, 0, 0) + + val loopback: Ip4Address = apply(127, 0, 0, 1) + +} + +type Port = Int + +opaque type SocketAddressIp4 = Ptr[Byte] + +object SocketAddress4 { + + val size: CSize = helpers.uv_scala_sizeof_sockaddr_in() + + inline def apply(address: Ip4Address, port: Port): SocketAddressIp4 = { + val sockaddr = stackalloc[Byte](size).asInstanceOf[SocketAddressIp4] + helpers.uv_scala_init_sockaddr_in(address, port, sockaddr) + sockaddr + } + + inline def fromBytes(a: Int, b: Int, c: Int, d: Int)( + port: Port + ): SocketAddressIp4 = + apply(Ip4Address(a, b, c, d), port) + + inline def fromString( + ip: String, + port: Port + ): Uv[SocketAddressIp4] = withZone { + val cString = toCString(ip) + val sockaddr = stackalloc[Byte](size).asInstanceOf[SocketAddressIp4] + LibUv + .uv_ip4_addr(cString, port, sockaddr) + .ifSuccess(sockaddr) + } + + inline def unspecifiedAddress(port: Port): SocketAddressIp4 = + apply(Ip4Address.Unspecified, port) + + inline def loopbackAddress(port: Port): SocketAddressIp4 = + apply(Ip4Address.loopback, port) + +} + +opaque type SocketAddressIp6 = Ptr[Byte] + +extension (r: LibUv.ConnectReq) { + + inline def connectReqStreamHandle: LibUv.StreamHandle = + helpers.uv_scala_connect_stream_handle(r) + +} diff --git a/src/main/scala/scalauv/errors.scala b/src/main/scala/scalauv/errors.scala new file mode 100644 index 0000000..2dd6b61 --- /dev/null +++ b/src/main/scala/scalauv/errors.scala @@ -0,0 +1,185 @@ +package scalauv + +import scala.scalanative.unsafe.* +import LibUv.ErrorCode + +object ErrorCodes { + + import errors.* + + val E2BIG: ErrorCode = uv_scala_errorcode_E2BIG() + val EACCES: ErrorCode = uv_scala_errorcode_EACCES() + val EADDRINUSE: ErrorCode = uv_scala_errorcode_EADDRINUSE() + val EADDRNOTAVAIL: ErrorCode = uv_scala_errorcode_EADDRNOTAVAIL() + val EAFNOSUPPORT: ErrorCode = uv_scala_errorcode_EAFNOSUPPORT() + val EAGAIN: ErrorCode = uv_scala_errorcode_EAGAIN() + val EAI_ADDRFAMILY: ErrorCode = uv_scala_errorcode_EAI_ADDRFAMILY() + val EAI_AGAIN: ErrorCode = uv_scala_errorcode_EAI_AGAIN() + val EAI_BADFLAGS: ErrorCode = uv_scala_errorcode_EAI_BADFLAGS() + val EAI_BADHINTS: ErrorCode = uv_scala_errorcode_EAI_BADHINTS() + val EAI_CANCELED: ErrorCode = uv_scala_errorcode_EAI_CANCELED() + val EAI_FAIL: ErrorCode = uv_scala_errorcode_EAI_FAIL() + val EAI_FAMILY: ErrorCode = uv_scala_errorcode_EAI_FAMILY() + val EAI_MEMORY: ErrorCode = uv_scala_errorcode_EAI_MEMORY() + val EAI_NODATA: ErrorCode = uv_scala_errorcode_EAI_NODATA() + val EAI_NONAME: ErrorCode = uv_scala_errorcode_EAI_NONAME() + val EAI_OVERFLOW: ErrorCode = uv_scala_errorcode_EAI_OVERFLOW() + val EAI_PROTOCOL: ErrorCode = uv_scala_errorcode_EAI_PROTOCOL() + val EAI_SERVICE: ErrorCode = uv_scala_errorcode_EAI_SERVICE() + val EAI_SOCKTYPE: ErrorCode = uv_scala_errorcode_EAI_SOCKTYPE() + val EALREADY: ErrorCode = uv_scala_errorcode_EALREADY() + val EBADF: ErrorCode = uv_scala_errorcode_EBADF() + val EBUSY: ErrorCode = uv_scala_errorcode_EBUSY() + val ECANCELED: ErrorCode = uv_scala_errorcode_ECANCELED() + val ECHARSET: ErrorCode = uv_scala_errorcode_ECHARSET() + val ECONNABORTED: ErrorCode = uv_scala_errorcode_ECONNABORTED() + val ECONNREFUSED: ErrorCode = uv_scala_errorcode_ECONNREFUSED() + val ECONNRESET: ErrorCode = uv_scala_errorcode_ECONNRESET() + val EDESTADDRREQ: ErrorCode = uv_scala_errorcode_EDESTADDRREQ() + val EEXIST: ErrorCode = uv_scala_errorcode_EEXIST() + val EFAULT: ErrorCode = uv_scala_errorcode_EFAULT() + val EFBIG: ErrorCode = uv_scala_errorcode_EFBIG() + val EHOSTUNREACH: ErrorCode = uv_scala_errorcode_EHOSTUNREACH() + val EINTR: ErrorCode = uv_scala_errorcode_EINTR() + val EINVAL: ErrorCode = uv_scala_errorcode_EINVAL() + val EIO: ErrorCode = uv_scala_errorcode_EIO() + val EISCONN: ErrorCode = uv_scala_errorcode_EISCONN() + val EISDIR: ErrorCode = uv_scala_errorcode_EISDIR() + val ELOOP: ErrorCode = uv_scala_errorcode_ELOOP() + val EMFILE: ErrorCode = uv_scala_errorcode_EMFILE() + val EMSGSIZE: ErrorCode = uv_scala_errorcode_EMSGSIZE() + val ENAMETOOLONG: ErrorCode = uv_scala_errorcode_ENAMETOOLONG() + val ENETDOWN: ErrorCode = uv_scala_errorcode_ENETDOWN() + val ENETUNREACH: ErrorCode = uv_scala_errorcode_ENETUNREACH() + val ENFILE: ErrorCode = uv_scala_errorcode_ENFILE() + val ENOBUFS: ErrorCode = uv_scala_errorcode_ENOBUFS() + val ENODEV: ErrorCode = uv_scala_errorcode_ENODEV() + val ENOENT: ErrorCode = uv_scala_errorcode_ENOENT() + val ENOMEM: ErrorCode = uv_scala_errorcode_ENOMEM() + val ENONET: ErrorCode = uv_scala_errorcode_ENONET() + val ENOPROTOOPT: ErrorCode = uv_scala_errorcode_ENOPROTOOPT() + val ENOSPC: ErrorCode = uv_scala_errorcode_ENOSPC() + val ENOSYS: ErrorCode = uv_scala_errorcode_ENOSYS() + val ENOTCONN: ErrorCode = uv_scala_errorcode_ENOTCONN() + val ENOTDIR: ErrorCode = uv_scala_errorcode_ENOTDIR() + val ENOTEMPTY: ErrorCode = uv_scala_errorcode_ENOTEMPTY() + val ENOTSOCK: ErrorCode = uv_scala_errorcode_ENOTSOCK() + val ENOTSUP: ErrorCode = uv_scala_errorcode_ENOTSUP() + val EOVERFLOW: ErrorCode = uv_scala_errorcode_EOVERFLOW() + val EPERM: ErrorCode = uv_scala_errorcode_EPERM() + val EPIPE: ErrorCode = uv_scala_errorcode_EPIPE() + val EPROTO: ErrorCode = uv_scala_errorcode_EPROTO() + val EPROTONOSUPPORT: ErrorCode = uv_scala_errorcode_EPROTONOSUPPORT() + val EPROTOTYPE: ErrorCode = uv_scala_errorcode_EPROTOTYPE() + val ERANGE: ErrorCode = uv_scala_errorcode_ERANGE() + val EROFS: ErrorCode = uv_scala_errorcode_EROFS() + val ESHUTDOWN: ErrorCode = uv_scala_errorcode_ESHUTDOWN() + val ESPIPE: ErrorCode = uv_scala_errorcode_ESPIPE() + val ESRCH: ErrorCode = uv_scala_errorcode_ESRCH() + val ETIMEDOUT: ErrorCode = uv_scala_errorcode_ETIMEDOUT() + val ETXTBSY: ErrorCode = uv_scala_errorcode_ETXTBSY() + val EXDEV: ErrorCode = uv_scala_errorcode_EXDEV() + val UNKNOWN: ErrorCode = uv_scala_errorcode_UNKNOWN() + val EOF: ErrorCode = uv_scala_errorcode_EOF() + val ENXIO: ErrorCode = uv_scala_errorcode_ENXIO() + val EMLINK: ErrorCode = uv_scala_errorcode_EMLINK() + val EHOSTDOWN: ErrorCode = uv_scala_errorcode_EHOSTDOWN() + val EREMOTEIO: ErrorCode = uv_scala_errorcode_EREMOTEIO() + val ENOTTY: ErrorCode = uv_scala_errorcode_ENOTTY() + val EFTYPE: ErrorCode = uv_scala_errorcode_EFTYPE() + val EILSEQ: ErrorCode = uv_scala_errorcode_EILSEQ() + val ESOCKTNOSUPPORT: ErrorCode = uv_scala_errorcode_ESOCKTNOSUPPORT() + val ENODATA: ErrorCode = uv_scala_errorcode_ENODATA() + val EUNATCH: ErrorCode = uv_scala_errorcode_EUNATCH() + +} + +@extern +private[scalauv] object errors { + + def uv_scala_errorcode_E2BIG(): ErrorCode = extern + def uv_scala_errorcode_EACCES(): ErrorCode = extern + def uv_scala_errorcode_EADDRINUSE(): ErrorCode = extern + def uv_scala_errorcode_EADDRNOTAVAIL(): ErrorCode = extern + def uv_scala_errorcode_EAFNOSUPPORT(): ErrorCode = extern + def uv_scala_errorcode_EAGAIN(): ErrorCode = extern + def uv_scala_errorcode_EAI_ADDRFAMILY(): ErrorCode = extern + def uv_scala_errorcode_EAI_AGAIN(): ErrorCode = extern + def uv_scala_errorcode_EAI_BADFLAGS(): ErrorCode = extern + def uv_scala_errorcode_EAI_BADHINTS(): ErrorCode = extern + def uv_scala_errorcode_EAI_CANCELED(): ErrorCode = extern + def uv_scala_errorcode_EAI_FAIL(): ErrorCode = extern + def uv_scala_errorcode_EAI_FAMILY(): ErrorCode = extern + def uv_scala_errorcode_EAI_MEMORY(): ErrorCode = extern + def uv_scala_errorcode_EAI_NODATA(): ErrorCode = extern + def uv_scala_errorcode_EAI_NONAME(): ErrorCode = extern + def uv_scala_errorcode_EAI_OVERFLOW(): ErrorCode = extern + def uv_scala_errorcode_EAI_PROTOCOL(): ErrorCode = extern + def uv_scala_errorcode_EAI_SERVICE(): ErrorCode = extern + def uv_scala_errorcode_EAI_SOCKTYPE(): ErrorCode = extern + def uv_scala_errorcode_EALREADY(): ErrorCode = extern + def uv_scala_errorcode_EBADF(): ErrorCode = extern + def uv_scala_errorcode_EBUSY(): ErrorCode = extern + def uv_scala_errorcode_ECANCELED(): ErrorCode = extern + def uv_scala_errorcode_ECHARSET(): ErrorCode = extern + def uv_scala_errorcode_ECONNABORTED(): ErrorCode = extern + def uv_scala_errorcode_ECONNREFUSED(): ErrorCode = extern + def uv_scala_errorcode_ECONNRESET(): ErrorCode = extern + def uv_scala_errorcode_EDESTADDRREQ(): ErrorCode = extern + def uv_scala_errorcode_EEXIST(): ErrorCode = extern + def uv_scala_errorcode_EFAULT(): ErrorCode = extern + def uv_scala_errorcode_EFBIG(): ErrorCode = extern + def uv_scala_errorcode_EHOSTUNREACH(): ErrorCode = extern + def uv_scala_errorcode_EINTR(): ErrorCode = extern + def uv_scala_errorcode_EINVAL(): ErrorCode = extern + def uv_scala_errorcode_EIO(): ErrorCode = extern + def uv_scala_errorcode_EISCONN(): ErrorCode = extern + def uv_scala_errorcode_EISDIR(): ErrorCode = extern + def uv_scala_errorcode_ELOOP(): ErrorCode = extern + def uv_scala_errorcode_EMFILE(): ErrorCode = extern + def uv_scala_errorcode_EMSGSIZE(): ErrorCode = extern + def uv_scala_errorcode_ENAMETOOLONG(): ErrorCode = extern + def uv_scala_errorcode_ENETDOWN(): ErrorCode = extern + def uv_scala_errorcode_ENETUNREACH(): ErrorCode = extern + def uv_scala_errorcode_ENFILE(): ErrorCode = extern + def uv_scala_errorcode_ENOBUFS(): ErrorCode = extern + def uv_scala_errorcode_ENODEV(): ErrorCode = extern + def uv_scala_errorcode_ENOENT(): ErrorCode = extern + def uv_scala_errorcode_ENOMEM(): ErrorCode = extern + def uv_scala_errorcode_ENONET(): ErrorCode = extern + def uv_scala_errorcode_ENOPROTOOPT(): ErrorCode = extern + def uv_scala_errorcode_ENOSPC(): ErrorCode = extern + def uv_scala_errorcode_ENOSYS(): ErrorCode = extern + def uv_scala_errorcode_ENOTCONN(): ErrorCode = extern + def uv_scala_errorcode_ENOTDIR(): ErrorCode = extern + def uv_scala_errorcode_ENOTEMPTY(): ErrorCode = extern + def uv_scala_errorcode_ENOTSOCK(): ErrorCode = extern + def uv_scala_errorcode_ENOTSUP(): ErrorCode = extern + def uv_scala_errorcode_EOVERFLOW(): ErrorCode = extern + def uv_scala_errorcode_EPERM(): ErrorCode = extern + def uv_scala_errorcode_EPIPE(): ErrorCode = extern + def uv_scala_errorcode_EPROTO(): ErrorCode = extern + def uv_scala_errorcode_EPROTONOSUPPORT(): ErrorCode = extern + def uv_scala_errorcode_EPROTOTYPE(): ErrorCode = extern + def uv_scala_errorcode_ERANGE(): ErrorCode = extern + def uv_scala_errorcode_EROFS(): ErrorCode = extern + def uv_scala_errorcode_ESHUTDOWN(): ErrorCode = extern + def uv_scala_errorcode_ESPIPE(): ErrorCode = extern + def uv_scala_errorcode_ESRCH(): ErrorCode = extern + def uv_scala_errorcode_ETIMEDOUT(): ErrorCode = extern + def uv_scala_errorcode_ETXTBSY(): ErrorCode = extern + def uv_scala_errorcode_EXDEV(): ErrorCode = extern + def uv_scala_errorcode_UNKNOWN(): ErrorCode = extern + def uv_scala_errorcode_EOF(): ErrorCode = extern + def uv_scala_errorcode_ENXIO(): ErrorCode = extern + def uv_scala_errorcode_EMLINK(): ErrorCode = extern + def uv_scala_errorcode_EHOSTDOWN(): ErrorCode = extern + def uv_scala_errorcode_EREMOTEIO(): ErrorCode = extern + def uv_scala_errorcode_ENOTTY(): ErrorCode = extern + def uv_scala_errorcode_EFTYPE(): ErrorCode = extern + def uv_scala_errorcode_EILSEQ(): ErrorCode = extern + def uv_scala_errorcode_ESOCKTNOSUPPORT(): ErrorCode = extern + def uv_scala_errorcode_ENODATA(): ErrorCode = extern + def uv_scala_errorcode_EUNATCH(): ErrorCode = extern + +} diff --git a/src/test/resources/test.txt b/src/test/resources/test.txt new file mode 100644 index 0000000..5dd01c1 --- /dev/null +++ b/src/test/resources/test.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/src/test/scala/scalauv/FileSpec.scala b/src/test/scala/scalauv/FileSpec.scala new file mode 100644 index 0000000..04cd54d --- /dev/null +++ b/src/test/scala/scalauv/FileSpec.scala @@ -0,0 +1,125 @@ +package scalauv + +import LibUv.* +import scalanative.unsafe.* +import scalanative.unsigned.* +import java.nio.charset.StandardCharsets + +import org.junit.Test +import org.junit.Assert.* +import scala.scalanative.libc.string +import java.nio.file.Files +import java.nio.file.Path + +final class FileSpec { + + @Test + def readFile(): Unit = { + val expected = "Hello, world!" + val filename = c"src/test/resources/test.txt" + + withZone { + val loop = uv_default_loop() + + val BufSize: CSize = 1024.toUInt + + val buffer = alloc[Byte](BufSize) + + val readFileHandle = UvUtils.FsReq + .use { openReq => + uv_fs_open( + loop, + openReq, + filename, + FileOpenFlags.O_RDONLY, + 0, + null + ) + } + .checkErrorThrowIO() + + val bytesRead = UvUtils.FsReq + .use { readReq => + val iov = IOVector.stackAllocateForBuffer(buffer, BufSize.toUInt) + uv_fs_read( + loop, + readReq, + readFileHandle, + iov.nativeBuffers, + iov.nativeNumBuffers, + -1, + null + ) + } + .checkErrorThrowIO() + + val readText = fromCString(buffer, StandardCharsets.UTF_8) + + UvUtils.FsReq + .use { closeReq => + uv_fs_close(loop, closeReq, readFileHandle, null) + } + .checkErrorThrowIO() + + assertEquals(13, bytesRead) + assertEquals(expected, readText) + } + + } + + @Test + def writeNewFile(): Unit = { + val text = "my country is the world, and my religion is to do good" + val filename = "src/test/resources/write-test.txt" + + withZone { + val loop = uv_default_loop() + + val cText = toCString(text, StandardCharsets.UTF_8) + val cFilename = toCString(filename, StandardCharsets.UTF_8) + + val fileHandle = UvUtils.FsReq + .use { openReq => + uv_fs_open( + loop, + openReq, + cFilename, + FileOpenFlags.O_WRONLY | FileOpenFlags.O_CREAT | FileOpenFlags.O_TRUNC, + CreateMode.S_IRUSR | CreateMode.S_IWUSR, + null + ) + } + .checkErrorThrowIO() + + val bytesWritten = UvUtils.FsReq + .use { writeReq => + val iov = + IOVector.stackAllocateForBuffer(cText, string.strlen(cText).toUInt) + uv_fs_write( + loop, + writeReq, + fileHandle, + iov.nativeBuffers, + iov.nativeNumBuffers, + -1, + null + ) + } + .checkErrorThrowIO() + + UvUtils.FsReq + .use { closeReq => + uv_fs_close(loop, closeReq, fileHandle, null) + } + .checkErrorThrowIO() + + val path = Path.of(filename) + val actualText = Files.readString(path) + Files.delete(path) + + assertEquals(text.length(), bytesWritten) + assertEquals(text, actualText) + } + } + +} diff --git a/src/test/scala/scalauv/TcpSpec.scala b/src/test/scala/scalauv/TcpSpec.scala new file mode 100644 index 0000000..76646cf --- /dev/null +++ b/src/test/scala/scalauv/TcpSpec.scala @@ -0,0 +1,163 @@ +package scalauv +import org.junit.Test +import org.junit.Assert.* +import LibUv.* +import scalanative.unsafe.* +import scalanative.unsigned.* +import scala.scalanative.libc.stdlib + +final class TcpSpec { + + import TcpSpec.* + + def recordReceived(s: String): Unit = { + receivedData = receivedData :+ s + } + + def setFailed(msg: String): Unit = { + failed = Some(msg) + } + + def onClose: CloseCallback = CFuncPtr1.fromScalaFunction(stdlib.free) + + def onRead: StreamReadCallback = CFuncPtr3.fromScalaFunction { + (handle: StreamHandle, numRead: CSSize, buf: Buffer) => + numRead match { + case ErrorCodes.EOF => + uv_close(handle, onClose) + case code if code < 0 => + uv_close(handle, onClose) + setFailed(UvUtils.errorMessage(code.toInt)) + case _ => + val (text, done) = + buf.asUtf8String(numRead.toInt).span(_ != DoneMarker) + recordReceived(text) + if done.nonEmpty then { + val listenHandle = uv_handle_get_data(handle) + uv_close(listenHandle, null) + } + } + stdlib.free(buf.base) + } + + @Test + def foo(): Unit = { + println("XXXXXX") + assertEquals(1, 1) + } + + @Test + def listen(): Unit = { + + var runResult = 0 + + withZone { + + val loop = stackalloc[Byte](uv_loop_size()).asInstanceOf[Loop] + uv_loop_init(loop).checkErrorThrowIO() + + def allocBuffer: AllocCallback = CFuncPtr3.fromScalaFunction { + (handle: StreamHandle, suggestedSize: CSize, buf: Buffer) => + buf.mallocInit(suggestedSize) + } + + def onNewConnection: ConnectionCallback = CFuncPtr2.fromScalaFunction { + (handle: StreamHandle, status: ErrorCode) => + val loop = uv_handle_get_loop(handle) + val attempt = for { + _ <- status.attempt.mapErrorMessage(s => + s"New connection error: $s" + ) + clientTcpHandle = UvUtils.mallocHandle(HandleType.UV_TCP) + _ <- Uv.onFail(stdlib.free(clientTcpHandle)) + _ <- { + println("New connection") + uv_tcp_init(loop, clientTcpHandle).attempt + .mapErrorMessage(s => s"TCP handle init failed: $s") + } + _ <- Uv.succeed { + uv_handle_set_data(clientTcpHandle, handle) + } + _ <- uv_accept(handle, clientTcpHandle).attempt + .mapErrorMessage(s => s"Accept failed: $s") + _ <- Uv.onFail(uv_close(clientTcpHandle, onClose)) + _ <- uv_read_start(clientTcpHandle, allocBuffer, onRead).attempt + .mapErrorMessage(s => s"Read start failed: $s") + } yield () + attempt.foreachFailure(e => setFailed(e.message)) + } + + val port = 10000 + val serverTcpHandle = UvUtils.stackAllocateHandle(HandleType.UV_TCP) + uv_tcp_init(loop, serverTcpHandle).checkErrorThrowIO() + val serverSocketAddress = SocketAddress4.unspecifiedAddress(port) + uv_tcp_bind(serverTcpHandle, serverSocketAddress, 0.toUInt) + .checkErrorThrowIO() + uv_listen(serverTcpHandle, 128, onNewConnection).checkErrorThrowIO() + + def onWrite: StreamWriteCallback = CFuncPtr2.fromScalaFunction { + (req: WriteReq, status: ErrorCode) => + status.onFailMessage(setFailed) + val buf = Buffer.unsafeFromNative(uv_req_get_data(req)) + stdlib.free(buf.base) + buf.free() + stdlib.free(req) + } + + def onConnect: ConnectCallback = CFuncPtr2.fromScalaFunction { + (req: ConnectReq, status) => + status.onFailMessage(setFailed) + val stream = req.connectReqStreamHandle + def doWrite(text: String) = { + val writeReq = UvUtils.mallocRequest(RequestType.WRITE) + val cText = mallocCString(text) + val buf = Buffer.malloc(cText, text.length.toULong) + uv_req_set_data(writeReq, buf.toNative) + uv_write(writeReq, stream, buf, 1.toUInt, onWrite).onFailMessage { + s => + stdlib.free(cText) + buf.free() + stdlib.free(writeReq) + setFailed(s) + } + } + doWrite(text) + doWrite(text) + doWrite(DoneMarker.toString) + uv_close(stream, null) + () + } + + val clientTcpHandle = UvUtils.stackAllocateHandle(HandleType.UV_TCP) + uv_tcp_init(loop, clientTcpHandle).checkErrorThrowIO() + val clientSocketAddress = SocketAddress4.loopbackAddress(port) + val connectReq = UvUtils.stackAllocateRequest(RequestType.CONNECT) + uv_tcp_connect( + connectReq, + clientTcpHandle, + clientSocketAddress, + onConnect + ).checkErrorThrowIO() + + runResult = uv_run(loop, RunMode.DEFAULT).checkErrorThrowIO() + + uv_loop_close(loop).checkErrorThrowIO() + } + + assertEquals("runResult", 0, runResult) + assertEquals("failed", None, failed) + assertEquals("recievedData", text + text, receivedData.mkString) + + } + +} + +object TcpSpec { + private val DoneMarker = '!' + + private val text = "my country is the world, and my religion is to do good" + + private var receivedData = Vector.empty[String] + private var failed = Option.empty[String] + +} diff --git a/src/test/scala/scalauv/UvSpec.scala b/src/test/scala/scalauv/UvSpec.scala new file mode 100644 index 0000000..bf0f08b --- /dev/null +++ b/src/test/scala/scalauv/UvSpec.scala @@ -0,0 +1,50 @@ +package scalauv + +import org.junit.Test +import org.junit.Assert.* + +final class UvSpec { + + @Test + def doesNotRunCallbacksOnSuccess(): Unit = { + + var results = Vector.empty[String] + + def record(s: String): Unit = { + results = results :+ s + } + + val run = for { + a <- 1.attempt.onFail(record("a")) + b <- 2.attempt.onFail(record("b")) + c <- 3.attempt.onFail(record("c")) + } yield (a, b, c) + + assertTrue("succeeded", run.isSuccess) + assertEquals("value", Some((1, 2, 3)), run.toOption) + assertEquals("on fail callbacks not executed", Vector.empty, results) + } + + @Test + def runsCallbacksOnFailure(): Unit = { + + var results = Vector.empty[String] + + def record(s: String): Unit = { + results = results :+ s + } + + val run = for { + a <- 1.attempt.onFail(record("a")) + b <- 2.attempt.onFail(record("b")) + c <- (-666).attempt.onFail(record("c")) + d <- 4.attempt.onFail(record("d")) + e <- (-1000).attempt.onFail(record("e")) + } yield (a, b, c, d, e) + + assertTrue("failed", run.isFailure) + assertEquals("on fail callbacks executed", Vector("c", "b", "a"), results) + + } + +}