diff --git a/build.sbt b/build.sbt index 46dcd73..26b6a36 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,7 @@ organization := "io.github.quelgar" name := "scala-uv" -version := "0.0.2" +version := "0.0.3-SNAPSHOT" ThisBuild / versionScheme := Some("early-semver") diff --git a/src/main/resources/scala-native/helpers.c b/src/main/resources/scala-native/helpers.c index 2bccf2c..74e321a 100644 --- a/src/main/resources/scala-native/helpers.c +++ b/src/main/resources/scala-native/helpers.c @@ -7,6 +7,11 @@ #include #endif +uv_loop_t *scala_uv_fs_req_get_loop(const uv_fs_t *req) +{ + return req->loop; +} + void scala_uv_buf_init(char *base, unsigned int len, uv_buf_t *buffer) { uv_buf_t buf = uv_buf_init(base, len); diff --git a/src/main/scala/scalauv/Buffer.scala b/src/main/scala/scalauv/Buffer.scala index 232e758..c799b0b 100644 --- a/src/main/scala/scalauv/Buffer.scala +++ b/src/main/scala/scalauv/Buffer.scala @@ -25,23 +25,26 @@ object Buffer { given Tag[Buffer] = Tag.Ptr(Tag.Byte) + // to avoid compiler bug where same name is used in inline methods in different files + private val h = scalauv.helpers + extension (buffer: Buffer) { /** Pointer to the actual content. */ - def base: Ptr[Byte] = helpers.scala_uv_buf_base(buffer) + inline def base: Ptr[Byte] = h.scala_uv_buf_base(buffer) - def base_=(ptr: Ptr[Byte]): Unit = - helpers.scala_uv_buf_base_set(buffer, ptr) + inline def base_=(ptr: Ptr[Byte]): Unit = + h.scala_uv_buf_base_set(buffer, ptr) /** The number of bytes (starting from `base`) that should be considered * part of this buffer. When reading, you probably want to set this to the * number of bytes read. */ - def length: Int = helpers.scala_uv_buf_len(buffer).toInt + inline def length: Int = h.scala_uv_buf_len(buffer).toInt - def length_=(len: Int): Unit = - helpers.scala_uv_buf_len_set(buffer, len.toUInt) + inline def length_=(len: Int): Unit = + h.scala_uv_buf_len_set(buffer, len.toUInt) /** Gets the byte at the specified index. * @@ -93,23 +96,47 @@ object Buffer { * `length`. */ inline def init(base: Ptr[Byte], length: CSize): Unit = - helpers.scala_uv_buf_init(base, length.toUInt, buffer) + h.scala_uv_buf_init(base, length.toUInt, buffer) /** Allocates a new native byte array of `size` bytes, and uses it to * initialize this buffer structure. `base` is set to the newly allocated * array, and `length` is set to `size`. */ inline def mallocInit(size: CSize): Unit = - helpers.scala_uv_buf_init(stdlib.malloc(size), size.toUInt, buffer) + h.scala_uv_buf_init(stdlib.malloc(size), size.toUInt, buffer) /** Frees this boffer structure. **Note:** does not free the `base` pointer. */ inline def free(): Unit = stdlib.free(buffer) + + /** Perform an operation using this buffer with the length temporarily + * changed. This can be useful after a read that only partially filled the + * buffer. + */ + inline def withLength[A](tempLength: Int)(f: Buffer => A): A = { + val oldLength = length + length = tempLength + val result = f(buffer) + length = oldLength + result + } + + inline def withOffset[A](offset: Int)(f: Buffer => A): A = { + val origBase = base + val origLength = length + base += offset + length -= offset + val result = f(buffer) + length = origLength + base = origBase + result + } + } /** The size of the `uv_buf_t` structure. Useful for manual allocation. */ - val structureSize: CSize = helpers.scala_uv_buf_struct_size() + val structureSize: CSize = h.scala_uv_buf_struct_size() /** Cast a native pointer to a buffer structure pointer. */ @@ -166,7 +193,7 @@ object Buffer { */ def malloc(base: Ptr[Byte], size: CSize): Buffer = { val uvBuf = stdlib.malloc(structureSize.toULong) - helpers.scala_uv_buf_init(base, size.toUInt, uvBuf) + h.scala_uv_buf_init(base, size.toUInt, uvBuf) uvBuf } @@ -176,7 +203,7 @@ object Buffer { */ def malloc(array: Array[Byte], index: Int = 0): Buffer = { val uvBuf = stdlib.malloc(structureSize.toULong) - helpers.scala_uv_buf_init( + h.scala_uv_buf_init( array.at(index), (array.length - index).toUInt, uvBuf diff --git a/src/main/scala/scalauv/Handle.scala b/src/main/scala/scalauv/Handle.scala index f7f3f5f..3101682 100644 --- a/src/main/scala/scalauv/Handle.scala +++ b/src/main/scala/scalauv/Handle.scala @@ -21,6 +21,14 @@ object Handle { extension (h: Handle) { + inline def loop: Loop = LibUv.uv_handle_get_loop(h) + + inline def handleType: HandleType = LibUv.uv_handle_get_type(h) + + inline def data: Ptr[Byte] = LibUv.uv_handle_get_data(h) + + inline def data_=(data: Ptr[Byte]): Unit = LibUv.uv_handle_set_data(h, data) + /** Extract the underlying native pointer for this handle. */ inline def toPtr: Ptr[Byte] = h @@ -230,6 +238,12 @@ object StreamHandle { given Tag[StreamHandle] = Tag.Ptr(Tag.Byte) + extension (h: StreamHandle) { + + inline def writeQueueSize: CSize = LibUv.uv_stream_get_write_queue_size(h) + + } + } /** The `uv_tcp_t*` type. diff --git a/src/main/scala/scalauv/LibUv.scala b/src/main/scala/scalauv/LibUv.scala index df0b1ae..7ee31d5 100644 --- a/src/main/scala/scalauv/LibUv.scala +++ b/src/main/scala/scalauv/LibUv.scala @@ -1599,82 +1599,13 @@ object LibUv { // ========================================================= // File system operations - /** Pointer to time strucutre. - * - * @see - * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_timespec_t LibUv docs]] - * @group fs - */ - type TimeSpec = Ptr[CStruct2[CLong, CLong]] - - /** Pointer to file `stat` strcture. - * - * @see - * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_stat_t LibUv docs]] - * @group fs - */ - type Stat = Ptr[CStruct16[ - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - TimeSpec, - TimeSpec, - TimeSpec, - TimeSpec - ]] - - /** Pointer to `struct statfs`. - * - * @see - * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_statfs_t LibUv docs]] - * @group fs - */ - type StatFs = Ptr[CStruct11[ - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong, - CUnsignedLongLong - ]] - - /** Pointer to directory entry structure. - * - * @see - * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_dirent_t LibUv docs]] - * @group fs - */ - type DirEnt = Ptr[CStruct2[CString, DirEntType]] - - /** Pointer to directory structure. - * - * @see - * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_dir_t LibUv docs]] - * @group fs - */ - type Dir = Ptr[CStruct2[DirEnt, CSize]] - /** Callback for file system requests. * * @see * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_cb LibUv docs]] * @group fs */ - type FsCallback = CFuncPtr1[Req, Unit] + type FsCallback = CFuncPtr1[FileReq, Unit] /** Cleans up a file system request. * diff --git a/src/main/scala/scalauv/Req.scala b/src/main/scala/scalauv/Req.scala index dc44ef0..a6d48ff 100644 --- a/src/main/scala/scalauv/Req.scala +++ b/src/main/scala/scalauv/Req.scala @@ -45,6 +45,14 @@ object Req { extension (r: Req) { + inline def data: Ptr[Byte] = LibUv.uv_req_get_data(r) + + inline def data_=(data: Ptr[Byte]): Unit = LibUv.uv_req_set_data(r, data) + + inline def reqType: RequestType = LibUv.uv_req_get_type(r) + + inline def cancel(): ErrorCode = LibUv.uv_cancel(r) + /** Casts this request to a native pointer. */ inline def toPtr: Ptr[Byte] = r @@ -62,6 +70,13 @@ opaque type RequestType = CInt /** Constants for all the request types supported by libuv. */ object RequestType { + + extension (rt: RequestType) { + + inline def size: CSize = LibUv.uv_req_size(rt) + + } + val UNKNOWN_REQ: RequestType = 0 val REQ: RequestType = 1 val CONNECT: RequestType = 2 @@ -114,6 +129,13 @@ object ShutdownReq { inline def malloc(): ShutdownReq = Req.malloc(RequestType.SHUTDOWN) + extension (r: ShutdownReq) { + + inline def shutdownReqStreamHandle: StreamHandle = + helpers.scala_uv_shutdown_stream_handle(r) + + } + } opaque type WriteReq <: Req = Ptr[Byte] @@ -131,6 +153,16 @@ object WriteReq { inline def malloc(): WriteReq = Req.malloc(RequestType.WRITE) + extension (r: WriteReq) { + + inline def writeReqStreamHandle: StreamHandle = + helpers.scala_uv_write_stream_handle(r) + + inline def writeReqSendStreamHandle: StreamHandle = + helpers.scala_uv_send_stream_handle(r) + + } + } opaque type UdpSendReq <: Req = Ptr[Byte] @@ -154,6 +186,28 @@ opaque type FileReq <: Req = Ptr[Byte] object FileReq { + given Tag[FileReq] = Tag.Ptr(Tag.Byte) + + extension (req: FileReq) { + + inline def loop: Loop = helpers.scala_uv_fs_req_get_loop(req) + + inline def fsType: FsType = LibUv.uv_fs_get_type(req) + + inline def result: CSSize = LibUv.uv_fs_get_result(req) + + inline def systemError: CInt = LibUv.uv_fs_get_system_error(req) + + inline def ptr: Ptr[Byte] = LibUv.uv_fs_get_ptr(req) + + inline def path: CString = LibUv.uv_fs_get_path(req) + + inline def statBuf: Stat = LibUv.uv_fs_get_statbuf(req) + + inline def cleanup(): Unit = LibUv.uv_fs_req_cleanup(req) + + } + inline def stackAllocate(): FileReq = Req.stackAllocate(RequestType.FS) diff --git a/src/main/scala/scalauv/Stat.scala b/src/main/scala/scalauv/Stat.scala new file mode 100644 index 0000000..0fca639 --- /dev/null +++ b/src/main/scala/scalauv/Stat.scala @@ -0,0 +1,159 @@ +package scalauv + +import scala.scalanative.unsafe.* +import scala.scalanative.unsigned.* +import java.time.Instant + +/** Time structure. + * + * @see + * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_timespec_t LibUv docs]] + */ +trait TimeSpec { + def seconds: CLong + def nanoSeconds: CLong + + final inline def toInstant: Instant = + Instant.ofEpochSecond(seconds, nanoSeconds) +} + +/** Pointer to file `stat` strcture. + * + * @see + * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_stat_t LibUv docs]] + * @group fs + */ +opaque type Stat = Ptr[CStruct20[ + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CLongLong, + CLongLong, + CLongLong, + CLongLong, + CLongLong, + CLongLong, + CLongLong, + CLongLong +]] + +object Stat { + + extension (s: Stat) { + + inline def device: CUnsignedLongLong = s._1 + inline def mode: CUnsignedLongLong = s._2 + inline def nlink: CUnsignedLongLong = s._3 + inline def uid: CUnsignedLongLong = s._4 + inline def gid: CUnsignedLongLong = s._5 + inline def rdev: CUnsignedLongLong = s._6 + inline def ino: CUnsignedLongLong = s._7 + inline def size: CUnsignedLongLong = s._8 + inline def blksize: CUnsignedLongLong = s._9 + inline def blocks: CUnsignedLongLong = s._10 + inline def flags: CUnsignedLongLong = s._11 + inline def gen: CUnsignedLongLong = s._12 + + inline def accessTimeSpec: TimeSpec = new TimeSpec { + override def seconds: CLong = s._13 + override def nanoSeconds: CLong = s._14 + } + inline def accessTime: Instant = Instant.ofEpochSecond(s._13, s._14) + + inline def modificationTimeSpec: TimeSpec = new TimeSpec { + override def seconds: CLong = s._15 + override def nanoSeconds: CLong = s._16 + } + inline def modificationTime: Instant = Instant.ofEpochSecond(s._15, s._16) + + inline def inodeChangeTimeSpec: TimeSpec = new TimeSpec { + override def seconds: CLong = s._17 + override def nanoSeconds: CLong = s._18 + } + inline def inodeChangeTime: Instant = Instant.ofEpochSecond(s._17, s._18) + + inline def birthTimeSpec: TimeSpec = new TimeSpec { + override def seconds: CLong = s._19 + override def nanoSeconds: CLong = s._20 + } + inline def birthTime: Instant = Instant.ofEpochSecond(s._19, s._20) + + } +} + +/** Pointer to `struct statfs`. + * + * @see + * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_statfs_t LibUv docs]] + * @group fs + */ +type StatFs = Ptr[CStruct11[ + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong, + CUnsignedLongLong +]] + +/** Pointer to directory entry structure. + * + * @see + * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_dirent_t LibUv docs]] + * @group fs + */ +opaque type DirEnt = Ptr[CStruct2[CString, DirEntType]] + +object DirEnt { + + extension (d: DirEnt) { + + inline def name: CString = d._1 + inline def entryType: DirEntType = d._2 + + } +} + +/** Pointer to directory structure. + * + * @see + * [[https://docs.libuv.org/en/v1.x/fs.html#c.uv_dir_t LibUv docs]] + * @group fs + */ +opaque type Dir = Ptr[CStruct2[DirEnt, CSize]] + +object Dir { + + extension (d: Dir) { + + inline def entries: DirEnt = d._1 + inline def entries_=(entries: DirEnt): Unit = d._1 = entries + + inline def numberOfEntries: CSize = d._2 + inline def numberOfEntries_=(n: CSize): Unit = d._2 = n + + } + + def zoneAllocate(size: Int)(using Zone): Dir = { + val entries = alloc[CStruct2[CString, DirEntType]](size.toUInt) + val dir = alloc[CStruct2[DirEnt, CSize]](1) + dir.entries = entries + dir.numberOfEntries = size.toUInt + dir + } + +} diff --git a/src/main/scala/scalauv/UvConstants.scala b/src/main/scala/scalauv/UvConstants.scala index d36146e..3ea01df 100644 --- a/src/main/scala/scalauv/UvConstants.scala +++ b/src/main/scala/scalauv/UvConstants.scala @@ -39,8 +39,24 @@ object FileOpenFlags { object CreateMode { + val S_IRWXU = 0x1c0 val S_IRUSR = 0x100 val S_IWUSR = 0x80 + val S_IXUSR = 0x40 + + val S_IRWXG = 0x38 + val S_IRGRP = 0x20 + val S_IWGRP = 0x10 + val S_IXGRP = 0x8 + + val S_IRWXO = 0x7 + val S_IROTH = 0x4 + val S_IWOTH = 0x2 + val S_IXOTH = 0x1 + + val S_ISUID = 0x800 + val S_ISGID = 0x400 + val S_ISVTX = 0x200 val None = 0 @@ -199,6 +215,8 @@ opaque type DirEntType = CInt object DirEntType { + given Tag[DirEntType] = Tag.Int + val UV_DIRENT_UNKNOWN: DirEntType = 0 val UV_DIRENT_FILE: DirEntType = 1 val UV_DIRENT_DIR: DirEntType = 2 diff --git a/src/main/scala/scalauv/UvUtils.scala b/src/main/scala/scalauv/UvUtils.scala index 30d608a..09e31aa 100644 --- a/src/main/scala/scalauv/UvUtils.scala +++ b/src/main/scala/scalauv/UvUtils.scala @@ -283,7 +283,7 @@ object SocketAddressIp4 { val size: CSize = helpers.scala_uv_sizeof_sockaddr_in() - inline def apply(address: Ip4Address, port: Port): SocketAddressIp4 = { + def apply(address: Ip4Address, port: Port): SocketAddressIp4 = { val sockaddr = stackalloc[Byte](size).asInstanceOf[SocketAddressIp4] helpers.scala_uv_init_sockaddr_in(address, port, sockaddr) sockaddr diff --git a/src/main/scala/scalauv/helpers.scala b/src/main/scala/scalauv/helpers.scala index 395e834..f56d829 100644 --- a/src/main/scala/scalauv/helpers.scala +++ b/src/main/scala/scalauv/helpers.scala @@ -5,6 +5,8 @@ import scala.scalanative.unsafe.* @extern private[scalauv] object helpers { + def scala_uv_fs_req_get_loop(req: FileReq): Loop = extern + def scala_uv_buf_init( base: Ptr[CChar], len: CUnsignedInt, @@ -25,12 +27,12 @@ private[scalauv] object helpers { def scala_uv_connect_stream_handle(req: ConnectReq): StreamHandle = extern - def scala_uv_shutdown_stream_handle(req: Req): StreamHandle = + def scala_uv_shutdown_stream_handle(req: ShutdownReq): StreamHandle = extern - def scala_uv_write_stream_handle(req: Req): StreamHandle = extern + def scala_uv_write_stream_handle(req: WriteReq): StreamHandle = extern - def scala_uv_send_stream_handle(req: Req): StreamHandle = extern + def scala_uv_send_stream_handle(req: WriteReq): StreamHandle = extern def scala_uv_sizeof_sockaddr_in(): CSize = extern diff --git a/src/test/scala/scalauv/TcpSpec.scala b/src/test/scala/scalauv/TcpSpec.scala index 4be97ab..e8fbf35 100644 --- a/src/test/scala/scalauv/TcpSpec.scala +++ b/src/test/scala/scalauv/TcpSpec.scala @@ -21,32 +21,6 @@ final class TcpSpec { val loop = stackalloc[Byte](uv_loop_size()).asInstanceOf[Loop] uv_loop_init(loop).checkErrorThrowIO() - def allocBuffer: AllocCallback = { - (handle: Handle, suggestedSize: CSize, buf: Buffer) => - buf.mallocInit(suggestedSize) - } - - def onNewConnection: ConnectionCallback = { - (handle: StreamHandle, status: ErrorCode) => - val loop = uv_handle_get_loop(handle) - UvUtils.attemptCatch { - status.checkErrorThrowIO() - val clientTcpHandle = TcpHandle.malloc() - println("New connection") - uv_tcp_init(loop, clientTcpHandle) - .onFail(clientTcpHandle.free()) - .checkErrorThrowIO() - UvUtils.onFail(uv_close(clientTcpHandle, onClose)) - uv_handle_set_data(clientTcpHandle, handle.toPtr) - uv_accept(handle, clientTcpHandle).checkErrorThrowIO() - uv_read_start(clientTcpHandle, allocBuffer, onRead) - .checkErrorThrowIO() - () - } { exception => - setFailed(exception.getMessage()) - } - } - val port = 10000 val serverTcpHandle = TcpHandle.stackAllocate() uv_tcp_init(loop, serverTcpHandle).checkErrorThrowIO() @@ -55,37 +29,6 @@ final class TcpSpec { .checkErrorThrowIO() uv_listen(serverTcpHandle, 128, onNewConnection).checkErrorThrowIO() - def onWrite: StreamWriteCallback = { (req: WriteReq, status: ErrorCode) => - status.onFailMessage(setFailed) - val buf = Buffer.unsafeFromPtr(uv_req_get_data(req)) - stdlib.free(buf.base) - buf.free() - req.free() - } - - def onConnect: ConnectCallback = { (req: ConnectReq, status: ErrorCode) => - status.onFailMessage(setFailed) - val stream = req.connectReqStreamHandle - def doWrite(text: String) = { - val writeReq = WriteReq.malloc() - val cText = mallocCString(text) - val buf = Buffer.malloc(cText, text.length.toULong) - uv_req_set_data(writeReq, buf.toPtr) - uv_write(writeReq, stream, buf, 1.toUInt, onWrite).onFailMessage { - s => - stdlib.free(cText) - buf.free() - writeReq.free() - setFailed(s) - } - } - doWrite(text) - doWrite(text) - doWrite(DoneMarker.toString) - uv_close(stream, null) - () - } - val clientTcpHandle = TcpHandle.stackAllocate() uv_tcp_init(loop, clientTcpHandle).checkErrorThrowIO() val clientSocketAddress = SocketAddressIp4.loopbackAddress(port) @@ -126,8 +69,64 @@ object TcpSpec { failed = Some(msg) } + def allocBuffer: AllocCallback = { + (handle: Handle, suggestedSize: CSize, buf: Buffer) => + buf.mallocInit(suggestedSize) + } + + def onNewConnection: ConnectionCallback = { + (handle: StreamHandle, status: ErrorCode) => + val loop = uv_handle_get_loop(handle) + UvUtils.attemptCatch { + status.checkErrorThrowIO() + val clientTcpHandle = TcpHandle.malloc() + println("New connection") + uv_tcp_init(loop, clientTcpHandle) + .onFail(clientTcpHandle.free()) + .checkErrorThrowIO() + UvUtils.onFail(uv_close(clientTcpHandle, onClose)) + uv_handle_set_data(clientTcpHandle, handle.toPtr) + uv_accept(handle, clientTcpHandle).checkErrorThrowIO() + uv_read_start(clientTcpHandle, allocBuffer, onRead) + .checkErrorThrowIO() + () + } { exception => + setFailed(exception.getMessage()) + } + } + def onClose: CloseCallback = (_: Handle).free() + def onWrite: StreamWriteCallback = { (req: WriteReq, status: ErrorCode) => + status.onFailMessage(setFailed) + val buf = Buffer.unsafeFromPtr(uv_req_get_data(req)) + stdlib.free(buf.base) + buf.free() + req.free() + } + + def onConnect: ConnectCallback = { (req: ConnectReq, status: ErrorCode) => + status.onFailMessage(setFailed) + val stream = req.connectReqStreamHandle + def doWrite(text: String) = { + val writeReq = WriteReq.malloc() + val cText = mallocCString(text) + val buf = Buffer.malloc(cText, text.length.toULong) + uv_req_set_data(writeReq, buf.toPtr) + uv_write(writeReq, stream, buf, 1.toUInt, onWrite).onFailMessage { s => + stdlib.free(cText) + buf.free() + writeReq.free() + setFailed(s) + } + } + doWrite(text) + doWrite(text) + doWrite(DoneMarker.toString) + uv_close(stream, null) + () + } + def onRead: StreamReadCallback = { (handle: StreamHandle, numRead: CSSize, buf: Buffer) => numRead match {