Fast, Simple Concurrency with Scala Native

This talk is about:

  • bare-metal concurrency on scala native
  • native as a platform
  • scala as a platform
  • sustainable libraries and communities

and it's about native-loop, a new concurrency package for Scala Native 0.4

  • an extensible event loop and IO system
  • backed by the libuv event loop
  • works with other C libraries (libcurl, etc)

http://...

Talk Outline

  1. Scala Native Crash Course
  2. implementation deep dive:
    • libuv-based ExecutionContext
    • simple streaming IO
  3. HTTP client/server API design
  4. the future
 

About me:

  • @RichardWhaling
  • Author of "Modern Systems Programming in Scala Native"
  • Lead Data Engineer at M1Finance

Scala Native is:

  • Scala!
  • a scalac plugin
  • targets LLVM rather than JVM bytecode
  • produces compact, optimized native binaries
  • a lot of ordinary Scala code "just works"
  • full control over memory allocation
  • struct and array layout
  • C FFI
  • An embedded scala DSL with the capabilities of C

But that's not all!

Caveat

  • this low-level functionality is powerful but dangerous
  • you don't need unsafe functionality to use scala native
  • ideally: idiomatic Scala API on top of low-level code
  • Scala Native is (for now) single-threaded
  • No JDK - essential classes re-implemented in Scala
  • C FFI fills in the gaps in capabilities

But what is "idiomatic Scala"?

Example: Memory Allocation

val raw_data:Ptr[Byte] = malloc(sizeof[Long])

val int_ptr:Ptr[Int] = raw_data.asInstanceOf[Int]

// the ! operator updates a pointer's contents on the left-hand side
!int_ptr = 0

// on the right-hand side, it dereferences the pointer to read its value
printf(c"pointer at %p has value %d\n", int_ptr, !int_ptr)

!int_ptr = 1
printf(c"pointer at %p has value %d\n", int_ptr, !int_ptr)

//this will segfault
free(raw_data)
printf(c"pointer at %p has value %d\n", int_ptr, !int_ptr)

not shown: arrays and pointer arithmetic

Example: C FFI

@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]]
      if (l._3 < r._3) {
        -1
      } else if (l._3 == l._3) {
        0
      else {
        1
      }
    }
  } 
}

Example: C FFI

@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)
    // ...
  }
}

Example: Function Pointers

  • instances of CFuncPtr can be passed as callbacks to C
  • In POSIX this is common for polymorphic functions
  • sort, bsearch, etc
  •  

We'll see more of this soon!

LibUV's IO system

libuv abstracts over different operating systems and different kinds of IO

LibUV's event loop

We just need to adapt a queue-based EC to libuv's lifecycle of callbacks
 

  • We queue up work
  • A prepare handle run immediately prior to IO
  • It runs tasks until the queue is exhausted
  • When there are no more tasks and no more IO, we are done!
  • The catch - how do we track IO that isn't a Future - like our Pipe example?

BYO ExecutionContext

  • Scala Native includes an EC already
  • The catch - it runs after main() returns
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)
      }
    }
  }
}

EventLoop and LoopExtensions

trait EventLoopLike extends ExecutionContextExecutor {
  def addExtension(e:LoopExtension):Unit 
  def run(mode:Int = UV_RUN_DEFAULT):Unit
}

trait LoopExtension {
  def activeRequests():Int
}

Our EventLoop

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()
  }
  // ...

Our EventLoop

  // ...
  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)
  // ...

LoopExtensions

  private val extensions = ListBuffer[LoopExtension]()

  private def extensionsWorking():Boolean = {
    extensions.exists( _.activeRequests > 0)
  }

  def addExtension(e:LoopExtension):Unit = {
    extensions.append(e)
  }

Timer

object Timer extends LoopExtension {
  EventLoop.addExtension(this)

  var serial = 0L
  var timers = mutable.HashMap[Long,Promise[Unit]]()

  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?
  def uv_timer_init(loop:Loop, handle:TimerHandle):Int = extern
  def uv_timer_start(handle:TimerHandle, cb:TimerCB, 
                     timeout:Long, repeat:Long):Int = extern
  def uv_timer_stop(handle:TimerHandle):Int = extern
}

Timer

object Timer extends LoopExtension {
  EventLoop.addExtension(this)

