quelgar / scala-uv   0.1.0

Apache License 2.0 GitHub

Handcrafted Scala Native language bindings for libuv

Scala versions: 3.x
Scala Native versions: 0.5 0.4

Scala Native bindings for libuv

Artisanal, handcrafted Scala Native bindings for libuv.

scala-uv is a Scala Native library that provides Scala bindings for libuv, which is a multi-platform asynchronous IO library written in C. libuv was originally developed for Node.js, but it's also used by other software projects.

Only Scala 3 is supported.

Getting it

libraryDependencies += "io.github.quelgar" %%% "scala-uv" % "0.1.0"

Current status

Very early days, most of the APIs have bindings, but not all. Many are not tested at all. What I've tried so far:

  • Error handling
  • Event loop
  • Async callbacks
  • Read and write files
  • TCP client and server

But many details of the API are still in flux.

Examples

Async callback

Runs a callback once, then closes the handle:

import scalauv.*

import scala.scalanative.*
import unsafe.*
import unsigned.*
import LibUv.*

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 = {

    Zone {
      val loop = uv_default_loop()

      val asyncHandle = AsyncHandle.zoneAllocate()
      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")
    }
  }

}

Test examples

See also the tests

Differences from the C API

scala-uv tries to expose the exact C API of libuv as directly as possible. However, due to the nature of Scala Native, some changes are necessary.

Functions

Most of the libuv functions can be found in the LibUv object, with the same name as the C function. The exceptions are the following:

  • uv_loop_configuration — not yet supported
  • uv_fileno — not yet supported
  • uv_poll_init_socket — not yet supported
  • Process handle functions — not yet supported
  • uv_socketpair — not yet supported
  • uv_udp_open — not yet supported
  • uv_fs_chown — not yet supported
  • uv_fs_fchown — not yet supported
  • uv_fs_lchown — not yet supported
  • uv_fs_getosfhandle — not yet supported
  • uv_open_osfhandle — not yet supported

Handles

The C handle type uv_handle_t* is represented by the Handle type in Scala. There are subtypes of Handle for each of the pseudo-subtypes of uv_handle_t in C, such as AsyncHandle, TcpHandle, etc.

Each type of handle has a companion object with methods to allocate the memory for that type of handle, for example AsyncHandle.zoneAllocate(), TcpHandle.stackAllocate(), etc.

The HandleType object has the handle type constants.

Requests

Similarly, the C request type uv_req_t* is represented by the Req type in Scala. There are subtypes of Req for each of the pseudo-subtypes of uv_req_t in C, such as WriteReq, ConnectReq, etc.

Each type of request has a companion object with methods to allocate the memory for that type of request, for example WriteReq.zoneAllocate(), ConnectReq.stackAllocate(), etc.

The ReqType object has the request type constants.

Buffers

The Buffer type is a Scala wrapper around the uv_buf_t* type in C. To allocate and initialize a new Buffer on the stack:

val size = 100
val base = stackAlloc[Byte](size)
val buffer = Buffer.stackAllocate(base, size)

Error codes

The ErrorCodes object has all the error codes from libuv as Scala constants, with the same names as in C.

Constants

Various objects provide the constant values needed to use various aspectes of the libuv API:

  • RunMode
  • FileOpenFlags
  • CreateMode
  • AccessCheckMode
  • PollEvent
  • ProcessFlags
  • StdioFlags
  • TtyMode
  • TtyVtermState
  • UdpFlags
  • Membership
  • FsEvent
  • FsType
  • DirEntType
  • ClockType

Conveniences

The LibUv object provides most othe exact libuv API, but when using it directly you are basically writing C code with Scala syntax. A few convenienves are provided to make this a little less painful.

Dealing with libuv failures

libuv functions that can fail return a negative integer on failure, with the value indicating the precise error. The possible error codes are in errors.scala.

Pass an error code to UvUtils.errorMessage to get the human-readable error message as a Scala string.

Use .onFail to run some cleanup if the function failed.

uv_write(writeReq, stream, buf, 1.toUInt, onWrite).onFail {
    stdlib.free(writeReq)
}

Use .checkErrorThrowIO() on the result of a libuv function to throw an IOException if the function failed. Note this isn't useful inside a callback, since you definitely should not throw exceptions from a C callback.

uv_listen(serverTcpHandle, 128, onNewConnection).checkErrorThrowIO()

When using a callback-based library like libuv, it is common that when everything works, cleanup like freeing memory must be done in a different callback function. However if something fails, we need to immediately cleanup anything we've allocated already. We can use .checkErrorThrowIO() with try/catch to do this, but we need to mainain some vars to keep track of how far we got:

def onClose: CloseCallback = (_: Handle).free()

def onNewConnection: ConnectionCallback = {
  (handle: StreamHandle, status: ErrorCode) =>
    val loop = uv_handle_get_loop(handle)
    var clientTcpHandle: TcpHandle = null
    var initialized = false
    try {
      status.checkErrorThrowIO()
      clientTcpHandle = TcpHandle.malloc()
      uv_tcp_init(loop, clientTcpHandle).checkErrorThrowIO()
      initialized = true
      uv_handle_set_data(clientTcpHandle, handle.toPtr)
      uv_accept(handle, clientTcpHandle).checkErrorThrowIO()
      uv_read_start(clientTcpHandle, allocBuffer, onRead)
        .checkErrorThrowIO()
      ()
    } catch { 
      case e: IOException =>
        if (initialized)
          // note the onClose callback will free the handle
          uv_close(clientTcpHandle, onClose)
        else if (clientTcpHandle != null)
          clientTcpHandle.free()
        setFailed(exception.getMessage())
    }
}

As an alternative, scala-uv provides UvUtils.attemptCatch to make scenarios such as this easier. Within an attemptCatch block, you can register cleanup actions at any point using UvUtils.onFail. These cleanup actions will be performed (in reverse order to the order registered) if an exception is thrown. If the code block completes, no cleanup is performed. A function to handle the exception must also be provided. This simplifies the above example to:

def onClose: CloseCallback = (_: Handle).free()

def onNewConnection: ConnectionCallback = {
  (handle: StreamHandle, status: ErrorCode) =>
    val loop = uv_handle_get_loop(handle)
    UvUtils.attemptCatch {
      status.checkErrorThrowIO()
      val clientTcpHandle = TcpHandle.malloc()
      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())
    }
}

UvUtils.attemptCatch is designed for use in callback functions where you don't want to throw any exceptions. There is also UvUtils.attempt, which runs the cleanup actions but does not catch the exception.

File I/O

The libuv file I/O functions support use of multiple buffers at once. scala-uv provides the IOVector type for working with multiple buffers with varying amoutns of data.

scala-uv provides a shortcut for allocating, using and freeing FileReq objects, if you are doing blocking I/O: FileReq.use.

// writes the C string pointed to by `cText` to a file
val bytesWritten = FileReq
  .use { writeReq =>
    val iov =
      IOVector.stackAllocateForBuffer(cText, string.strlen(cText).toUInt)
    uv_fs_write(
      loop,
      writeReq,
      fileHandle,
      iov.nativeBuffers,
      iov.nativeNumBuffers,
      -1,
      null
    )
  }
  .checkErrorThrowIO()

Malloc C strings

While the Zone memory allocation API from Scala Native is very nice, it's not useful when the memory is freed in a different callback, as there won't be a shared lexical scope. So mallocCString converts a Scala string to a C string, allocating the memory the old-fashioned way.


Copyright 2024 Lachlan O'Dea

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.