├── project ├── build.properties └── plugins.sbt ├── foo.c ├── src ├── main │ └── scala │ │ └── webserver │ │ ├── package.scala │ │ ├── Main.scala │ │ ├── Http.scala │ │ ├── uv.scala │ │ └── Server.scala └── test │ └── scala │ └── webserver │ └── HttpTest.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.1.0 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.6") 2 | -------------------------------------------------------------------------------- /foo.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | static uv_buf_t buf; 6 | static uv_tcp_t tcp; 7 | static uv_write_t w; 8 | 9 | int main(int argc, char **argv) { 10 | 11 | printf("Size of uv_buf_t = %lu\n", sizeof(buf)); 12 | printf("Size of uv_tcp_t = %lu\n", sizeof(tcp)); 13 | printf("Size of uv_write_t = %lu\n", sizeof(w)); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/webserver/package.scala: -------------------------------------------------------------------------------- 1 | 2 | import scala.scalanative.native._ 3 | 4 | package object webserver { 5 | 6 | def bailOnError(f: => CInt): Unit = { 7 | val result = f 8 | if (result < 0) { 9 | val errorName = uv.getErrorName(result) 10 | println(s"Failed: $result ${fromCString(errorName)}") 11 | System.exit(1) 12 | } 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala Native example - webserver 2 | 3 | Companion code for this blogpost: http://tech.ovoenergy.com/scala-native-webserver/ 4 | 5 | This is a toy HTTP erver that simply responds to any request with some info about that request. 6 | 7 | It is implemented using [libuv](http://libuv.org/) for event-driven I/O. 8 | 9 | ## To install dependencies 10 | 11 | On a Mac: 12 | 13 | ``` 14 | $ brew install llvm bdw-gc re2 libuv 15 | ``` 16 | 17 | For other platforms, see the official Scala Native docs. 18 | 19 | ## To run 20 | 21 | ``` 22 | $ sbt run 23 | ``` 24 | 25 | ## To run the unit tests 26 | 27 | ``` 28 | $ sbt test 29 | ``` 30 | -------------------------------------------------------------------------------- /src/test/scala/webserver/HttpTest.scala: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import utest._ 4 | import webserver.Http.{Header, StartLine} 5 | 6 | object HttpTest extends TestSuite { 7 | 8 | val tests = Tests { 9 | 'parseValidHttpRequest - { 10 | val raw = "GET /foo/bar?wow=yeah HTTP/1.1\r\nHost: localhost:7000\r\nUser-Agent: curl/7.54.0\r\nAccept: */*\r\n\r\n" 11 | val parsed = Http.parseRequest(raw).right.get 12 | 13 | assert(parsed.startLine == StartLine("GET", "/foo/bar?wow=yeah", "1.1")) 14 | assert(parsed.headers.toList == List( 15 | Header("Host", "localhost:7000"), 16 | Header("User-Agent", "curl/7.54.0"), 17 | Header("Accept", "*/*") 18 | )) 19 | } 20 | 21 | 'parseInvalidHttpRequest - { 22 | assert(Http.parseRequest("yolo").isLeft) 23 | } 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/webserver/Main.scala: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import webserver.uv.TcpHandle 4 | 5 | import scala.scalanative.native._ 6 | import scala.scalanative.posix.netinet.in.sockaddr_in 7 | 8 | object Main extends App { 9 | 10 | case class ServerConfig(host: String = "127.0.0.1", port: Int = 7000) 11 | 12 | private val parser = new scopt.OptionParser[ServerConfig]("hello-scala-native") { 13 | opt[String]('h', "host").action((x, c) => 14 | c.copy(host = x)).text("The host on which to bind") 15 | 16 | opt[Int]('p', "port").action((x, c) => 17 | c.copy(port = x)).text("The port on which to bind") 18 | } 19 | 20 | private val DefaultRunMode = 0 21 | 22 | parser.parse(args, ServerConfig()) match { 23 | case Some(serverConfig) => runServer(serverConfig) 24 | case None => System.exit(1) 25 | } 26 | 27 | private def runServer(config: ServerConfig): Unit = Zone { implicit z => 28 | val socketAddress = alloc[sockaddr_in] 29 | uv.ipv4Addr(toCString(config.host), config.port, socketAddress) 30 | 31 | val loop = uv.createDefaultLoop() 32 | println("Created event loop") 33 | 34 | val tcpHandle = alloc[TcpHandle] 35 | bailOnError(uv.tcpInit(loop, tcpHandle)) 36 | println("Initialised TCP handle") 37 | 38 | bailOnError(uv.tcpBind(tcpHandle, socketAddress, UInt.MinValue)) 39 | println(s"Bound server to ${config.host}:${config.port}") 40 | 41 | bailOnError(uv.listen(tcpHandle, connectionBacklog = 128, Server.onTcpConnection)) 42 | println("Started listening") 43 | 44 | bailOnError(uv.run(loop, DefaultRunMode)) 45 | println("Started event loop") 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/webserver/Http.scala: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import fastparse.all._ 4 | import fastparse.core.Parsed.{Failure, Success} 5 | 6 | object Http { 7 | 8 | case class StartLine(httpMethod: String, requestTarget: String, httpVersion: String) 9 | case class Header(key: String, value: String) 10 | case class HttpRequest(startLine: StartLine, headers: Seq[Header]) 11 | 12 | /* 13 | Example request: 14 | 15 | GET / HTTP/1.1 16 | Host: localhost:7000 17 | User-Agent: curl/7.54.0 18 | Accept: foo/bar 19 | 20 | */ 21 | 22 | /* 23 | https://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 24 | 25 | "HTTP/1.1 defines the sequence CR LF as the end-of-line marker for all protocol elements except the entity-body" 26 | */ 27 | 28 | private val httpMethod = P ( CharPred(CharPredicates.isUpper).rep(min = 1).! ) 29 | private val requestTarget = P ( CharsWhile(_ != ' ').! ) 30 | private val httpVersion = P ( "HTTP/" ~ CharsWhileIn(List('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.')).! ) 31 | 32 | private val startLine: Parser[StartLine] = 33 | P( httpMethod ~ " " ~ requestTarget ~ " " ~ httpVersion ~ "\r\n" ) map StartLine.tupled 34 | 35 | private val header: Parser[Header] = 36 | P ( CharsWhile(_ != ':').! ~ ": " ~ CharsWhile(_ != '\r').! ~ "\r\n" ) map Header.tupled 37 | 38 | private val startLineAndHeaders = P ( startLine ~ header.rep ) map HttpRequest.tupled 39 | 40 | def parseRequest(raw: String): Either[String, HttpRequest] = startLineAndHeaders.parse(raw) match { 41 | case Success(parsedRequest, _) => Right(parsedRequest) 42 | case f: Failure[_, _] => Left(f.toString) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/webserver/uv.scala: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import scala.scalanative.native._ 4 | import scala.scalanative.native.Nat._ 5 | import scala.scalanative.posix.netinet.in.sockaddr_in 6 | 7 | @link("uv") 8 | @extern 9 | object uv { 10 | 11 | type Loop = Unit // we don't care what the uv_loop_t type is, as we just need to pass around a pointer to it 12 | 13 | type Buffer = CStruct2[ 14 | CString, // char* base; 15 | CSize // size_t len; 16 | ] 17 | 18 | type _100 = Digit[_1, Digit[_0, _0]] 19 | type TcpHandle = CStruct12[ 20 | Ptr[Byte], // void* data; 21 | Ptr[Loop], // uv_loop_t* loop; 22 | CInt, // uv_handle_type type; 23 | CFunctionPtr1[Ptr[Byte], Unit], // uv_close_cb close_cb; (Ptr[Byte] is actually Ptr[TcpHandle] but we can't have recursive types) 24 | CArray[Ptr[Unit], _2], // void* handle_queue[2]; 25 | CArray[Ptr[Unit], _4], // union { int fd; void* reserved[4]; } u; 26 | Ptr[Byte], // uv_handle_t* next_closing; (Ptr[Byte] is actually Ptr[TcpHandle]) 27 | UInt, // unsigned int flags; 28 | CSize, // size_t write_queue_size; 29 | CFunctionPtr3[Ptr[Byte], CSize, Ptr[Unit], Unit], // uv_alloc_cb alloc_cb; (Ptr[Byte] is actually Ptr[TcpHandle]) 30 | CFunctionPtr3[Ptr[Byte], CSSize, Ptr[Unit], Unit], // uv_read_cb read_cb; (Ptr[Byte] is actually Ptr[TcpHandle]) 31 | CArray[Byte, _100] // add enough padding to cover a bunch of private fields 32 | ] 33 | 34 | type Write = CStruct12[ 35 | Ptr[Byte], // void* data; 36 | CInt, // uv_req_type type; 37 | CArray[Ptr[Unit], _6], // void* reserved[6]; 38 | CFunctionPtr2[Ptr[Byte], CInt, Unit], // uv_write_cb cb; (Ptr[Byte] is actually Ptr[Write]) 39 | Ptr[TcpHandle], // uv_stream_t* send_handle; 40 | Ptr[TcpHandle], // uv_stream_t* handle; 41 | CArray[Ptr[Unit], _2], // void* queue[2]; 42 | UInt, // unsigned int write_index; 43 | Ptr[Buffer], // uv_buf_t* bufs; 44 | UInt, // unsigned int nbufs; 45 | CInt, // int error; 46 | CArray[Buffer, _4] // uv_buf_t bufsml[4]; 47 | ] 48 | 49 | @name("uv_ip4_addr") 50 | def ipv4Addr(ip: CString, port: CInt, addr: Ptr[sockaddr_in]): CInt = extern 51 | 52 | @name("uv_default_loop") 53 | def createDefaultLoop(): Ptr[Loop] = extern 54 | 55 | @name("uv_tcp_init") 56 | def tcpInit(loop: Ptr[Loop], handle: Ptr[TcpHandle]): CInt = extern 57 | 58 | @name("uv_tcp_bind") 59 | def tcpBind(handle: Ptr[TcpHandle], socketAddress: Ptr[sockaddr_in], flags: CUnsignedInt): CInt = extern 60 | 61 | @name("uv_listen") 62 | def listen(handle: Ptr[TcpHandle], connectionBacklog: CInt, onTcpConnection: CFunctionPtr2[Ptr[TcpHandle], CInt, Unit]): CInt = extern 63 | 64 | @name("uv_run") 65 | def run(loop: Ptr[Loop], runMode: CInt): CInt = extern 66 | 67 | @name("uv_accept") 68 | def accept(handle: Ptr[TcpHandle], clientHandle: Ptr[TcpHandle]): CInt = extern 69 | 70 | @name("uv_read_start") 71 | def readStart(clientHandle: Ptr[TcpHandle], allocateBuffer: CFunctionPtr3[Ptr[TcpHandle], CSize, Ptr[Buffer], Unit], onRead: CFunctionPtr3[Ptr[TcpHandle], CSSize, Ptr[Buffer], Unit]): CInt = extern 72 | 73 | @name("uv_write") 74 | def write(req: Ptr[Write], clientHandle: Ptr[TcpHandle], buffers: Ptr[Buffer], numberOfBuffers: UInt, onWritten: CFunctionPtr2[Ptr[Write], CInt, Unit]): CInt = extern 75 | 76 | @name("uv_close") 77 | def close(clientHandle: Ptr[TcpHandle], callback: CFunctionPtr1[Ptr[TcpHandle], Unit]): CInt = extern 78 | 79 | @name("uv_err_name") 80 | def getErrorName(errorCode: CInt): CString = extern 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/scala/webserver/Server.scala: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import java.nio.charset.{Charset, StandardCharsets} 4 | 5 | import webserver.uv._ 6 | 7 | import scala.scalanative.native._ 8 | 9 | object Server { 10 | 11 | // defining this as an extern in `uv` object didn't work 12 | private val UV_EOF: CSSize = -4095 13 | 14 | def _onTcpConnection(tcpHandle: Ptr[TcpHandle], status: CInt): Unit = { 15 | println("Got a connection!") 16 | val loop: Ptr[Loop] = (!tcpHandle._2).cast[Ptr[Loop]] 17 | 18 | println("Allocating a client handle for the request") 19 | val clientTcpHandle = stdlib.malloc(sizeof[TcpHandle]).cast[Ptr[TcpHandle]] 20 | 21 | println("Initialising client handle") 22 | bailOnError(uv.tcpInit(loop, clientTcpHandle)) 23 | 24 | println("Accepting connection") 25 | bailOnError(uv.accept(tcpHandle, clientTcpHandle)) 26 | 27 | println("Reading request") 28 | bailOnError(uv.readStart(clientTcpHandle, allocateRequestBuffer, onRead)) 29 | } 30 | 31 | private def _onClose(clientHandle: Ptr[TcpHandle]): Unit = { 32 | println("Freeing the client handle for the request") 33 | stdlib.free(clientHandle.cast[Ptr[Byte]]) 34 | } 35 | 36 | private def _allocateRequestBuffer(clientHandle: Ptr[TcpHandle], suggestedSize: CSize, buffer: Ptr[Buffer]): Unit = { 37 | println(s"Allocating request read buffer of size $suggestedSize") 38 | !buffer._1 = stdlib.malloc(suggestedSize) 39 | !buffer._2 = suggestedSize 40 | } 41 | 42 | private def _onRead(clientHandle: Ptr[TcpHandle], bytesRead: CSSize, buffer: Ptr[Buffer]): Unit = { 43 | bytesRead match { 44 | case UV_EOF => 45 | println("Finished reading the request") 46 | 47 | println(s"Freeing request read buffer of size ${!buffer._2}") 48 | stdlib.free(!buffer._1) 49 | case n if n < 0 => 50 | println(s"Error reading request: ${uv.getErrorName(n.toInt)}") 51 | uv.close(clientHandle, onClose) 52 | 53 | println(s"Freeing request read buffer of size ${!buffer._2}") 54 | stdlib.free(!buffer._1) 55 | case n => 56 | println(s"Read $n bytes of the request") 57 | 58 | val requestAsString: String = readRequestAsString(n, buffer) 59 | 60 | println(s"Freeing request read buffer of size ${!buffer._2}") 61 | stdlib.free(!buffer._1) 62 | 63 | /* 64 | The onRead callback can be called multiple times for a single request, 65 | but for simplicity we assume the request is < 64k and is thus read in a single chunk. 66 | As soon as we read the first chunk, we parse it as an HTTP request and write the response. 67 | */ 68 | parseAndRespond(clientHandle, requestAsString) 69 | } 70 | } 71 | 72 | private def readRequestAsString(bytesRead: CSSize, buffer: Ptr[Buffer]): String = { 73 | val buf: Ptr[CChar] = !buffer._1 74 | 75 | val bytes = new Array[Byte](bytesRead.toInt) 76 | var c = 0 77 | while (c < bytesRead) { 78 | bytes(c) = !(buf + c) 79 | c += 1 80 | } 81 | new String(bytes, Charset.defaultCharset()) 82 | } 83 | 84 | private val ErrorResponse = 85 | s"""HTTP/1.1 400 Bad Request\r 86 | |Connection: close\r 87 | |Content-Type: text/plain; charset=utf8\r 88 | |Content-Length: 35\r 89 | |\r 90 | |What on earth did you just send me?!""".stripMargin 91 | 92 | private def parseAndRespond(clientHandle: Ptr[TcpHandle], rawRequest: String): Unit = { 93 | val responseText = Http.parseRequest(rawRequest) match { 94 | case Right(parsedRequest) => 95 | val entity = 96 | s""" 97 | | 98 | | 99 | |