  var serial = 0L
  var timers = mutable.HashMap[Long,Promise[Unit]]()

  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?
  def uv_timer_init(loop:Loop, handle:TimerHandle):Int = extern
  def uv_timer_start(handle:TimerHandle, cb:TimerCB, 
                     timeout:Long, repeat:Long):Int = extern
  def uv_timer_stop(handle:TimerHandle):Int = extern
}

Timer

  def delay(dur:Duration):Future[Unit] = {
    val promise = Promise[Unit]()
    serial += 1
    val timer_id = serial
    timers(timer_id) = promise
    val millis = dur.toMillis

    val timer_handle = stdlib.malloc(uv_handle_size(UV_TIMER_T))
    uv_timer_init(EventLoop.loop,timer_handle)
    val timer_data = timer_handle.asInstanceOf[Ptr[Long]]
    !timer_data = timer_id
    uv_timer_start(timer_handle, timerCB, millis, 0)

    promise.future
  }

Timer

  val timerCB = new TimerCB { 
    def apply(handle:TimerHandle):Unit = {
      println("callback fired!")
      val timer_data = handle.asInstanceOf[Ptr[Long]]
      val timer_id = !timer_data
      val timer_promise = timers(timer_id)
      timers.remove(timer_id)
      println(s"completing promise ${timer_id}")
      timer_promise.success(())
    }
  }

Timer

  val timerCB = new TimerCB { 
    def apply(handle:TimerHandle):Unit = {
      println("callback fired!")
      val timer_data = handle.asInstanceOf[Ptr[Long]]
      val timer_id = !timer_data
      val timer_promise = timers(timer_id)
      timers.remove(timer_id)
      println(s"completing promise ${timer_id}")
      timer_promise.success(())
    }
  }

Timer

object Main {
  implicit val ec:ExecutionContext = EventLoop

  def main(args:Array[String]):Unit = {
    println("hello!")
    Timer.delay(3 seconds).onComplete { _ =>
      println("goodbye!")
    }

    EventLoop.run()
  }
}

Streaming with libuv

  • libuv tracks IO via handles of different types
  • handles are configured with callbacks
  • handles all contain "free" space for a custom data pointer
@extern
object LibUV {
  def uv_pipe_init(loop:Loop, handle:PipeHandle, ipc:Int):Int = extern
  def uv_pipe_open(handle:PipeHandle, fd:Int):Int = extern
  def uv_read_start(client:PipeHandle, allocCB:AllocCB, readCB:ReadCB): Int = extern

  type Loop = ???
  type PipeHandle = ???
  type AllocCB = ???
  type ReadCB = ???
}

Streaming with libuv

  • we can represent opaque structures as Ptr[Byte] (like void *)
  • libuv provides helpers to allocate and initialize loop/handles
  • function pointer arguments must be static methods
@extern
object LibUV {
  def uv_pipe_init(loop:Loop, handle:PipeHandle, ipc:Int):Int = extern
  def uv_pipe_open(handle:PipeHandle, fd:Int):Int = extern
  def uv_read_start(client:PipeHandle, allocCB:AllocCB, readCB:ReadCB): Int = extern

  type Loop = Ptr[Byte]
  type PipeHandle = Ptr[Byte]
  type AllocCB = CFunctionPtr3[PipeHandle,CSize,Ptr[Buffer],Unit]
  type ReadCB = CFunctionPtr3[PipeHandle,CSSize,Ptr[Buffer],Unit]

  def uv_handle_size(h_type:Int): CSize = extern

  type Buffer = CStruct2[Ptr[Byte],CSize]
}

An API Sketch

trait Pipe[I,O] {
  def map[U](f: O => U):Pipe[O,U] 
  def feed(i:I):Unit
}

case class SourcePipe(fd:Int) extends Pipe[String,String] 

object SourcePipe {
  val loop = uv_default_loop()

  def stream(fd:Int):SourcePipe = {
    val handle = malloc(uv_handle_size(UV_PIPE_T)) // no need to cast
    uv_pipe_init(loop,handle,0)
    uv_pipe_open(handle,fd)
    uv_read_start(handle,???,???) // we need to supply these callbacks
  }
}

An API Sketch

trait Pipe[I,O] {
  def map[U](f: O => U):Pipe[O,U] 
  def feed(i:I):Unit
}

case class SourcePipe(fd:Int) extends Pipe[String,String] 

object SourcePipe {
  val loop = uv_default_loop()

  def stream(fd:Int):SourcePipe = {
    val handle = malloc(uv_handle_size(UV_PIPE_T)) // no need to cast
    uv_pipe_init(loop,handle,0)
    uv_pipe_open(handle,fd)
    uv_read_start(handle,???,???) // we need to supply these callbacks
  }
}

