👑

NimでJavaScriptターゲットの開発をする方法

2023/02/16に公開

NimではC言語にトランスパイルして実行可能バイナリを作る以外にもJavaScriptを出力することもできます。
これが非常にテクニックが必要なので今回はわかりやすく網羅的に解説していきたいと思います。

JSターゲットの基礎

コンパイル

https://nim-lang.org/docs/backends.html#backends-the-javascript-target

js_sample.nim
echo "hoge"
コンパイル
nim js js_sample.nim

nim jsコマンドでjs_sample.jsを出力します。
もし実行環境にNodeJSが入っていれば、そのまま実行することもできます。

コンパイル
nim js -r js_sample.nim
出力
hoge

ライブラリ

JavaScript向けの標準ライブラリがあり、便利に使うことができます。

lib 説明
asyncjs JSの非同期処理のasync/awaitを使うことができます。NimのFuture[T]がJSのPromise<T>になります。
dom ブラウザが持っているdocumentwindowなどDOM操作をするためのライブラリです。
jsbigints JSのBitInt型を扱います。
jsconsole conoel.log()などを呼び出せるようになります。
jscore JSのMathJSONDateなどのライブラリを提供しますが標準ライブラリを使ったほうが安全です。
jsffi NimとJSの間で型を相互に変換するライブラリです。
jsfetch JSからAPIアクセスするためのHTTPクライアントです。
jsheaders jsfetchと共に使うHTTPヘッダーを扱うライブラリです。
jsformdata jsfetchと共に使うHTTPフォームデータを扱うライブラリです。
jsre JSでの正規表現を扱うライブラリです。
jsutils JSでの型を扱う便利機能を提供するライブラリです。

また3rdパーティライブラリとしてはnodejsというラッパーライブラリがあります。かなり巨大です。

型の扱い

Nimの型はJSに出力されるとどうなるか見ていきましょう。

app.nim
import std/jsffi
import std/times

let i = 0
let j = 0.0
let str = "string"
let cstr:cstring = "cstring"
let date = now()

コンパイルするとJSファイルが出力されます。

app.js
function makeNimstrLit(c_33556801) {
      var result = [];
  for (var i = 0; i < c_33556801.length; ++i) {
    result[i] = c_33556801.charCodeAt(i);
  }
  return result;
}

function getTime_922747872() {
  var result_922747873 = ({seconds: 0, nanosecond: 0});

    var millis_922747874 = new Date().getTime();
    var seconds_922747880 = convert_922747358(2, 3, millis_922747874);
    var nanos_922747891 = convert_922747358(2, 0, modInt(millis_922747874, convert_922747358(3, 2, 1)));
    result_922747873 = nimCopy(result_922747873, initTime_922747806(seconds_922747880, chckRange(nanos_922747891, 0, 999999999)), NTI922746910);

  return result_922747873;

}

function now_922748331() {
  var result_922748332 = ({m_type: NTI922746911, nanosecond: 0, second: 0, minute: 0, hour: 0, monthdayZero: 0, monthZero: 0, year: 0, weekday: 0, yearday: 0, isDst: false, timezone: null, utcOffset: 0});

    result_922748332 = nimCopy(result_922748332, local_922748328(getTime_922747872()), NTI922746911);

  return result_922748332;

}
var i_469762051 = 0;
var f_469762052 = 0.0;
var str_469762053 = makeNimstrLit("string");
var cstr_469762054 = "cstring";
var date_469762055 = now_922748331();

JSの世界で素の文字列として扱うには、cstringを使う必要があります。

配列の扱い

JSの世界の動的配列を扱うためのJsObject型が用意されています。

https://nim-lang.org/docs/jsffi.html#JsObject

JsObject = ref object of JsRoot
  Dynamically typed wrapper around a JavaScript object.
app.nim
import std/jsconsole
import std/jsffi

proc func1()  =
  let dyArr = newJsObject()
  dyArr["id"] = 1
  dyArr["name"] = "Alice".cstring
  dyArr["status"] = true

  console.log(dyArr)
  console.log(jsTypeOf(dyArr))

func1()
実行結果
{ id: 1, name: 'Alice', status: true }
object

Nimの構造体を定義するとJSの世界ではobjectとして扱われます。
totoJsの関数を使って、JsObjectと構造体の相互変換ができます。
JsObjectを使うとコンパイル時の静的な型チェックが行われなくなるので、なるべくロジックは構造体とそのメソッドを使ったほうが良いでしょう。

proc to(x: JsObject; T: typedesc): T:type {.importjs: "(#)"}
  Converts a JsObject x to type T.

