scala-uv v0.0.2
I've published v0.0.2 of scala-uv, "Artisanal, handcrafted Scala Native bindings for libuv".
Documentation
There's now some API docs, with links to the corresponding libuv docs for each function. The readme also now has details about how scala-uv represents libuv types like uv_handle_t
in Scala.
Type safety
As previously discussed, structures used in C APIs are often "abstract", meaning their precise layout is not documented, and often vary across platforms. In Scala Native, we use a Ptr[Byte]
to point to such structures, with all the code that actually accesses fields of the structures is implemented in C.
Previously I had used type aliases for such types. This works, but gives worse type safety than C, as you can freely pass a uv_tcp_t*
where a uv_udp_t*
is required, for example. But opaque types solve the problem nicely:
opaque type Handle = Ptr[Byte]
opaque type TcpHandle <: Handle = Ptr[Byte]
opaque type UdpHandle <: Handle = Ptr[Byte]
This also gives us proper subtyping, where TcpHandle
can be passed to any function requiring Handle
, whereas in C we'd have to manually "upcast" it. Nice!
To make this work, we need one bit of boilerplate so that Scala Native knows how to deal with our opaque type:
object Handle {
given Tag[Handle] = Tag.Ptr(Tag.Byte)
}
Error handling
Allocating memory or acquiring resources in C is a painful business. When using callbacks you often free memory and close resources in another function that is called by libuv after whatever operation you're doing has finished. But there are typically failure conditions that can crop up that will prevent running the operation, meaning your callback will never be called. In such cases, you need to free whatever you've already allocated right away.
Doing this just with if
statements is quite tedious, and in C goto
is often used for error handling as a poor man's try
/catch
. In Scala we have actual exceptions, so scala-uv provides checkErrorThrowIO()
to turn negative error codes into thrown exceptions. This allows us to collect our error handling in one place, but requires maintaining some var
s to track 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())
}
}
To make this a little easier, scala-uv also offers UvUtils.attemptCatch
, which allows you to register whatever cleanup code you like using UvUtils.onFail
. If an exception is thrown from the attemptCatch
block, then any cleanup code that has been registered so far is then run, in order from the most recently registered first:
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())
}
}
There is also UvUtils.attempt
for situations where you want to run cleanup code, but still want the exception to propagate up. Cleanup code registered using UvUtils.onFail
only runs if an exception is thrown, whereas cleanup code registered using UvUtils.onComplete
runs whether the attempt
block succeeds or fails.
Note that the above example is a little tricky. If the uv_tcp_init
call fails, we want to free the handle we've allocated. But once the initialisation succeeds, then we will use uv_close
to close the handle, which will also free the memory allocated for the handle (in the onClose
callback). So we do not want to execute both uv_close
and clientTcpHandle.free()
. In the first version, this is achieved by only calling free
if initialized
was false. In the second version, we attach the call to free
only to the result of uv_tcp_init
without registering it as a cleanup operation for the attemptCatch
block.