Richard Whaling
@RichardWhaling
Scala Days 2019
and it's about native-loop, a new concurrency library for Scala Native 0.4
http://github.com/scala-native/scala-native-loop
def delay(duration:Duration)(implicit ec:ExecutionContext):Future[Unit]
delay(3 seconds).flatMap { _ =>
println("beep")
delay(2 seconds)
}.flatMap { _ =>
println("boop")
delay(1 second)
}.onComplete { _ =>
println("done")
}
So we introduce a higher-level model: Actors, Streaming, etc...
Yet problems persist.
Hot Take: the JVM's threading and memory model is hostile to closure-heavy patterns.
Not At All Hot Take: a huge proportion of JVM libraries will happily block a thread
In Scala Native we are not constrained by the JVM,
nor can we piggyback on its successes;
whatever we build, we build from scratch.
Can Scala Native provide a model of IO and concurrency that is true to Scala?
Can we provide a model of IO and concurrency that is a better fit than the JVM?
But what is "idiomatic Scala"?
How can we provide the essential capabilities for the ecosystem?
libuv abstracts over different operating systems
and different kinds of IO
Consistent model of callbacks attached to handles
Can we design our API avoid the "callback hell"
or "pyramid of doom" anti-pattern?
High Level Design Goals:
Meta-goal: an unopinionated base for other idioms: streams, actor model, IO monad, etc
trait EventLoopLike extends ExecutionContextExecutor {
def addExtension(e:LoopExtension):Unit
def run(mode:Int = UV_RUN_DEFAULT):Unit
}
object Timer {
def delay(duration:Duration)(implicit ec:ExecutionContext):Future[Unit]
}
object Main {
implicit val ec:ExecutionContext = EventLoop
def main(args:Array[String]):Unit = {
println("hello!")
Timer.delay(3 seconds).onComplete { _ =>
println("goodbye!")
}
EventLoop.run()
}
}
trait Pipe[I,O] {}
def main(args:Array[String]):Unit = {
val p = FilePipe(c"./data.txt")
.map { d =>
println(s"consumed $d")
d
}.addDestination(Tokenizer("\n"))
.addDestination(Tokenizer(" "))
.map { d => d + "\n" }
.addDestination(FileOutputPipe(c"./output.txt", false))
EventLoop.run()
}
trait Stream {
def open(fd:Int): Stream
def stream(fd:Int)(f:String => Unit):Future[Unit]
def write(s:String):Future[Unit]
def pause():Unit
def close():Future[Unit]
}
def main(args:Array[String]):Unit = {
val stdout = open(STDOUT)
Pipe(STDIN).stream { line =>
println(s"read line $line")
val split = line.trim().split(" ")
for (word <- split) {
stdout.write(word + "\n")
}
}
EventLoop.run()
}
Curl.get(c"https://www.example.com").map { response =>
Response(200,"OK",Map(),response.body)
}
Curl gives us a highly capable service client, basically for free
trait Server {
def serve(port:Int)(handler:(Request,Conection) => Unit):Unit
}
def main(args:Array[String]):Unit = {
Server.serve(9999) { request, connection =>
val response = makeResponse(request)
connectionHandle.sendResponse(response)
}
EventLoop.run()
}
def main(args:Array[String]):Unit = {
Service()
.getAsync("/async") { r => Future {
s"got (async routed) request $r"
}.map { message => OK(
Map("asyncMessage" -> message)
)
}
}
.get("/") { r => OK {
Map("default_message" -> s"got (default routed) request $r")
}
}
.run(9999)
}
def main(args:Array[String]):Unit = {
Service()
.getAsync("/async") { r => Future {
s"got (async routed) request $r"
}.map { message => OK(
Map("asyncMessage" -> message)
)
}
}
.getAsync("/fetch/example") { r =>
Curl.get(c"https://www.example.com").map { response =>
Response(200,"OK",Map(),response.body)
}
}
.get("/") { r => OK {
Map("default_message" -> s"got (default routed) request $r")
}
}
.run(9999)
}
Because LibUV coordinates everything, all of these capabilities can be mixed and matched
To implement what we've seen, we will need two techniques from systems programming:
A pointer is a representation of the location of a piece of data. A Ptr[T] stores the location of a T, as a 8-byte unsigned integer.
An unsafe cast lets us treat a Ptr[T] as any other type, typically another Ptr[X] or just Long
Data structures designed for generic programming in C often have a "void pointer" field for custom data structures.
In Scala Native we represent these as Ptr[Byte]
val raw_data:Ptr[Byte] = malloc(sizeof[Long])
val long_ptr:Ptr[Long] = raw_data.asInstanceOf[Ptr[Long]]
// the ! operator updates a pointer's contents on the left-hand side
!long_ptr:Ptr = 0
// on the right-hand side, it dereferences the pointer to read its value
printf(c"pointer at %p has value %d\n", long_ptr:Ptr, !long_ptr:Ptr)
!int_ptr = 1
printf(c"pointer at %p has value %d\n", long_ptr:Ptr, !long_ptr:Ptr)
//this will segfault
free(raw_data)
printf(c"pointer at %p has value %d\n", long_ptr:Ptr, !long_ptr:Ptr)
not shown: arrays and pointer arithmetic
@extern
object Quicksort {
type Comparator = CFuncPtr2[Ptr[Byte],Ptr[Byte],Int]
def qsort(array:Ptr[Byte],num:CSize,size:CSize,cmp:Comparator):Unit = extern
}
@extern
object Quicksort {
type Comparator = CFuncPtr2[Ptr[Byte],Ptr[Byte],Int]
def qsort(array:Ptr[Byte],num:CSize,size:CSize,cmp:Comparator):Unit = extern
}
object App {
type MyStruct = CStruct3[CString,CString,Int]
val myStructComp = new Comparator {
def apply(left:Ptr[Byte],right:[Byte]):Int = {
val l = left.asInstanceOf[Ptr[MyStruct]]
val r = right.asInstanceOf[Ptr[MyStruct]]
l - r
}
}
def main(args:Array[String]):Unit = {
// ...
val data:Ptr[MyStruct] = ???
val data_size = ???
qsort(data,data_size,sizeof[MyStruct],myStructComp)
// ...
}
}
"Any sufficiently complicated C or Fortran program contains an ad hoc, informally specified, bug ridden, and slow implementation of half of Common Lisp"
object ExecutionContext {
def global: ExecutionContextExecutor = QueueExecutionContext
private object QueueExecutionContext extends ExecutionContextExecutor {
def execute(runnable: Runnable): Unit = queue += runnable
def reportFailure(t: Throwable): Unit = t.printStackTrace()
}
private val queue: ListBuffer[Runnable] = new ListBuffer
private[runtime] def loop(): Unit = { // this runs after main() returns
while (queue.nonEmpty) {
val runnable = queue.remove(0)
try {
runnable.run()
} catch {
case t: Throwable =>
QueueExecutionContext.reportFailure(t)
}
}
}
}
We just need to adapt a queue-based EC to libuv's lifecycle of callbacks
trait EventLoopLike extends ExecutionContextExecutor {
def addExtension(e:LoopExtension):Unit
def run(mode:Int = UV_RUN_DEFAULT):Unit
}
trait LoopExtension {
def activeRequests():Int
}
The LoopExtension trait lets us coordinate Future execution with other IO tasks on the same loop, and modularize our code.
object EventLoop extends EventLoopLike {
val loop = uv_default_loop()
private val taskQueue = ListBuffer[Runnable]()
def execute(runnable: Runnable): Unit = taskQueue += runnable
def reportFailure(t: Throwable): Unit = {
println(s"Future failed with Throwable $t:")
t.printStackTrace()
}
// ...
execute() is invoked as soon as a Future is ready to start running, but we can defer it until a callback fires
// ...
private def dispatchStep(handle:PrepareHandle) = {
while (taskQueue.nonEmpty) {
val runnable = taskQueue.remove(0)
try {
runnable.run()
} catch {
case t: Throwable => reportFailure(t)
}
}
if (taskQueue.isEmpty && !extensionsWorking) {
println("stopping dispatcher")
LibUV.uv_prepare_stop(handle)
}
}
private val dispatcher_cb = CFunctionPtr.fromFunction1(dispatchStep)
private def initDispatcher(loop:LibUV.Loop):PrepareHandle = {
val handle = stdlib.malloc(uv_handle_size(UV_PREPARE_T))
check(uv_prepare_init(loop, handle), "uv_prepare_init")
check(uv_prepare_start(handle, dispatcher_cb), "uv_prepare_start")
return handle
}
private val dispatcher = initDispatcher(loop)
// ...
private val extensions = ListBuffer[LoopExtension]()
private def extensionsWorking():Boolean = {
extensions.exists( _.activeRequests > 0)
}
def addExtension(e:LoopExtension):Unit = {
extensions.append(e)
}
All the actual IO work will be implemented as LoopExtensions
We'll implement the simplest one, a delay.
object Timer extends LoopExtension {
EventLoop.addExtension(this)
var serial = 0L
var timers = mutable.HashMap[Long,Promise[Unit]]() // the secret sauce
override def activeRequests():Int =
timers.size
def delay(dur:Duration):Future[Unit] = ???
val timerCB:TimerCB = ???
}
@extern
object TimerImpl {
type Timer: Ptr[Long] // why long and not byte?
type TimerCB = CFuncPtr1[TimerHandle,Unit]
def uv_timer_init(loop:Loop, handle:TimerHandle):Int = extern
def uv_timer_start(handle:TimerHandle, cb:TimerCB,
timeout:Long, repeat:Long):Int = extern
}
How is it safe to treat Timer as Ptr[Long]?
Sometimes it's necessary to work with a data structure without knowing its internal layout.
Ptr[CStruct3[Ptr[Byte],Float,CString]]
Ptr[CStruct1[Ptr[Byte]]
Ptr[CStruct1[Long]]
Ptr[Long]
(No safety guarantees)
def delay(dur:Duration):Future[Unit] = {
val millis = dur.toMillis
val promise = Promise[Unit]()
serial += 1
val timer_id = serial
timers(timer_id) = promise
val timer_handle = stdlib.malloc(uv_handle_size(UV_TIMER_T))
uv_timer_init(EventLoop.loop,timer_handle)
!timer_handle = timer_id
uv_timer_start(timer_handle, timerCB, millis, 0)
promise.future
}
We can store an 8-byte serial number in the TimerHandle, and retrieve it in our callback.
val timerCB = new TimerCB {
def apply(timer_handle:TimerHandle):Unit = {
println("callback fired!")
val timer_id = !timer_handle
val timer_promise = timers(timer_id)
timers.remove(timer_id)
println(s"completing promise ${timer_id}")
timer_promise.success(())
}
}
We can dereference the TimerHandle safely - the compiler thinks it's a Ptr[Long] so it only reads the first 8 bytes.
Then we use the serial number for a map lookup to retrieve our state.
object Main {
implicit val ec:ExecutionContext = EventLoop
def main(args:Array[String]):Unit = {
Timer.delay(3 seconds).flatMap { _ =>
println("beep")
Timer.delay(2 seconds)
}.flatMap { _ =>
println("boop")
Timer.delay(1 second)
}.onComplete { _ =>
println("done")
}
EventLoop.run()
}
}
It just works!
@RichardWhaling