Skip to content

Commit

Permalink
Add docs and cleanup.
Browse files Browse the repository at this point in the history
  • Loading branch information
quelgar committed Feb 5, 2024
1 parent b6837ab commit 647ae8f
Show file tree
Hide file tree
Showing 15 changed files with 2,105 additions and 95 deletions.
179 changes: 169 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Scala Native bindings for libuv

> Handcrafted, artisanal 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](https://libuv.org), 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.

Expand All @@ -9,7 +9,7 @@ Only Scala 3 is supported.
## Getting it

```scala
libraryDependencies += "io.github.quelgar" %%% "scala-uv" % "0.0.1"
libraryDependencies += "io.github.quelgar" %%% "scala-uv" % "0.0.2"
```

## Current status
Expand All @@ -31,9 +31,11 @@ But many details of the API are still in flux.
Runs a callback once, then closes the handle:

```scala
import scala.scalanative.unsafe.*
import scala.scalanative.unsigned.*
import scalauv.*

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

object Main {
Expand All @@ -51,7 +53,7 @@ object Main {
withZone {
val loop = uv_default_loop()

val asyncHandle = UvUtils.zoneAllocateHandle(HandleType.UV_ASYNC)
val asyncHandle = AsyncHandle.zoneAllocate()
uv_async_init(loop, asyncHandle, callback).checkErrorThrowIO()

uv_async_send(asyncHandle).checkErrorThrowIO()
Expand All @@ -60,7 +62,6 @@ object Main {
uv_run(loop, RunMode.DEFAULT).checkErrorThrowIO()
println(s"Main after, done = $done")
}

}

}
Expand All @@ -73,30 +74,188 @@ See also the tests
* [TcpSpec.scala](src/test/scala/scalauv/TcpSpec.scala)
* [FileSpec.scala](src/test/scala/scalauv/FileSpec.scala)

## 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` — currently not supported
* `uv_fileno` — currently not supported
* `uv_poll_init_socket` — currently not 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:

```scala
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` objects provides the 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 less painful.
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](src/main/scala/scalauv/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.

```scala
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.

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

Use `.onFail` to run some cleanup if the function failed.
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 it's a little verbose as everything that might need to be cleaned up needs to be declared as a `var` outside the `try` block:

```scala
uv_write(writeReq, stream, buf, 1.toUInt, onWrite).onFail {
stdlib.free(writeReq)
def onNewConnection: ConnectionCallback = {
(handle: StreamHandle, status: ErrorCode) =>
val loop = uv_handle_get_loop(handle)
var clientTcpHandle: TcpHandle = null
try {
status.checkErrorThrowIO()
clientTcpHandle = TcpHandle.malloc()
println("New connection")
uv_tcp_init(loop, clientTcpHandle).checkErrorThrowIO()
uv_handle_set_data(clientTcpHandle, handle.toPtr)
uv_accept(handle, clientTcpHandle).checkErrorThrowIO()
try {
uv_read_start(clientTcpHandle, allocBuffer, onRead)
.checkErrorThrowIO()
} catch {
case e: IOException =>
uv_close(clientTcpHandle, onClose)
throw e
}
()
} catch {
case e: IOException =>
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:


```scala
def onNewConnection: ConnectionCallback = {
(handle: StreamHandle, status: ErrorCode) =>
val loop = uv_handle_get_loop(handle)
UvUtils.attemptCatch {
status.checkErrorThrowIO()
val clientTcpHandle = TcpHandle.malloc()
UvUtils.onFail(clientTcpHandle.free())
println("New connection")
uv_tcp_init(loop, clientTcpHandle).checkErrorThrowIO()
uv_handle_set_data(clientTcpHandle, handle.toPtr)
uv_accept(handle, clientTcpHandle).checkErrorThrowIO()
UvUtils.onFail(uv_close(clientTcpHandle, onClose))
uv_read_start(clientTcpHandle, allocBuffer, onRead)
.checkErrorThrowIO()
()
} { exception =>
setFailed(exception.getMessage())
}
// if `uv_read_start` failed, then `uv_close` followed by `clientTcpHandle.free()`
// have been run in that order by this point
}
```

`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`.

```scala
// 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.
Expand Down
2 changes: 0 additions & 2 deletions build-windows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ mkdir -p build
(cd build && cmake ..)
cmake --build build

find build

echo SBT_NATIVE_COMPILE="set nativeCompileOptions += \"--include-directory=$repo_dir/libuv-build/include\" ; " >> $GITHUB_ENV

echo SBT_NATIVE_LINK="set nativeLinkingOptions += \"--library-directory=$repo_dir/libuv-build/build/Debug\" ; " >> $GITHUB_ENV
22 changes: 20 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ organization := "io.github.quelgar"

name := "scala-uv"

version := "0.0.1"
version := "0.0.2"

ThisBuild / versionScheme := Some("early-semver")

Expand Down Expand Up @@ -40,11 +40,29 @@ sonatypeCredentialHost := "s01.oss.sonatype.org"

sonatypeRepository := "https://s01.oss.sonatype.org/service/local"

credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credentials")
// credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credentials")

licenses := Seq("APL2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt"))

import xerial.sbt.Sonatype._
sonatypeProjectHosting := Some(
GitHubHosting("quelgar", "scala-uv", "[email protected]")
)

autoAPIMappings := true

apiURL := Some(
url(
s"https://javadoc.io/doc/io.github.quelgar/scala-uv_native0.4_3/${version.value}/index.html"
)
)

Compile / doc / scalacOptions ++= Seq(
"-social-links:github::https://github.com/quelgar,twitter::https://twitter.com/quelgar",
"-groups",
"-project-version",
version.value,
"-doc-root-content",
"doc-root.txt",
"-skip-by-id:scalauv.main"
)
9 changes: 9 additions & 0 deletions doc-root.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** **scala-uv** provides Scala Native bindings for the [libuv](https://libuv.org/) library.

The main API is located in the [[scalauv.LibUv]] object. Other important types are
[[scalauv.Handle]] and [[scalauv.Req]].

See [[scalauv.ErrorCodes]] for error codes.

See [[scalauv.UvUtils]] for some useful utilities to make working with libuv easier.
*/
10 changes: 10 additions & 0 deletions src/main/resources/scala-native/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,21 @@ void *scala_uv_buf_base(const uv_buf_t *buffer)
return buffer->base;
}

void scala_uv_buf_base_set(uv_buf_t *buffer, void *base)
{
buffer->base = base;
}

size_t scala_uv_buf_len(const uv_buf_t *buffer)
{
return buffer->len;
}

void scala_uv_buf_len_set(uv_buf_t *buffer, size_t len)
{
buffer->len = len;
}

size_t scala_uv_buf_struct_size()
{
return sizeof(uv_buf_t);
Expand Down
Loading

0 comments on commit 647ae8f

Please sign in to comment.