diff options
author | Jakob Odersky <jakob@inpher.io> | 2019-11-26 12:57:50 -0500 |
---|---|---|
committer | Jakob Odersky <jakob@inpher.io> | 2019-11-26 13:40:36 -0500 |
commit | 62b5a6a24a8824f996ad82b7bf0b0e5f1210326a (patch) | |
tree | b32dd1fd4e3b5e3ae3e449ff7c051daa31e28a28 | |
parent | f38dd59d93f56213a8400841c7ebb0a7202144a7 (diff) | |
download | scala-tutorial-62b5a6a24a8824f996ad82b7bf0b0e5f1210326a.tar.gz scala-tutorial-62b5a6a24a8824f996ad82b7bf0b0e5f1210326a.tar.bz2 scala-tutorial-62b5a6a24a8824f996ad82b7bf0b0e5f1210326a.zip |
Add CLI based on scalanative
-rw-r--r-- | build.sc | 7 | ||||
-rw-r--r-- | cli/src/Main.scala | 66 | ||||
-rw-r--r-- | cli/src/http/ArrayUtils.scala | 27 | ||||
-rw-r--r-- | cli/src/http/CurlBackend.scala | 227 | ||||
-rw-r--r-- | cli/src/http/Request.scala | 8 | ||||
-rw-r--r-- | cli/src/http/Response.scala | 7 | ||||
-rw-r--r-- | cli/src/http/curl.scala | 85 |
7 files changed, 426 insertions, 1 deletions
@@ -1,4 +1,4 @@ -import mill._, scalalib._, scalajslib._ +import mill._, scalalib._, scalajslib._, scalanativelib._ trait Shared extends ScalaModule { def sharedSources = T.sources(build.millSourcePath / "shared") @@ -46,3 +46,8 @@ object webapp extends ScalaJSModule with Shared { } } + +object cli extends ScalaNativeModule { + def scalaVersion = "2.11.12" // scala native does not support newer versions yet + def scalaNativeVersion = "0.3.8" +} diff --git a/cli/src/Main.scala b/cli/src/Main.scala new file mode 100644 index 0000000..e7cb5aa --- /dev/null +++ b/cli/src/Main.scala @@ -0,0 +1,66 @@ + +import java.time.Instant +import scala.collection.mutable +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.util.control.NonFatal + +object Main extends App { + + val usage = """Usage: cli [--author=<author>] content""" + + def error(message: String) = { + System.err.println(message) + System.err.println(usage) + sys.exit(1) + } + + // process arguments + var author: Option[String] = None + val content = new mutable.StringBuilder + + val it = args.iterator + while(it.hasNext) { + val arg = it.next() + if (arg == "--author") { + if (it.hasNext) { + author = Some(it.next()) + } else { + error("expected author") + } + } else if (arg.startsWith("--")) { + error("invalid option") + } else { + content ++= arg + } + } + + // we could use the shared message model if we used a serialization library + // that also supports scala native, such as https://github.com/jodersky/spray-json/ + // + // val message = Message(author, Instant.now.getEpochSecond, content) + + val message = s"""{"message":{ + "author": "${author.getOrElse(sys.env("USER"))}", + "date" : ${Instant.now().toEpochMilli / 1000}, + "content": "$content" + }}""" + val req = http.Request("POST", "http://localhost:8080", body = message.getBytes) + + try { + Await.result(http.CurlBackend.send(req), 10.seconds) match { + case resp if 200 <= resp.statusCode && resp.statusCode <= 300 => + sys.exit(0) + case resp => + System.err.println( + s"Bad response code while posting message ${resp.statusCode}." + ) + sys.exit(1) + } + } catch { + case NonFatal(e) => + System.err.println(e.getMessage) + sys.exit(1) + } + +} diff --git a/cli/src/http/ArrayUtils.scala b/cli/src/http/ArrayUtils.scala new file mode 100644 index 0000000..80944d4 --- /dev/null +++ b/cli/src/http/ArrayUtils.scala @@ -0,0 +1,27 @@ +package http + +import scala.scalanative.native._ + +object ArrayUtils { + + def toBuffer(array: Array[Byte])(implicit z: Zone): Ptr[Byte] = { + val buffer = z.alloc(array.size) + var i = 0 + while (i < array.size) { + buffer(i) = array(i) + i += 1 + } + buffer + } + + def toArray(buffer: Ptr[Byte], size: CSize): Array[Byte] = { + val array = new Array[Byte](size.toInt) + var i = 0 + while (i < array.size) { + array(i) = buffer(i) + i += 1 + } + array + } + +} diff --git a/cli/src/http/CurlBackend.scala b/cli/src/http/CurlBackend.scala new file mode 100644 index 0000000..472d750 --- /dev/null +++ b/cli/src/http/CurlBackend.scala @@ -0,0 +1,227 @@ +package http + +import curl._ +import curlh._ + +import scala.collection.{Map, mutable} +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.Future +import scala.scalanative.native._ +import scala.util.{Failure, Success, Try} + +object CurlBackend { + + type Chunk = CStruct4[Ptr[CStruct0], Ptr[CStruct0], CSize, Ptr[Byte]] + implicit class ChunksOps(val self: Ptr[Chunk]) extends AnyVal { + @inline def prev: Ptr[Chunk] = (!self._1).cast[Ptr[Chunk]] + @inline def prev_=(other: Ptr[Chunk]): Unit = + !(self._1) = other.cast[Ptr[CStruct0]] + @inline def next: Ptr[Chunk] = (!self._2).cast[Ptr[Chunk]] + @inline def next_=(other: Ptr[Chunk]): Unit = + !(self._2) = other.cast[Ptr[CStruct0]] + @inline def size: CSize = !(self._3) + @inline def size_=(value: CSize): Unit = !(self._3) = value + @inline def buffer: Ptr[Byte] = !(self._4) + @inline def buffer_=(value: Ptr[Byte]): Unit = !(self._4) = value + } + + object Chunk { + + @inline def NullPtr[T] = 0.cast[Ptr[T]] + + def allocHead() = allocAppend(0, NullPtr[Chunk]) + + def allocAppend( + size: CSize, + head: Ptr[Chunk] = NullPtr[Chunk] + ): Ptr[Chunk] = { + val chunk: Ptr[Chunk] = stdlib.malloc(sizeof[Chunk]).cast[Ptr[Chunk]] + if (chunk == NullPtr[Chunk]) return NullPtr[Chunk] + + chunk.buffer = stdlib.malloc(size) + + if (chunk.buffer == NullPtr[Chunk] && size != 0) { + stdlib.free(chunk.cast[Ptr[Byte]]) + return NullPtr[Chunk] + } + chunk.size = size + + if (head == NullPtr[Chunk]) { // this will be the head + chunk.next = chunk + chunk.prev = chunk + } else { + val last = head.prev + last.next = chunk + chunk.prev = last + head.prev = chunk + chunk.next = head + } + chunk + } + + def freeAll(head: Ptr[Chunk]): Unit = { + var chunk: Ptr[Chunk] = head + do { + val next = chunk.next + stdlib.free(chunk.buffer) + stdlib.free(chunk.cast[Ptr[Byte]]) + chunk = next + } while (chunk != head) + } + + def toArray(head: Ptr[Chunk]): Array[Byte] = { + val buffer = new ArrayBuffer[Byte]() + var chunk = head + do { + val next = chunk.next + var i = 0L + while (i < next.size) { + buffer += next.buffer(i) + i += 1 + } + chunk = next + } while (chunk != head) + buffer.toArray + } + + def traverse(head: Ptr[Chunk])(fct: Array[Byte] => Unit) = { + var chunk = head + do { + val next = chunk.next + val buffer = new ArrayBuffer[Byte]() + var i = 0L + while (i < next.size) { + buffer += next.buffer(i) + i += 1 + } + chunk = next + fct(buffer.toArray) + } while (chunk != head) + } + + } + + private def receive( + data: Ptr[Byte], + size: CSize, + nmemb: CSize, + userdata: Ptr[Byte] + ): CSize = { + val head = userdata.cast[Ptr[Chunk]] + val length = size * nmemb + val chunk = Chunk.allocAppend(length, head) + string.memcpy(chunk.buffer, data, chunk.size) + chunk.size + } + private val receivePtr: WriteFunction = CFunctionPtr.fromFunction4(receive) + + private def chain[A](success: A)(calls: (() => A)*) = { + var result: A = success + for (c <- calls if result == success) { + result = c() + } + result + } + + private def request(request: Request)(implicit z: Zone): Try[Response] = { + val curl: CURL = curl_easy_init() + if (curl != null) { + val errorBuffer = stackalloc[Byte](CURL_ERROR_SIZE) + !errorBuffer = 0 + val requestHeaders = stackalloc[curl_slist](1) + !requestHeaders = 0.cast[curl_slist] + + val responseChunks = Chunk.allocHead() + val responseHeaderChunks = Chunk.allocHead() + + val curlResult = chain(CURLcode.CURL_OK)( + () => + curl_easy_setopt(curl, CURLoption.CURLOPT_ERRORBUFFER, errorBuffer), + () => + curl_easy_setopt( + curl, + CURLoption.CURLOPT_CUSTOMREQUEST, + toCString(request.method) + ), + () => + curl_easy_setopt( + curl, + CURLoption.CURLOPT_URL, + toCString(request.url) + ), + () => { + val buffer = ArrayUtils.toBuffer(request.body) + curl_easy_setopt(curl, CURLoption.CURLOPT_POSTFIELDS, buffer) + curl_easy_setopt( + curl, + CURLoption.CURLOPT_POSTFIELDSIZE, + request.body.size + ) + }, + () => { + for ((k, v) <- request.headers) { + !requestHeaders = + curl_slist_append(!requestHeaders, toCString(s"$k:$v")) + } + curl_easy_setopt(curl, CURLoption.CURLOPT_HTTPHEADER, !requestHeaders) + }, + () => + curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEFUNCTION, receivePtr), + () => + curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEDATA, responseChunks), + () => + curl_easy_setopt( + curl, + CURLoption.CURLOPT_HEADERDATA, + responseHeaderChunks + ), + () => curl_easy_perform(curl) + ) + + val result = curlResult match { + case CURLcode.CURL_OK => + val responseCode: Ptr[Long] = stackalloc[Long](1) + curl_easy_getinfo(curl, CURLINFO.CURLINFO_RESPONSE_CODE, responseCode) + + val responseHeaders = mutable.HashMap.empty[String, String] + Chunk.traverse(responseHeaderChunks) { headerChunk => + val line = new String(headerChunk, "utf-8").trim + if (line.contains(":")) { + val parts = line.split(":", 2) + responseHeaders += parts(0) -> parts(1) + } + } + + Success( + Response( + statusCode = (!responseCode).toInt, + headers = responseHeaders.toMap, + body = Chunk.toArray(responseChunks) + ) + ) + + case code => + val message = curl_easy_strerror(curl, code) + Failure( + new RuntimeException( + s"${fromCString(errorBuffer)} (curl exit status $code)" + ) + ) + } + Chunk.freeAll(responseChunks) + Chunk.freeAll(responseHeaderChunks) + curl_slist_free_all(!requestHeaders) + curl_easy_cleanup(curl) + result + } else { + Failure(new RuntimeException(s"curl failed to initialize")) + } + } + + def curlVersion = fromCString(curl_version()) + + def send(req: Request): Future[Response] = Zone { implicit z => + Future.fromTry(CurlBackend.request(req)) + } + +} diff --git a/cli/src/http/Request.scala b/cli/src/http/Request.scala new file mode 100644 index 0000000..c1544f8 --- /dev/null +++ b/cli/src/http/Request.scala @@ -0,0 +1,8 @@ +package http + +case class Request( + method: String, + url: String, + headers: Map[String, String] = Map.empty, + body: Array[Byte] = Array.empty +) diff --git a/cli/src/http/Response.scala b/cli/src/http/Response.scala new file mode 100644 index 0000000..d271daa --- /dev/null +++ b/cli/src/http/Response.scala @@ -0,0 +1,7 @@ +package http + +case class Response( + statusCode: Int, + headers: Map[String, String], + body: Array[Byte] +) diff --git a/cli/src/http/curl.scala b/cli/src/http/curl.scala new file mode 100644 index 0000000..9107353 --- /dev/null +++ b/cli/src/http/curl.scala @@ -0,0 +1,85 @@ +package http + +import scala.scalanative.native._ + +object curlh { + final val CURL_ERROR_SIZE = 256 + + type CURL = Ptr[CStruct0] + + /* + * #define CURLOPTTYPE_LONG 0 + * #define CURLOPTTYPE_OBJECTPOINT 10000 + * #define CURLOPTTYPE_STRINGPOINT 10000 + * #define CURLOPTTYPE_FUNCTIONPOINT 20000 + * #define CURLOPTTYPE_OFF_T 30000 + */ + + type CURLcode = CInt + object CURLcode { + final val CURL_OK: CInt = 0 + } + + type CURLoption = CInt + object CURLoption { + final val CURLOPT_VERBOSE: CInt = 41 + final val CURLOPT_POSTFIELDSIZE: CInt = 60 + final val CURLOPT_WRITEDATA: CInt = 10001 + final val CURLOPT_URL: CInt = 10002 + final val CURLOPT_ERRORBUFFER: CInt = 10010 + final val CURLOPT_POSTFIELDS: CInt = 10015 + final val CURLOPT_HTTPHEADER: CInt = 10023 + final val CURLOPT_HEADERDATA: CInt = 10029 + final val CURLOPT_CUSTOMREQUEST: CInt = 10036 + final val CURLOPT_WRITEFUNCTION: CInt = 20011 + } + + type CURLINFO = CInt + object CURLINFO { + final val CURLINFO_RESPONSE_CODE = 0x200002 + } + + type WriteFunction = CFunctionPtr4[ + Ptr[Byte], // data + CSize, // size + CSize, // nmemb + Ptr[Byte], // userdata + CSize // return + ] + + type curl_slist = Ptr[CStruct0] + +} + +@link("curl") +@extern +object curl { + import curlh._ + + def curl_easy_init(): CURL = extern + + def curl_easy_setopt( + curl: CURL, + option: CURLoption, + parameter: CVararg* + ): CURLcode = extern + + def curl_easy_perform(curl: CURL): CURLcode = extern + + def curl_easy_getinfo( + curl: CURL, + option: CURLINFO, + parameter: CVararg* + ): CURLcode = extern + + def curl_easy_cleanup(curl: CURL): Unit = extern + + def curl_easy_strerror(curl: CURL, code: CURLcode): CString = extern + + def curl_slist_append(head: curl_slist, string: CString): curl_slist = extern + + def curl_slist_free_all(head: curl_slist): Unit = extern + + def curl_version(): CString = extern + +} |