An API Sketch

trait Pipe[I,O] {
  def map[U](f: O => U):Pipe[O,U] 
  def feed(i:I):Unit
}

case class SourcePipe(fd:Int) extends Pipe[String,String]

object SourcePipe {
  val loop = uv_default_loop()
  var handlers:mutable.Map[Int,SourcePipe] // our "dispatcher"

  def stream(fd:Int):SourcePipe = {
    val handle = malloc(uv_handle_size(UV_PIPE_T)) // no need to cast
    uv_pipe_init(loop, handle, 0)
    uv_pipe_open(handle, fd)

    // store the fd in the pipe's custom data slot
    val pipeDataPointer = handle.cast[Ptr[Int]]
    !pipeDataPointer = fd
    
    uv_read_start(handle, ???, ???) // need to fill these in
    val result = SourcePipe(fd)
    handlers(fd) = result
    result
  }
}

libuv Callbacks

  • we need to supply callbacks to allocate memory and receive data
  • an alloc callback runs immediately before data is received
  • we can supply any memory allocation strategy we like
  • libuv will adjust its buffering to whatever chunk size we provide
  • we can be naive for now, but this is very powerful
  def on_alloc(client:PipeHandle, size:CSize, buffer:Ptr[Buffer]):Unit = {
    val bufferData = malloc(4096)
    !buffer._1 = bufferData
    !buffer._2 = 4096
  }
  val allocCB = CFunctionPtr.fromFunction3(on_alloc)

libuv Callbacks

  • our read callback needs to:
  • dereference the handle to get the file descriptor
  • convert the buffer struct to a regular String
  • look up the right handler and pass it the string
  • free the temp buffer
  def on_read(handle:PipeHandle,size:CSize,buffer:Ptr[Buffer]):Unit = {
    val pipeDataPointer = handle.cast[Ptr[Int]]
    val fd = !pipeDataPointer
    println(s"read $size bytes from fd $fd")
    if (size < 0) {
      println("size < 0, closing")
      handlers.remove(fd)
    } else {
      val tempBuffer = stdlib.malloc(size + 1)
      strncpy(tempBuffer, !buffer._1, size + 1)      
      val stringData = fromCString(tempBuffer)
      handlers(fd).feed(stringData)
      stdlib.free(tempBuffer)
    }
  }
  val readCB = CFunctionPtr.fromFunction3(on_read)

libuv Callbacks

  • we can customize string construction and save on copying!
  def on_read(handle:PipeHandle,size:CSize,buffer:Ptr[Buffer]):Unit = {
    val pipeDataPointer = handle.cast[Ptr[Int]]
    val fd = !pipeDataPointer
    println(s"read $size bytes from fd $fd")
    if (size < 0) {
      println("size < 0, closing")
      handlers.remove(fd)
    } else {
      val stringData = bytesToString(!buffer._1, size)
      handlers(fd).feed(stringData)
      stdlib.free(tempBuffer)
    }
  }
  val readCB = CFunctionPtr.fromFunction3(on_read)

  def bytesToString(data:Ptr[Byte],len:Long):String = {
    val bytes = new Array[Byte](len.toInt)
    var c = 0
    while (c < len) {
      bytes(c) = !(data + c)
      c += 1
    }

    new String(bytes)
  }

Tying it all together

  • The rest is straightforward, idiomatic Scala
case class SourcePipe(fd:Int) extends Pipe[String,String] {
  var destinations:List[Pipe[String,_]] = List.empty
  def feed(t:String) = {
    for (dest <- destinations) {
      dest.feed(t)
    }
  }

  def map[T](f: String => T) = {
    val m = MapPipe(f)
    destinations = destinations + m
    m
  }
}

case class MapPipe[I,O](f: I => O) extends Pipe[I,O] {
  var destinations:List[Pipe[O]] = List.empty
  def feed(t:I) = {
    val o = f(i)
    for (dest <- destinations) {
      dest.feed(o)
    }
  }
  // ...
}

Tying it all together

  • The rest is straightforward, idiomatic Scala
def main(args:Array[String]):Unit = {
  val STDIN = 0
  
  SourcePipe(STDIN).map { line =>
    print(s"read '${line.trim()}' from standard input")
  }
  uv_loop_run(uv_default_loop,0)
}
  • With a little work this becomes capable (and complex) quickly
  • flatMap, mapConcat, mapAsync, reduce, fold, etc.
  • Does coupling IO and concurrency break abstractions?
  • What are the core capabilities that other frameworks need?

