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.
libraryDependencies += "io.github.quelgar" %%% "scala-uv" % "0.1.0"
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.
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")
}
}
}
See also the tests
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.
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 supporteduv_fileno
— not yet supporteduv_poll_init_socket
— not yet supported- Process handle functions — not yet supported
uv_socketpair
— not yet supporteduv_udp_open
— not yet supporteduv_fs_chown
— not yet supporteduv_fs_fchown
— not yet supporteduv_fs_lchown
— not yet supporteduv_fs_getosfhandle
— not yet supporteduv_open_osfhandle
— not yet supported
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.
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.
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)
The ErrorCodes
object has all the error codes from libuv as Scala constants, with the same names as in C.
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
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.
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 var
s 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.
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()
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.