🥰

Scala native ことはじめ

2021/08/27に公開

https://scala-native.readthedocs.io/en/latest/user/setup.html

Scala 3 のリリースがひと段落したおかげか Scala native の開発が盛んになってきているようなので scala native を試してみる. 2021年8月時点の最新安定版は4.0.

graal native と異なり細かいセットアップをしなくてもイイ感じに ネイティブファイルを生成できる場合が多い.

セットアップ

Scala native をコンパイルするには sbt と Java 8+に加えて llvm, clang と clang++ が必要.

sbt と java は coursierでインストールできる.

curl -fLo cs https://git.io/coursier-cli-"$(uname | tr LD ld)"
chmod +x cs
./cs install
rm ./cs
cs setup

c 言語のコンパイラには gcc, msvc(microsoft visual c/c++), clang などがあるが、 clang は gcc の後継に位置するコンパイラである.

llvm による最適化や gcc と比べてより詳細な警告・エラーを表示してくれる利点がある.

ubuntu では以下のコマンドで最新のllvmとclang, clang++ がインストールされる.

sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"

または

sudo apt install clang
sudo apt install libgc-dev # optional

mac OS は、以下のコマンドで llvm をインストールすると clang がついてくる. インストールできたら 環境にインストールされている clang と衝突しないように path を通す.

brew install llvm
brew install bdw-gc # optional

詳しくは以下のサイトを参考にされたし.

https://students-tech.blog/post/install-clang.html#最新版のインストール方法

次に環境変数に CLANG_PATH, CLANGPP_PATH を設定する.

~/.bashrc
export CLANG_PATH=/path/to/clang-<version>
export CLANGPP_PATH=/path/to/clang++-<version>

以下のコマンドでプロジェクトの雛形を作成できる.

sbt new scala-native/scala-native.g8

または以下のようにセットアップする.

.
├── build.sbt
├── project
│   ├── build.properties
│   ├── plugins.sbt
│
├── src
    └── main
        └── scala
build.sbt
lazy val app = project.in(file("."))
  .enablePlugins(ScalaNativePlugin)
  .settings(
    scalaVersion := "3.2.1"
  )
project/build.properties
sbt.version = 1.8.0
project/plugins.sbt
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.8")

scala native で何ができる?

scala native のユースケースとしては次のようなものが挙げられる.

  • CLIツール
  • aws lambda/gcp cloud function
  • c/c++ で公開されている OS の API(win32apiなど) を利用するアプリケーション
  • c/c++ ネイティブに近いパフォーマンスが必要な場合

scala native の何がうれしい?

  • vs Java/Scala:
    • JVM 立ち上げのもっさりが無くなる. aws lambda/gcp functions での利用が捗る.
    • c/c++ を呼び出すときに JNI のようにボイラプレートをたくさん書かなくていい.
  • vs Java + graal native:
    • Scala をサポートしているので細かい設定が不要.
  • vs c/c++:
    • コードの記述量が減る. Scala の型や文法など高度に抽象化された機能がそのまま使える.
  • vs rust:
    • メモリ、ライフタイムや所有権などを気にしなくても良い.

scala native の基礎

ひな型を生成した直後は src/main/scala/Main.scala は以下のようなコードが書かれているはず.

object Main {
  def main(args: Array[String]): Unit =
    println("Hello, world!")
}

まずはこれが正しく動くか確かめる.
プロジェクトルートで以下のコマンドを実行すると scala native のコンパイルが走り、生成された実行ファイルが実行される.

sbt run

実行ファイルは target/scala-2.x.x/<project-name>-out に吐き出される.

もちろんターミナルから./target/scala-2.x.x/<project-name>-out と呼び出しても動く.

これだけでは何ができるのかわかりにくいので scala-native のパッケージを使って http リクエスト、 json のパース、ファイルの操作を順にみていこう.

メジャーな Scala のライブラリ(scalatest, cats, shapelessなど)は scala native に対応していることが多い.

http リクエストには sttp, json のパースには argonaut, ファイルの操作には oslib を使う.
パッケージ名の区切りに現れる%が3つになっていることに注意してほしい. scala native のパッケージを使うときはこのように記述する.

libraryDependencies ++= Seq(
  "com.softwaremill.sttp.client3" %%% "core" % "3.3.13",
  "io.argonaut" %%% "argonaut" % "6.3.6",
  "com.lihaoyi" %%% "os-lib" % "0.7.8"
)

まずは適当な json をパースしてみよう.

src/main/scala/Main.scala
import argonaut._

object Main {
  def main(args: Array[String]): Unit ={
    val json ="""{
    |  "foo": "bar",
    |  "baz": 1,
    |  "hoge": {
    |     "fuga": "piyo"
    |   }
    }
    """.stripMargin
    println(Parse.parse(json))
  } 
}

再度 sbt run を走らせると パースされた結果が出力されるはずだ.

次は http リクエストを扱う.

http リクエストを扱うには sttp を使うのだが、これは curl をバックエンドに使っているので次のライブラリが必要である.

  • curl
  • libcrypto
  • libidn11-dev
sudo apt install libidn11-dev libssl-dev curl

Main.scala を書き換える.

src/main/scala/Main.scala
import sttp.client3._

object Main {
  def main(args: Array[String]): Unit ={
    val query ="scala-native"
    val request:Request[Either[String,String],Any] = basicRequest.get(uri"https://api.github.com/search/repositories?q=$query")
    val backend:SttpBackend[Identity,Any] = CurlBackend()
    val response: Identity[Response[Either[String,String]]] = request.send(backend)

    println(response.body)
  } 
}

再度 sbt run コマンドを走らせる.
ひょっとすると 次のように環境変数を渡してやる必要があるかもしれない.

STTP_NATIVE=1 sbt run

同様に oslib をインポートしてファイルを操作するコードを書ける.

import os._

object Main {
  def main(args: Array[String]): Unit ={
    val example = os.read( os.pwd / "example.txt")
    println(example)
    // do whatever you want
  } 
}

  

他にも cli を作るのに便利な コマンド引数のパーサー decline(https://github.com/bkirwi/decline) や ターミナルに出力される文字列を装飾する fansi(https://github.com/com-lihaoyi/fansi) などのライブラリが scala native に対応している.

scala native の設定

scala native の設定は build.sbt から指定できる.

build.sbt
import scala.scalanative.build._

nativeConfig ~= {
  _.withClang(path/to/clang)
   .withClangPP(path/to/clangpp)
   .withCompileOptions(Seq())
   .withMode(Mode.default)  // default is debug
   .withGC(GC.default) // default is immix
   .withOptimize(true)
  }

その他設定項目は https://scala-native.readthedocs.io/en/latest/user/sbt.html#sbt-settings-and-tasks を参照されたし.

executable な cli ツール、チョットしたスクリプトから大規模なアプリケーションまで Scala を使って Scalable なプログラミングを楽しみましょう(^ω^)

Discussion