An API Sketch

object PipeIO extends LoopExtension {
  type ItemHandler = ((String,PipeHandle,Long) => Unit)
  type DoneHandler = ((PipeHandle,Long) => Unit)
  type Handlers = (ItemHandler, DoneHandler)

  var streams = mutable.HashMap[Long,Handlers]()
  var serial = 0L

  override def activeRequests:Int = {
    streams.size
  }

  def stream(fd:Int)(itemHandler:ItemHandler,
                     doneHandler:DoneHandler):Long = ???

  def streamUntilDone(fd:Int)(handler:ItemHandler):Future[Long] = ???
}

Introducing native-loop

  • Concurrency as a library for Scala Native 0.4
  • Provides ExecutionContext/Futures/etc
  • Backed by the C event loop library libuv
  • Extensible with other C libraries
  • Cross-platform async IO capabilities
  • Integrate with curl to provide an async HTTP client
  • Integrates with node's http-parser for an HTTP server

Example: Future

  • Concurrency as a library for Scala Native 0.4
  • Provides ExecutionContext/Futures/etc
  • Backed by the C event loop library libuv
  • Extensible with other C libraries
def main(args:Array[String]):Unit = {
    println("hello Scala Days!")
    Future.successful(()).onComplete { v => 
        println(s"Future has completed with value $v")
    } 
    println("about to invoke event loop")
    Loop.run()
}

Example: HTTP Client

    val resp = Zone { implicit z => 
      for (arg <- args) {
        val url = toCString(arg)
        val resp = Curl.get(url)

        resp.onComplete { 
          case Success(data) =>
            println(s"got back response for ${arg} - body of length ${data.body.size}")
            println(s"headers:")
            for (h <- data.headers) {
              println(s"request header: $h")
            }
            println(s"body: ${data.body}")
          case Failure(f) =>
            println("request failed",f)
        }
      }
    }

    loop.run()

Example: HTTP Server

  def main(args:Array[String]):Unit = {
    Service()
      .getAsync("/async/") { r => Future(OK(
        Map("asyncMessage" -> s"got (async routed) request $r")
      ))}
      .get("/") { r => OK( 
        Map("message" -> s"got (routed) request $r")
      )}
      .run(9999)
    println("done")
  }

Adding an HTTP Server

  • We'll use node/http-parser
  • same C library that nodejs uses for http parsing
  • simple API, zero-copy implementation:
object Parsing {
  def http_parser_init(p:Ptr[Parser],parser_type:Int):Unit = extern
  def http_parser_settings_init(s:Ptr[ParserSettings]):Unit = extern
  def http_parser_execute(p:Ptr[Parser],s:Ptr[ParserSettings],
                          data:Ptr[Byte],len:Long):Long = extern
  def http_method_str(method:CChar):CString = extern

  type Parser = CStruct8[
    Long,   // private data
    Long,   // private data
    UShort, // major version
    UShort, // minor version
    UShort, // status (request only)
    CChar, // method
    CChar, // Error (last bit upgrade)
    Ptr[Byte] // user data
  ]

  //...

Adding an HTTP Server

  • We'll use node/http-parser
  • same C library that nodejs uses for http parsing
  • simple API, zero-copy implementation:
object Parsing {
  type HttpCB = CFunctionPtr1[Ptr[Parser],Int]
  type HttpDataCB = CFunctionPtr3[Ptr[Parser],CString,Long,Int]

  type ParserSettings = CStruct8[
    HttpCB, // on_message_begin
    HttpDataCB, // on_url
    HttpDataCB, // on_status
    HttpDataCB, // on_header_field
    HttpDataCB, // on_header_value
    HttpCB, // on_headers_complete
    HttpDataCB, // on_body
    HttpCB  // on_message_complete
  ]
  //...

Adding an HTTP Server

  1. Accept inbound TCP connections
  2. When a connection is established, allocate a parser
  3. When data is received, pass it to the parser
  4. Parser callbacks write HttpRequest state (sync)
  5. When parser's complete callback fires, invoke handler

 

type ConnectionState
type RequestState
type Router
type Response

Adding an HTTP Server

object Server extends Parsing with LoopExtension {
  import LibUVConstants._, LibUV._,HttpParser._
  implicit val ec = EventLoop
  val loop = EventLoop.loop
  var serial = 1L