proc toJs[T](val: T): JsObject {.importjs: "(#)"}
  Converts a value of any type to type JsObject.
app.nim
type Person = object
  id:int
  name:cstring
  status:bool

proc new(_:type Person, id:int, name:string, status:bool):Person =
  return Person(id:id, name:name.cstring, status:status)

proc func1()  =
  let dyArr = newJsObject()
  dyArr["id"] = 1
  dyArr["name"] = "Alice".cstring
  dyArr["status"] = true

  console.log(dyArr)
  console.log(jsTypeOf(dyArr))

  let person = dyArr.to(Person)
  console.log(person)

  let person2 = Person.new(2, "Bob", false)
  console.log(person2)


func1()
実行結果
{ id: 1, name: 'Alice', status: true }
object
{ id: 1, name: 'Alice', status: true }
{ id: 2, name: 'Bob', status: false }

DOM操作

HTMLのinputタグから入力した文字をリアルタイムでpタグに表示させてみましょう。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script defer type="module" src="app.js"></script>
  <title>Document</title>
</head>
<body>
  <input type="text" id="input">
  <p id="content"></p>
</body>
</html>
app.nim
import dom

proc onInput(e:Event) =
  let content = document.getElementById("content")
  content.innerText = e.target.value

let input = document.getElementById("input")
input.addEventListener("input", onInput)

JSファイルが出力されます。

app.js
function onInput_469762050(e_469762051) {
    var content_469762052 = document.getElementById("content");
    content_469762052.innerText = e_469762051.target.value;


}
var input_469762062 = document.getElementById("input");
input_469762062.addEventListener("input", onInput_469762050, false);

domライブラリを使うことで、NimからEventdocumentgetElementByIdなどが使えるようになります。

APIアクセス

フロントエンドの開発をするにはAPIアクセスは欠かせません。
NimにはJSターゲットでAPIアクセスをするためのjsfetchライブラリが用意されています。

app.nim
import std/asyncjs
import std/jsfetch
import std/jsconsole

proc apiAccess() {.async.} =
  let url:cstring = "https://api.coindesk.com/v1/bpi/currentprice.json"
  let resp = await fetch(url)
  let json = await resp.json()
  console.log(json)

discard apiAccess()

JSファイルが出力されます。

app.js
async function apiAccess_469762071() {
  var result_469762073 = null;

  BeforeRet: do {
    var url_469762079 = "https://api.coindesk.com/v1/bpi/currentprice.json";
    var resp_469762087 = (await fetch(url_469762079));
    var json_469762092 = (await resp_469762087.json());
    console.log(json_469762092);
    result_469762073 = undefined;
    break BeforeRet;
  } while (false);

  return result_469762073;

}
var _ = apiAccess_469762071();

プラグマについて

NimではJSターゲットの開発を行う時にはプラグマをよく使う必要があります。
プラグマとは他の言語であるアノテーションのようなもので、コンパイラに対してコンパイル時に指示を出すことができます。

exportc

これまで見てきた出力されたJSファイルを見ると、変数名や関数名にsuffixがついていました。exportcを使うことで、suffixを付けるのを禁止できます。

app.nim
import std/jsconsole
import std/jsffi

proc hello(arg: cstring){.exportc.} =
  let arg {.exportc.} = arg
  console.log("hello " & arg)

let name {.exportc.}: cstring = "Alice"
hello(name)
app.js
function hello(arg_469762052) {
    var arg = arg_469762052;
    console.log(("hello " + arg));
}
var name = "Alice";
hello(name);

emit

emitを使うと、その中で書いた処理がそのまま出力されるJSファイルに入れられます。
JSターゲットの開発を行う時にはその中で素のJSの処理を定義することができます。

app.nim
{.emit:"""
function hello(arg){
  console.log("hello " + arg)
}
""".}
app.js
function hello(arg){
  console.log("hello " + arg)
}

importjs

JSの関数とNimの関数をマッピングし、Nimの世界からJSの関数を呼べるようにするために使います。
#を使うと引数が前から順番に、@を使うと後ろ全部がその位置に挿入されます。

app.nim
import std/jsffi

{.emit:"""
function add(a, b){
  console.log(a + b)
}
""".}

proc add(a, b:int) {.importjs:"add(#, #)".}

add(2, 3)
app.js
function add(a, b){
  console.log(a + b)
}

add(2, 3);

実践的な開発を行う

ではこれまで見てきたことを踏まえて、Preactという軽量なReact風ライブラリをNimから呼び出して使ってみましょう。