Thanks for your request!

100 | |

Here's what you sent me.

101 | |
    102 | |
  • Method = ${parsedRequest.startLine.httpMethod}
  • 103 | |
  • Target = ${parsedRequest.startLine.requestTarget}
  • 104 | |
  • HTTP version = ${parsedRequest.startLine.httpVersion}
  • 105 | |
  • Headers: 106 | |
      107 | | ${parsedRequest.headers.map(h => s"
    • ${h.key}: ${h.value}
    • ").mkString} 108 | |
    109 | |
  • 110 | |
111 | | 112 | | 113 | |""".stripMargin 114 | 115 | s"""HTTP/1.1 200 OK\r 116 | |Connection: close\r 117 | |Content-Type: text/html; charset=utf8\r 118 | |Content-Length: ${entity.length}\r 119 | |\r 120 | |$entity""".stripMargin 121 | 122 | case Left(_) => 123 | ErrorResponse 124 | } 125 | 126 | writeResponse(clientHandle, responseText.getBytes(StandardCharsets.UTF_8)) 127 | } 128 | 129 | private def writeResponse(clientHandle: Ptr[TcpHandle], responseBytes: Array[Byte]): Unit = { 130 | println(s"Allocating a buffer for the response (${responseBytes.length} bytes)") 131 | val responseBuffer = stdlib.malloc(responseBytes.length) 132 | 133 | var c = 0 134 | while (c < responseBytes.length) { 135 | responseBuffer(c) = responseBytes(c) 136 | c += 1 137 | } 138 | 139 | println("Allocating a wrapper for the response buffer") 140 | val buffer = stdlib.malloc(sizeof[Buffer]).cast[Ptr[Buffer]] 141 | 142 | !buffer._1 = responseBuffer 143 | !buffer._2 = responseBytes.length 144 | 145 | println("Allocating a Write for the response") 146 | val req = stdlib.malloc(sizeof[Write]).cast[Ptr[Write]] 147 | 148 | // Store a pointer to the response buffer in the 'data' field to make it easy to free it later 149 | !req._1 = buffer.cast[Ptr[Byte]] 150 | 151 | bailOnError(uv.write(req, clientHandle, buffer, 1.toUInt, onWritten)) 152 | } 153 | 154 | private def _onWritten(write: Ptr[Write], status: CInt): Unit = { 155 | println(s"Write succeeded: ${status >= 0}") 156 | 157 | val buffer = (!write._1).cast[Ptr[Buffer]] 158 | 159 | println(s"Freeing the response buffer (${(!buffer._2).cast[CSize]} bytes)") 160 | stdlib.free(!buffer._1) 161 | 162 | println("Freeing the wrapper for the response buffer") 163 | stdlib.free(buffer.cast[Ptr[Byte]]) 164 | 165 | val clientHandle = (!write._6).cast[Ptr[TcpHandle]] 166 | uv.close(clientHandle, onClose) 167 | 168 | println("Freeing the Write for the response") 169 | stdlib.free(write.cast[Ptr[Byte]]) 170 | } 171 | 172 | val onTcpConnection: CFunctionPtr2[Ptr[TcpHandle], CInt, Unit] = CFunctionPtr.fromFunction2(_onTcpConnection) 173 | 174 | private val onClose = CFunctionPtr.fromFunction1(_onClose) 175 | private val allocateRequestBuffer = CFunctionPtr.fromFunction3(_allocateRequestBuffer) 176 | private val onRead = CFunctionPtr.fromFunction3(_onRead) 177 | private val onWritten = CFunctionPtr.fromFunction2(_onWritten) 178 | 179 | } 180 | --------------------------------------------------------------------------------