  override val requests = mutable.Map[Long,RequestState]()
  var activeRequests = 0

  val urlCB:HttpDataCB = CFunctionPtr.fromFunction3(onURL)
  val onKeyCB:HttpDataCB = CFunctionPtr.fromFunction3(onHeaderKey)
  val onValueCB:HttpDataCB = CFunctionPtr.fromFunction3(onHeaderValue)
  val completeCB:HttpCB = CFunctionPtr.fromFunction1(onMessageComplete)

  val parserSettings = malloc(sizeof[ParserSettings]).cast[Ptr[ParserSettings]]
  http_parser_settings_init(parserSettings)
  !parserSettings._2 = urlCB
  !parserSettings._4 = onKeyCB
  !parserSettings._5 = onValueCB
  !parserSettings._8 = completeCB
  // We'll supply the definitions of these callbacks soon!

Adding an HTTP Server

  var router:Function1[Request[String],Route] = null

  def init(port:Int, f:Request[String] => Route):Unit = {
    EventLoop.addExtension(this)
    router = f
    val addr = malloc(64)
    check(uv_ip4_addr(c"0.0.0.0", 9999, addr),"uv_ip4_addr")
    val server = malloc(uv_handle_size(UV_TCP_T)).cast[TCPHandle]    
    check(uv_tcp_init(loop, server), "uv_tcp_init")
    check(uv_tcp_bind(server, addr, 0), "uv_tcp_bind")
    check(uv_listen(server, 4096, connectCB), "uv_listen")
    this.activeRequests = 1
  }

Adding an HTTP Server


  def onConnect(server:TCPHandle, status:Int):Unit = {
    val client = malloc(uv_handle_size(UV_TCP_T)).cast[TCPHandle]
    val id = serial
    serial += 1

    val state = malloc(sizeof[ConnectionState]).cast[Ptr[ConnectionState]]
    !state._1 = serial
    !state._2 = client
    http_parser_init(state._3,HTTP_REQUEST)
    !(state._3)._8 = state.cast[Ptr[Byte]]
    !(client.cast[Ptr[Ptr[Byte]]]) = state.cast[Ptr[Byte]]

    check(uv_tcp_init(loop, client), "uv_tcp_init (client)")
    check(uv_accept(server, client), "uv_accept")
    check(uv_read_start(client, allocCB, readCB), "uv_read_start")
  }

Adding an HTTP Server

  def onAlloc(handle:TCPHandle, size:CSize, buffer:Ptr[Buffer]):Unit = {
    val buf = stdlib.malloc(4096)
    buf(4095) = 0
    !buffer._1 = buf
    !buffer._2 = 4095
  }

  def onRead(handle:TCPHandle, size:CSize, buffer:Ptr[Buffer]):Unit = {
    val state_ptr = handle.cast[Ptr[Ptr[ConnectionState]]]
    val parser = (!state_ptr)._3
    val message_id = !(!state_ptr)._1
    println(s"conn $message_id: read message of size $size")

    if (size < 0) {
      uv_close(handle, null)
      stdlib.free(!buffer._1)
    } else {
      http_parser_execute(parser,parserSettings,!buffer._1,size)
      stdlib.free(!buffer._1)
    }
  }

Adding an HTTP Server

trait Parsing {
  import LibUV._,HttpParser._
  val requests:mutable.Map[Long,RequestState]

  def handleRequest(id:Long,handle:TCPHandle,request:RequestState):Unit

  type ConnectionState = CStruct3[Long,TCPHandle,Parser]

  val HTTP_REQUEST = 0
  val HTTP_RESPONSE = 1
  val HTTP_BOTH = 2

  def bytesToString(data:Ptr[Byte],len:Long):String = {
    val bytes = new Array[Byte](len.toInt)
    var c = 0
    while (c < len) {
      bytes(c) = !(data + c)
      c += 1
    }

    new String(bytes)
  }
  //...

Adding an HTTP Server

trait Parsing {
  import LibUV._,HttpParser._
  val requests:mutable.Map[Long,RequestState]

  def handleRequest(id:Long,handle:TCPHandle,request:RequestState):Unit

  type ConnectionState = CStruct3[Long,TCPHandle,Parser]

  val HTTP_REQUEST = 0
  val HTTP_RESPONSE = 1
  val HTTP_BOTH = 2