https://preactjs.com/

ここで使うHTMLファイルはこのようにします。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <script defer type="module" src="app.js"></script>
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

Preactの処理をNimから呼ぶ

emitを使ってCDNからライブラリをインポートし、importjsを使ってライブラリの関数とNimの関数をマッピングします。

lib.nim
import std/dom
import std/jsffi

# ==================== Preactの定義 ====================

{.emit: """
import { h, render } from 'https://cdn.jsdelivr.net/npm/preact@10.11.3/+esm';
import htm from 'https://cdn.jsdelivr.net/npm/htm@3.1.1/+esm';

const html = htm.bind(h);
""".}


type Component* = JsObject

proc html*(arg:cstring):Component {.importjs:"eval('html`' + # + '`')".}
template html*(arg:string):Component = html(arg.cstring)


{.emit: """
function renderApp(component, dom){
  render(html``<${component} />``, dom)
}
""".}
proc renderApp*(component: proc():Component, dom: Element) {.importjs: "renderApp(#, #)".}

# ================== hooks ==================

{.emit:"""
import { useState, useEffect } from 'https://cdn.jsdelivr.net/npm/preact@10.11.3/hooks/+esm';
""".}

type IntStateSetter = proc(arg: int)

proc intUseState(arg: int): JsObject {.importjs: "useState(#)".}
proc useState*(arg: int): (int, IntStateSetter) =
  let state = intUseState(arg)
  let value = to(state[0], int)
  let setter = to(state[1], IntStateSetter)
  return (value, setter)


type StrStateSetter = proc(arg: cstring)

proc strUseState(arg: cstring): JsObject {.importjs: "useState(#)".}
proc useState*(arg: cstring): (cstring, StrStateSetter) =
  let state = strUseState(arg)
  let value = to(state[0], cstring)
  let setter = to(state[1], StrStateSetter)
  return (value, setter)


type States* = cstring|int|float|bool

proc useEffect*(cb: proc(), dependency: array) {.importjs: "useEffect(#, [])".}
proc useEffect*(cb: proc(), dependency: seq[States]) {.importjs: "useEffect(#, #)".}

ライブラリの呼び出し側はこのようにします。
JSXの部分はJSが解釈する文字列であり、その中で呼び出したい変数や関数はそこに書いたとおりの変数名で呼ばれることを期待するため、{.exportc.}を使ってsuffixが付かないようにします。

app.nim
import std/jsffi
import std/dom
import ./lib

proc App():Component {.exportc.} =
  let (message {.exportc.}, setMessage) = useState("")
  let (msgLen {.exportc.}, setMsgLen) = useState(0)

  proc setMsg(e:Event) {.exportc.} =
    setMessage(e.target.value)

  useEffect(proc() =
    setMsgLen(message.len)
  , @[message])

  return html("""
    <input type="text" oninput=${setMsg} />
    <p>${message}</p>
    <p>message length...${msgLen}</p>
  """)

renderApp(App, document.getElementById("app"))

このように動きます。

JavaScriptの静的型付けとしてのNim

let (message {.exportc.}, setMessage) = useState("")

ここでのsetMessageはcstring型しか引数として受け付けない関数であるStrStateSetterです。
lib.nimでこのように定義しているからです。

type StrStateSetter = proc(arg: cstring)

proc strUseState(arg: cstring): JsObject {.importjs: "useState(#)".}
proc useState*(arg: cstring): (cstring, StrStateSetter) =
  let state = strUseState(arg)
  let value = to(state[0], cstring)
  let setter = to(state[1], StrStateSetter)
  return (value, setter)

もしここでint型を入れようとするとどうなるでしょうか

proc setMsg(e:Event) {.exportc.} =
  # setMessage(e.target.value)
  setMessage(1)

もちろんコンパイルエラーになります。

/projects/nimjs/app.nim(11, 15) Error: type mismatch: got <int literal(1)>
but expected one of:
StrStateSetter = proc (arg: cstring){.closure.}

終わりに

NimでJavaScriptターゲットの開発をするテクニックについて紹介しました。
このようにNodeJSの環境を使うことなく、非常に簡単にJSの資産を使ってNimでReact風SPAを静的な型安全に作れることがわかりました。
今回紹介したことをベースにして、Nim製フロントエンドフレームワークの開発を進めていきます。応援してくれたら嬉しいです。
またJSをラップしたNimのライブラリの資産が増えていくことも願っています。

https://github.com/itsumura-h/nim-palladian

GitHubで編集を提案

Discussion