Skip to content

Commit 9466b8d

Browse files
committed
I think it's done
1 parent c7327c1 commit 9466b8d

1 file changed

Lines changed: 90 additions & 12 deletions

File tree

collections/_posts/2022-09-17-typelevel-native.md

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ Christopher Davenport has put up a [scala-native-ember-example](https://github.c
3838

3939
The burden of cross-building the Typelevel ecosystem for Scala Native fell almost entirely to [Cats Effect] and [FS2].
4040

41-
#### event loops
41+
#### event loop runtime
4242

4343
**To cross-build Cats Effect for Native we had to get creative** because Scala Native currently does not support multithreading (although it will in the next major release). This is a similar situation to the JavaScript runtime, which is also fundamentally single-threaded. But an important difference is that JS runtimes are implemented with an [event loop] and offer callback-based APIs for scheduling timers and performing non-blocking I/O.
4444

4545
Meanwhile, Scala Native core does not implement an event loop nor offer such APIs. There is the [scala-native-loop] project, which wraps the [libuv] event loop runtime, but we did not want to bake such an opinionated dependency into Cats Effect core.
4646

47-
Fortunately Daniel Spiewak had the fantastic insight that the “dummy runtime” I initially used to cross-build Cats Effect for Native could be reformulated into a legitimate event loop implementation.
47+
Fortunately Daniel Spiewak had the fantastic insight that the “dummy runtime” which I created to initially cross-build Cats Effect for Native could be reformulated into a legitimate event loop implementation by extending it with the capability to “poll” for I/O events: a `PollingExecutorScheduler`.
4848

4949
The [`PollingExecutorScheduler`] implements both [`ExecutionContext`] and [`Scheduler`] and maintains two queues:
5050
- a queue of tasks (read: fibers) to execute
@@ -55,7 +55,9 @@ It also defines an abstract method:
5555
def poll(timeout: Duration): Boolean
5656
```
5757

58-
The idea of this method is very similar to `Thread.sleep()` except that besides sleeping it may also “poll” for I/O events. To demonstrate the API contract, consider invoking `poll(3.seconds)`:
58+
The idea of this method is very similar to `Thread.sleep()` except that besides sleeping it may also “poll” for I/O events. It turns out that APIs like this are ubiquitous in C libraries that perform I/O.
59+
60+
To demonstrate the API contract, consider invoking `poll(3.seconds)`:
5961

6062
*I have nothing to do for the next 3 seconds. So wake me up then, or earlier if there is an incoming I/O event that I should handle. But wake me up no later!*
6163

@@ -72,27 +74,85 @@ Thus, with tasks, timers, and the capability to poll for I/O, we can express the
7274
- **There is at least one outstanding timer**. Call `poll(durationToNextTimer)`, so it will sleep until the next I/O event arrives or the timeout expires, whichever comes first.
7375
- **There are no tasks to do and no outstanding timers.** Call `poll(Duration.Infinite)`, so it will sleep until the next I/O event arrives.
7476

75-
In fact, this is a very basic implementation of the [I/O Integrated Runtime Concept]. The grander idea is that every `WorkerThread` in the `WorkStealingThreadPool` that underpins the Cats Effect JVM runtime can run an event loop exactly like the one described above, for exceptionally high-performance I/O.
77+
This algorithm is not a Cats Effect original: the [libuv event loop] works in essentially the same way. It is however a first step towards to the much grander Cats Effect [I/O Integrated Runtime Concept]. The big idea is that every `WorkerThread` in the `WorkStealingThreadPool` that underpins the Cats Effect JVM runtime can run an event loop exactly like the one described above, for exceptionally high-performance I/O.
7678

7779
#### non-blocking I/O
7880

79-
**So, how do we implement `poll`?** The bad news is that the answer is OS-specific, which is a large reason why projects such as libuv exist. Furthermore, the entire purpose of polling is to support non-blocking I/O, which falls outside of the scope of Cats Effect. This brings us to FS2, and specifically the [`fs2-io`] module.
81+
**So, how do we implement `poll`?** The bad news is that the answer is OS-specific, which is a large reason why projects such as libuv exist. Furthermore, the entire purpose of polling is to support non-blocking I/O, which falls outside of the scope of Cats Effect. This brings us to FS2, and specifically the [`fs2-io`] module where we want to implement non-blocking TCP [`Socket`]s.
82+
83+
One such polling API is [epoll], available only on Linux:
84+
85+
```c
86+
#include <sys/epoll.h>
87+
88+
int epoll_create1(int flags);
89+
90+
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
91+
92+
int epoll_wait(int epfd, struct epoll_event *events,
93+
int maxevents, int timeout);
94+
```
95+
96+
After creating an epoll instance (identified by a file descriptor) we can register sockets (also identified by file descriptors) with `epoll_ctl`. Typically we will register to be notified of the “read-ready” (`EPOLLIN`) and “write-ready” (`EPOLLOUT`) events on that socket. Finally, the actual polling is implemented with `epoll_wait`, which sleeps until the next I/O event is ready or the `timeout` expires.
97+
98+
As previously mentioned, these sorts of polling APIs are ubiquitous and not just for working directly with sockets. For example, [libcurl](https://curl.se/libcurl/) (the C library behind the well-known CLI) exposes a function for polling for I/O on all ongoing HTTP connections.
99+
100+
```c
101+
#include <curl/curl.h>
102+
103+
CURLMcode curl_multi_poll(CURLM *multi_handle,
104+
struct curl_waitfd extra_fds[],
105+
unsigned int extra_nfds,
106+
int timeout_ms,
107+
int *numfds);
108+
```
109+
110+
Indeed, this function underpins the `CurlExecutorScheduler` in [http4s-curl].
111+
112+
On macOS and BSDs the [kqueue] API plays an analogous role to epoll. We will not talk about Windows today :)
113+
114+
Long story short, I did not want the FS2 codebase to absorb all of this cross-OS complexity. So in collaboration with Lee Tibbert we repurposed my cheeky [epollcat] experiment into an actual library implementing JDK NIO APIs (specifically, [`AsynchronousSocketChannel`] and friends). Since these are the same APIs used by the JVM implementation of `fs2-io`, it actually enables this code to be completely shared.
115+
116+
[epollcat] implements an `EpollExecutorScheduler` for Linux and a `KqueueExecutorScheduler` for macOS. They additionally provide an API for monitoring a socket file descriptor for read-ready and write-ready events.
117+
118+
```scala
119+
def monitor(fd: Int, reads: Boolean, writes: Boolean)(
120+
cb: EventNotificationCallback
121+
): Runnable // returns a `Runnable` to un-monitor the file descriptor
122+
123+
trait EventNotificationCallback {
124+
def notifyEvents(readReady: Boolean, writeReady: Boolean): Unit
125+
}
126+
```
127+
128+
These are then used to implement the callback-based `read` and `write` methods of the JDK `AsynchronousSocketChannel`.
129+
130+
It is worth pointing out that the JVM actually implements `AsynchronousSocketChannel` with an event loop as well. The difference is that on the JVM, this event loop is used only for I/O and runs on a separate thread from the compute pool used for fibers and the scheduler thread used for timers. Meanwhile, epollcat is an example of an I/O integrated runtime where fibers, timers, and I/O are all managed on a single thread.
80131

81132
#### TLS
82133

83-
**The final important piece of the cross-build puzzle was a [TLS] implementation** for `TLSSocket` and related APIs in FS2. Although this task was daunting, ultimately it was straightforward to directly integrate with [s2n-tls], which has a well-designed and well-documented API. This is effectively the only non-Scala dependency required to use the Typelevel stack on Native.
134+
**The last critical piece of the cross-build puzzle was a [TLS] implementation** for [`TLSSocket`] and related APIs in FS2. Although the prospect of this was daunting, in the end it was actually fairly straightforward to directly integrate with [s2n-tls], which exposes a well-designed and well-documented C API. This is effectively the only non-Scala dependency required to use the Typelevel stack on Native.
135+
136+
Finally, special thanks to Ondra Pelech and Lorenzo Gabriele for cross-building [scala-java-time] and [scala-java-locales] for Native and David Strawn for developing [idna4s]. These projects fill important gaps in the Scala Native re-implementation of the JDK that were essential to seamless cross-building.
137+
138+
And ... that is pretty much it. **From here, any library or application that is built using Cats Effect and FS2 cross-builds for Scala Native effectively for free.** Three spectacular examples of this are:
84139

85-
And that is pretty much it. **From here, any library or application that is built using Cats Effect and FS2 cross-builds for Scala Native effectively for free.** Three spectacular examples of this are:
86140
* [http4s] Ember, a server+client duo with HTTP/2 support
87141
* [Skunk], a Postgres client
88142
* [rediculous], a Redis client
89143

144+
These libraries in turn unlock projects such as [feral], [Grackle], and [smithy4s].
145+
90146
### What’s next and how can I get involved?
91147

92148
Please try the Typelevel Native stack! And even better deploy it, and do so loudly!
93149

94150
Besides that, here is a brain-dump of project ideas and existing projects that would love contributors. I am happy to help folks get started on any of these, or ideas of your own!
95151

152+
* Creating example applications, templates, and tutorials.
153+
- If you are lacking inspiration, try cross-building existing examples such as [fs2-chat], [kitteh-redis], [Jobby].
154+
- Spread the word: [you-forgot-a-percentage-sign-or-a-colon].
155+
96156
* Cross-building existing libraries and developing new, Typelevel-stack ones:
97157
- Go [feral] and implement a pure Scala [custom AWS Lambda runtime] that cross-builds for Native.
98158
- A pure Scala [gRPC] implementation built on http4s would be fantastic, even for the JVM. Christopher Davenport has published a [proof-of-concept][grpc-playground].
@@ -102,7 +162,7 @@ Besides that, here is a brain-dump of project ideas and existing projects that w
102162
* Integrations with native libraries:
103163
- I kick-started [http4s-curl] and would love to see someone take the reigns!
104164
- An [NGINX Unit] server backend for http4s promises exceptional performance. [snunit] pioneered this approach.
105-
- Using [quiche] for HTTP/3 looks fun!
165+
- Using [quiche] for HTTP/3 looks yummy!
106166
- An idiomatic wrapper for [SQLite]. See also [davenverse/sqlite-sjs#1] which proposes cross-platform API backed by Doobie on the JVM.
107167

108168
* Developing I/O-integrated runtimes:
@@ -113,12 +173,13 @@ Besides that, here is a brain-dump of project ideas and existing projects that w
113173
* Tooling. Anton Sviridov has spear-headed two major projects in this area:
114174
- [sbt-vcpkg] is working hard to solve the native dependency problem.
115175
- [sn-bindgen] generates Scala Native bindings to native libraries directly from `*.h` header files. I found it immensely useful while working on http4s-curl, epollcat, and the s2n-tls integration in FS2.
176+
- Also: we are _badly_ in need of a pure Scala port of the [Java Microbenchmark Harness]. Not the whole thing obviously, but just enough to run the existing Cats Effect benchmarks for example.
116177

117178
* Scala Native itself. Lots to do there!
118179

119180
### Ember native benchmark
120181

121-
```
182+
```console
122183
$ hey -z 30s http://localhost:8080
123184

124185
Summary:
@@ -165,42 +226,59 @@ Status code distribution:
165226
[200] 114525 responses
166227
```
167228

229+
[`AsynchronousSocketChannel`]: https://docs.oracle.com/javase/8/docs/api/java/nio/channels/AsynchronousSocketChannel.html
168230
[bobcats]: https://github.com/typelevel/bobcats
169231
[Cats Effect]: https://typelevel.org/cats-effect/
170232
[custom AWS Lambda runtime]: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html
171233
[davenverse/sqlite-sjs#1]: https://github.com/davenverse/sqlite-sjs/pull/1
172234
[`ExecutionContext`]: https://www.scala-lang.org/api/2.13.8/scala/concurrent/ExecutionContext.html
173235
[event loop]: https://javascript.info/event-loop
236+
[epoll]: https://man7.org/linux/man-pages/man7/epoll.7.html
174237
[epollcat]: https://github.com/armanbilge/epollcat
175238
[feral]: https://github.com/typelevel/feral
176239
[FS2]: https://fs2.io/
240+
[fs2-chat]: https://github.com/typelevel/fs2-chat/
177241
[fs2-data]: https://github.com/satabin/fs2-data/
178242
[`fs2-io`]: https://fs2.io/#/io
179243
[GraalVM Native Image]: https://www.graalvm.org/22.2/reference-manual/native-image/
244+
[Grackle]: https://github.com/gemini-hlsw/gsp-graphql
180245
[gRPC]: https://grpc.io/
181246
[grpc-playground]: https://github.com/ChristopherDavenport/grpc-playground
182247
[http4s]: https://http4s.org/
183248
[http4s-curl]: https://github.com/http4s/http4s-curl/
184249
[http4s-fs2-data]: https://github.com/http4s/http4s-fs2-data
250+
[idna4s]: https://github.com/typelevel/idna4s
185251
[I/O Integrated Runtime Concept]: https://github.com/typelevel/cats-effect/discussions/3070
186252
[io_uring]: https://en.wikipedia.org/wiki/Io_uring
253+
[Jobby]: https://github.com/keynmol/jobby/
254+
[kitteh-redis]: https://github.com/djspiewak/kitteh-redis
255+
[kqueue]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
256+
[Java Microbenchmark Harness]: https://github.com/openjdk/jmh
187257
[libuv]: https://github.com/libuv/libuv/
258+
[libuv event loop]: https://docs.libuv.org/en/v1.x/design.html#the-i-o-loop
259+
[libcurl]: https://curl.se/libcurl/
188260
[LLVM]: https://llvm.org/
261+
[`MonadCancel`]: https://typelevel.org/cats-effect/docs/typeclasses/monadcancel
189262
[NGINX Unit]: https://unit.nginx.org/
190263
[`PollingExecutorScheduler`]: https://github.com/typelevel/cats-effect/blob/7ca03db50342773a79a01ecf137d953408ac6b1d/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala
191264
[quiche]: https://github.com/cloudflare/quiche
192265
[rediculous]: https://github.com/davenverse/rediculous
266+
[`Resource`]: https://typelevel.org/cats-effect/docs/std/resource
193267
[sbt-vcpkg]: https://github.com/indoorvivants/sbt-vcpkg/
194268
[ScalablyTyped]: https://scalablytyped.org/
195269
[Scala Native]: https://scala-native.org/
196270
[Scala.js]: https://www.scala-js.org/
271+
[scala-java-locales]: https://github.com/cquiroz/scala-java-locales
272+
[scala-java-time]: https://github.com/cquiroz/scala-java-time
197273
[scala-native-loop]: https://github.com/scala-native/scala-native-loop/
198274
[`Scheduler`]: https://github.com/typelevel/cats-effect/blob/236a0db0e95be829de34d7a8e3c06914738b7b06/core/shared/src/main/scala/cats/effect/unsafe/Scheduler.scala
199275
[Skunk]: https://github.com/tpolecat/skunk
276+
[smithy4s]: https://disneystreaming.github.io/smithy4s/
277+
[`Socket`]: https://www.javadoc.io/doc/co.fs2/fs2-docs_2.13/latest/fs2/io/net/Socket.html
200278
[SQLite]: https://www.sqlite.org/index.html
201279
[snunit]: https://github.com/lolgab/snunit
202280
[sn-bindgen]: https://github.com/indoorvivants/sn-bindgen
203281
[s2n-tls]: https://github.com/aws/s2n-tls
204-
[TLS]: https://en.wikipedia.org/wiki/Transport_Layer_Security
205-
[`MonadCancel`]: https://typelevel.org/cats-effect/docs/typeclasses/monadcancel
206-
[`Resource`]: https://typelevel.org/cats-effect/docs/std/resource
282+
[TLS]: https://en.wikipedia.org/wiki/Transport_Layer_Security\
283+
[`TLSSocket`]: https://www.javadoc.io/doc/co.fs2/fs2-docs_2.13/latest/fs2/io/net/tls/TLSSocket.html
284+
[you-forgot-a-percentage-sign-or-a-colon]: https://youforgotapercentagesignoracolon.com/

0 commit comments

Comments
 (0)