  def bytesToString(data:Ptr[Byte],len:Long):String = {
    val bytes = new Array[Byte](len.toInt)
    var c = 0
    while (c < len) {
      bytes(c) = !(data + c)
      c += 1
    }

    new String(bytes)
  }
  //...

Adding an HTTP Server

  def onURL(p:Ptr[Parser],data:CString,len:Long):Int = {
    val state = (!p._8).cast[Ptr[ConnectionState]]
    val message_id = !state._1
    val url = bytesToString(data,len)
    val m = !p._6
    val method = fromCString(http_method_str(m))
    requests(message_id) = RequestState(url,method)
    0
  }

  def onHeaderKey(p:Ptr[Parser],data:CString,len:Long):Int = {
    val state = (!p._8).cast[Ptr[ConnectionState]]
    val message_id = !state._1
    val request = requests(message_id)
    val k = bytesToString(data,len)
    request.lastHeader = k
    requests(message_id) = request
    0
  }

  def onHeaderValue(p:Ptr[Parser],data:CString,len:Long):Int = {
    val state = (!p._8).cast[Ptr[ConnectionState]]
    val message_id = !state._1
    val request = requests(message_id)
    val v = bytesToString(data,len)
    request.headerMap(request.lastHeader) = v
    requests(message_id) = request
    0
  }

Adding an HTTP Server

  def onMessageComplete(p:Ptr[Parser]):Int = {
    val state = (!p._8).cast[Ptr[ConnectionState]]
    val message_id = !state._1
    val tcpHandle = !state._2
    val request = requests(message_id)
    handleRequest(message_id,tcpHandle,request)
    0
  }


Handling Requests

What Goes Here?

Adding an HTTP Client

  • We'll use libcurl - c library that powers curl
  • great support for http, https, ftp, scp
  • big library with lots of features
  • complex async story

Adding an HTTP Client

  • simple libcurl bindings:
@extern 
object CurlBindings {
  // we model the curl handle as an opaque pointer of unknown size
  type Curl = Ptr[Byte]

  // allocate and initialize a curl handle 
  def easy_init():Curl = extern

  // set an option 
  def easy_setopt(handle: Curl, option: CInt, parameter: Any): CInt = extern
  // retrieve any of many named fields
  def easy_getinfo(handle: Curl, info: CInt, parameter: Any): CInt = extern

  // run the query
  def easy_perform(easy_handle: Curl): CInt = extern

  // clean up resources
  def easy_cleanup(handle: Curl): Unit = extern

  // flags and constants
  val WRITEDATA = 10001
  val URL = 10002
  val PORT = 10003
  // ...
}

Adding an HTTP Client

  • simple libcurl example:
        val url:String = "https://www.example.com"

        // initialize a curl handle
        val curl = easy_init()

        // convert the url to a zero-terminated CString
        // (CString is an alias for Ptr[Byte])
        // we need an implicit Zone allocator for this    
        val url_cstring = toCString(url) 
       
        // set the url on the handle
        easy_setopt(curl, URL, url_cstring)
        
        // set callbacks 
        easy_setopt(curl, HEADERCALLBACK, headerCB)
        easy_setopt(curl, HEADERDATA, req_id_ptr.cast[Ptr[Byte]])
        easy_setopt(curl, WRITECALLBACK, writeCB)
        easy_setopt(curl, WRITEDATA, req_id_ptr.cast[Ptr[Byte]])

        // run the query
        val res = easy_perform(curl) 

        // finish
        easy_cleanup(curl)

Adding an HTTP Client

  • we'll need callbacks and mutable state to actually see our data!
        val url:String = "https://www.example.com"

        // initialize a curl handle
        val curl = easy_init()

        // convert the url to a zero-terminated CString
        // (CString is an alias for Ptr[Byte])
        // we need an implicit Zone allocator for this    
        val url_cstring = toCString(url) 
       
        // set the url on the handle
        easy_setopt(curl, URL, url_cstring)
        
        // set callbacks 
        easy_setopt(curl, HEADERCALLBACK, headerCB)
        easy_setopt(curl, HEADERDATA, req_id_ptr.cast[Ptr[Byte]])
        easy_setopt(curl, WRITECALLBACK, writeCB)
        easy_setopt(curl, WRITEDATA, req_id_ptr.cast[Ptr[Byte]])

        // run the query
        val res = easy_perform(curl) 

        // finish
        easy_cleanup(curl)

Adding an HTTP Client

  • To integrate with libuv, we use curl's multi API
  • Allows multiple requests to advance simultaneously
  • Allows either libcurl or external event loop to drive IO
  • Adds a handful of new functions and callbacks:
  type MultiCurl = Ptr[Byte]

  def multi_init():MultiCurl = extern
  def multi_add_handle(multi:MultiCurl, easy:Curl):Int = extern
  def multi_setopt(multi:MultiCurl, option:CInt, parameter:Any):CInt = extern
  
  def multi_assign(
    multi:MultiCurl, 
    socket:Ptr[Byte], 
    socket_data:Ptr[Byte]):Int = extern
  
  def multi_socket_action(
    multi:MultiCurl, 
    socket:Ptr[Byte], 
    events:Int, 
    numhandles:Ptr[Int]):Int = extern

  def multi_info_read(multi:MultiCurl, message:Ptr[Int]):Ptr[CurlMessage] = extern
  def multi_cleanup(multi:MultiCurl):Int = extern

Adding an HTTP Client

  • multi_socket_action allows us to drive curl with libuv events
  • multi_assign lets us associate custom data with a socket
  • libuv's poll handles can detect readiness without doing IO
  • libuv's timer can be set and reset dynamically by curl
  def multi_assign(
    multi:MultiCurl, 
    socket:Ptr[Byte], 
    socket_data:Ptr[Byte]):Int = extern
  
  def multi_socket_action(
    multi:MultiCurl, 
    socket:Ptr[Byte], 
    events:Int, 
    numhandles:Ptr[Int]):Int = extern

Adding an HTTP Client

  • multi_socket_action allows us to drive curl with libuv events
  • multi_assign lets us associate custom data with a socket
  • libuv's poll handles can detect readiness without doing IO
  • libuv's timer can be set and reset dynamically by curl
  type SocketCallback = Function5[Curl,      // curl easy handle
                                  Ptr[Byte], // socket
                                  CurlAction,// socket state
                                  Ptr[Byte], // custom CurlMulti data pointer 
                                  Ptr[Byte], // custom per-socket data pointer
                                  CInt]      // returns Int

  type TimerCallback = Function3[MultiCurl,  // curl multi handle
                                 Long,       // time to set next timer period
                                 Ptr[Byte],  // custom CurlMulti data pointer
                                 CInt]       // returns Int

  type CurlAction = CInt
  val POLL_NONE:CurlAction = 0
  val POLL_IN:CurlAction = 1
  val POLL_OUT:CurlAction = 2
  val POLL_INOUT:CurlAction = 3
  val POLL_REMOVE:CurlAction = 4

Adding an HTTP Client

  1. curl creates one or more sockets.

  2. curl notifies us that it has created new sockets.

  3. we create pollhandles for libuv and start polling

  4. libuv sees that a socket is ready and invokes curl

  5. curl performs the appropriate transfer

  6. curl checks to see if the request is complete.

  7. If the request is complete, curl completes the request 

  8. When there are no more requests, we're done

Adding an HTTP Client

Request Setup

  val req_promises:mutable.Map[Long,Promise[ResponseState]] = mutable.HashMap.empty

  def get(url:CString, headers:Seq[String] = Seq.empty)
         (implicit ec:ExecutionContext):Future[ResponseState] = {
    req_count += 1
    activeRequests += 1
    val req_id = req_count
    val promise = Promise[ResponseState]()
    req_promises(req_id) = promise

    CurlInternals.beginRequest(req_id, url, headers)

    promise.future
  }

  def beginRequest(reqId:Long, url:CString, headers:Seq[String]):Unit = {
    val curlHandle = easy_init()
    val req_id_ptr = malloc(sizeof[Long]).cast[Ptr[Long]]
    !req_id_ptr = reqId
    requests(reqId) = ResponseState()

    easy_setopt(curlHandle, URL, url)
    easy_setopt(curlHandle, WRITECALLBACK, writeCB)
    easy_setopt(curlHandle, WRITEDATA, req_id_ptr.cast[Ptr[Byte]])
    easy_setopt(curlHandle, HEADERCALLBACK, headerCB)
    easy_setopt(curlHandle, HEADERDATA, req_id_ptr.cast[Ptr[Byte]])
    easy_setopt(curlHandle, PRIVATEDATA, req_id_ptr.cast[Ptr[Byte]])

    multi_add_handle(multi, curlHandle)
  }

Adding an HTTP Client

  // libuv provides helper functions to get the size of opaque structures
  val timer_size = uv_handle_size(UV_TIMER_T)
  // allocate a TimerHandle (really just a Ptr[Byte])
  val timer_handle:TimerHandle = malloc(timer_size)

  def set_timeout(curl:MultiCurl, timeout_ms:Long, data:Ptr[Byte]):Int = {
    val time = if (timeout_ms < 1) {
      // set timeout to minimum of 1, 0 causes problems
      1
    } else {
      timeout_ms
    }

    // we have a single global timer - this can set or reset its period
    uv_timer_start(timer_handle, timerCB, time, 0), "uv_timer_start")

    // complete any requests that have finished    
    cleanup_requests()
    0
  }

  // called by libuv when the timer fires
  def on_timeout(handle:TimerHandle):Unit = {
    val running_handles = stackalloc[Int]
    multi_socket_action(multi,-1.cast[Ptr[Byte]],0,running_handles)
    println(s"on_timer fired, ${!running_handles} sockets running")
  }
  val timerCB = CFunctionPtr.fromFunction1(on_timeout)

Adding an HTTP Client

def state_change(curl:Curl, socket:Ptr[Byte], action:Int, data:Ptr[Byte], 
                 socket_data:Ptr[Byte]):Int = {
    val pollHandle = if (socket_data == null) {
      val newHandle = malloc(uv_handle_size(UV_POLL_T)).cast[Ptr[Ptr[Byte]]
      !newHandle = socket
      uv_poll_init_socket(loop, newHandle, socket)
      multi_assign(multi, socket, newHandle.cast[Ptr[Byte]])
      newHandle
    } else {
      socket_data.cast[Ptr[Ptr[Byte]]]
    }

    val events = action match {
      case POLL_NONE => None
      case POLL_IN => Some(UV_READABLE)
      case POLL_OUT => Some(UV_WRITABLE)
      case POLL_INOUT => Some(UV_READABLE | UV_WRITABLE)
      case POLL_REMOVE => None
    }

    // update or stop the poll handle as needed
    events match {
      case Some(ev) =>
        uv_poll_start(pollHandle, ev, readyCB)
      case None =>
        uv_poll_stop(pollHandle)
        cleanup_requests()
    }
    0
  }

Adding an HTTP Client

  def on_socket_ready(pollHandle:PollHandle, status:Int, events:Int):Unit = {
    println(s"ready_for_curl fired with status ${status} and events ${events}")
    val socket = !(pollHandle.cast[Ptr[Ptr[Byte]]])
    val actions = (events & 1) | (events & 2) // Whoa, nelly!
    val running_handles = stackalloc[Int]
    val result = multi_socket_action(multi, socket, actions, running_handles)
    println("multi_socket_action",result)
  }
  val readyCB = CFunctionPtr.fromFunction3(on_socket_ready)

Adding an HTTP Client

  def cleanup_requests():Unit = {
    val messages = stackalloc[Int]
    val privateDataPtr= stackalloc[Ptr[Long]]
    var message:Ptr[CurlMessage] = multi_info_read(multi,messages)
    while (message != null) {
      val handle:Curl = !message._2
      easy_getinfo(handle, GET_PRIVATEDATA, privateDataPtr)
      val privateData = !privateDataPtr
      val reqId = !privateData
      val reqData = requests.remove(reqId).get
      Curl.complete_request(reqId,reqData)
      message = multi_info_read(multi,messages)
    }
  }

  def complete_request(reqId:Long, data:ResponseState):Unit = {
    val reqId = !request._4
    activeRequests -= 1
    println(s"completing reqId ${reqId}")
    val promise = Curl.req_promises.remove(reqId).get
    promise.success(data)
  }

Whew!

  • We've covered a LOT of ground
  • How do we go from slides to a sustainable project?
  • How do we build an ecosystem with splintering SN?

An Announcement

  • native-loop is moving into the Scala-Native GH org
  • Will live under scalanative.loop namespace
  • Artifacts available for SN 0.4M1 now!

 

Setting a Course

  • Improve support in SN for user-supplied event loop
  • STTP has great (blocking) curl/native support
  • Plan to spin out server API
  • Get feedback on streaming IO API

Building a Community

  • All of this needs contributors to be sustainable
  • There are a LOT of low-hanging fruit out there
  • Weekend-scale side projects can have a big impact!
  • Huge shout out to Scala Native's contributors
  • Get involved!

Backup Copy of Fast, Simple Concurrency with Scala Native

By Richard Whaling

Backup Copy of Fast, Simple Concurrency with Scala Native

  • 404