公開技術情報

[English] [Japanese]

80. Go 言語へのトランスコンパイル

LuneScript はトランスコンパイル先の言語として Lua だけでなく、 Go へのトランスコンパイルが可能です。

この機能により、 LuneScript のセルフホスティングコードを go 言語のコードにトランスコンパイルし、 それを go でビルドすることで、 LuneScript の高速化(約 16倍)を実現しています。

高速化については次を参照してください。 https://ifritjp.github.io/blog2/public/posts/2021/2021-06-28-lunescript-build-time-2000/

ただし、 LuneScript のセルフホスティングに必要な機能に限定したサポートになっているため、 一部の機能は対応できていません。

とはいえ、LuneScript のセルフホスティング自体が、 LuneScript のほとんどの機能を使用しているので、 普通に使う分には問題なく対応出来ています。

go 言語へのトランスコンパイル方法

go 言語へのトランスコンパイルは、 LuneScript のコマンドラインオプション save あるいは SAVE 時に -langGo を指定することで、 .go 拡張子のファイルに go へのトランスコンパイル結果を出力します。

Lua との連携動作

go 言語へのトランスコンパイルを行なう場合、 Lua との連携動作を行なう際に特別な型を使用する必要があります。

詳しくは以下を参照してください。

../lua

Go 言語へのトランスコンパイル後の構成

LuneScript は Lua へのトランスコンパイラであり、 LuneScript から Lua のコードを直接利用できるという特徴があります。

この特徴は、 Go 言語へトランスコンパイルした後でも引き継がれます。

例えば、次のようなコードを Go 言語へ変換した後でも動かせます。

// @lnsFront: ok
let step = 30;

// Lua のフィボナッチ関数
let code = ```
local function fib ( n )
   if n < 2 then return n end
   return fib(n - 2) + fib(n - 1)
end
return fib
```;

// Lua コードのロード
form fibFunc( num:int ):int;
fn loadLua( txt:str ) : Luaval<fibFunc> {
   let fib;
   __luago {
      let loaded = unwrap _load( txt ## );
      fib = unwrap loaded(##);
   }
   return fib@@fibFunc;
}

// プロファイル用マクロ
macro _profile( exec:__exp ) {
   {}
   {
      let prev = os.clock();
      print( "%s = %d: step = %d: time = %g"
             (,,,,exec, ,,exec, step, os.clock() - prev ) );
   }
}

// Lua のコードをロードして実行
let fib_lua = loadLua( code );
__luago {
   _profile( fib_lua( step ) );
}

// LuneScript のフィボナッチ関数
fn fib_lns( num:int ) : int {
   if num < 2 {
      return num;
   }
   return fib_lns( num - 2 ) + fib_lns( num - 1 );
}
// Go で定義した fib_lns 関数を実行
_profile( fib_lns( step ) );

このコードは、 フィボナッチの計算を lua と LuneScript で行なうものです。

最初の code 変数で定義しているのが lua のコードで、 それをロードするのが loadLua() 関数で、ロードされた Lua の関数が fib_lua です。 そして、後半の fib_lns() 関数は フィボナッチの計算を行なう LuneScript の関数です。

Lua へトランスコンパイルした場合

上記コードを Lua にトランスコンパイルして動かすと、次が出力されます。

fib_lua( step ) = 24157817: step = 37: time = 5.69
fib_lns( step ) = 24157817: step = 37: time = 5.38

これは、=fib_lua()= と fib_lns() それぞれの計算結果と、計算時間を表しています。

これを見ると、 Lua でロードされた関数 fib_lua() と、 Lns の関数 fib_lns() の実行時間がほとんど同じ時間掛ることが分かります。

これは当然の結果です。 なぜならば、 LuneScript を Lua に変換した場合は、 LuneScript で書いた fib_lns() と Lua の fib_lua() は、 ほとんど同じ Lua のコードになるからです。

上記のコードを Lua にトランスコンパイルした際のコードは次のようになります。

--miniGo.lns
local _moduleObj = {}
local __mod__ = '@miniGo'
local _lune = require( "lune.base.runtime2" )
if not _lune2 then
   _lune2 = _lune
end
local step = 37
local code = [==[
local function fib ( n )
   if n < 2 then return n end
   return fib(n - 2) + fib(n - 1)
end
return fib
]==]
local function loadLua( txt )
   local loaded = _lune.unwrap( _lune.loadstring52( txt ))
   local fib = _lune.unwrap( loaded(  ))
   return fib
end
local fib_lua = loadLua( code )
do
   local prev = os.clock(  )
   print( string.format( "%s = %d: step = %d: time = %g", "fib_lua( step )", fib_lua( step ), step, os.clock(  ) - prev) )
end
local function fib_lns( num )
   if num < 2 then
      return num
   end
   return fib_lns( num - 2 ) + fib_lns( num - 1 )
end
do
   local prev = os.clock(  )
   print( string.format( "%s = %d: step = %d: time = %g", "fib_lns( step )", fib_lns( step ), step, os.clock(  ) - prev) )
end
return _moduleObj

注目すべき fib_lns() 関数の定義を抜き出したものが以下です。

local function fib_lns( num )
   if num < 2 then
      return num
   end
   return fib_lns( num - 2 ) + fib_lns( num - 1 )
end

これを見ると分かる通り、 fib_lns()fib_lub() は等価のコードになっています。

よって、=fib_lns()= と fib_lua() が同程度時間がかかるのは当然と言えます。

go へトランスコンパイルした場合

一方、 go へトランスコンパイルした場合、実行結果は次の通りです。

fib_lua( step ) = 24157817: step = 37: time = 6.07
fib_lns( step ) = 24157817: step = 37: time = 0.34

fib_lns() の方が、=fib_lua()= よりも 計算時間が約 1/18 程度に短かくなっていることが分かります。

go へトランスコンパイルした結果は次の通りです。

// This code is transcompiled by LuneScript.
package lnsc
import . "lnsc/lune/base/runtime_go"
var init_miniGo bool
var miniGo__mod__ string
var miniGo_step LnsInt
var miniGo_code string
var miniGo_fib_lua *Lns_luaValue
type miniGo_fibFunc_1003_ func (arg1 LnsInt) LnsInt
// 14: decl @miniGo.loadLua
func miniGo_loadLua_1009_(txt string) *Lns_luaValue {
    var loaded *Lns_luaValue
    loaded = Lns_unwrap( Lns_car(Lns_getVM().Load(txt, nil))).(*Lns_luaValue)
    var fib LnsAny
    fib = Lns_unwrap( Lns_car(Lns_getVM().RunLoadedfunc(loaded,Lns_2DDD([]LnsAny{}))[0]))
    return fib.(*Lns_luaValue)
}

// 36: decl @miniGo.fib_lns
func miniGo_fib_lns_1025_(num LnsInt) LnsInt {
    if num < 2{
        return num
    }
    return miniGo_fib_lns_1025_(num - 2) + miniGo_fib_lns_1025_(num - 1)
}

func Lns_miniGo_init() {
    if init_miniGo { return }
    init_miniGo = true
    miniGo__mod__ = "@miniGo"
    Lns_InitMod()
    miniGo_step = 37
    miniGo_code = "local function fib ( n )\n   if n < 2 then return n end\n   return fib(n - 2) + fib(n - 1)\nend\nreturn fib\n"
    miniGo_fib_lua = miniGo_loadLua_1009_(miniGo_code)
    {
        var prev LnsReal
        prev = Lns_getVM().OS_clock()
        Lns_print([]LnsAny{Lns_getVM().String_format("%s = %d: step = %d: time = %g", []LnsAny{"fib_lua( step )", Lns_getVM().RunLoadedfunc(miniGo_fib_lua,Lns_2DDD(miniGo_step))[0].(LnsInt), miniGo_step, Lns_getVM().OS_clock() - prev})})
    }
    
    {
        var prev LnsReal
        prev = Lns_getVM().OS_clock()
        Lns_print([]LnsAny{Lns_getVM().String_format("%s = %d: step = %d: time = %g", []LnsAny{"fib_lns( step )", miniGo_fib_lns_1025_(miniGo_step), miniGo_step, Lns_getVM().OS_clock() - prev})})
    }
    
}
func init() {
    init_miniGo = false
}

非常に読み難いので fib_lns() 関数の定義部分を抜き出すと、次になります。

func miniGo_fib_lns_1025_(num LnsInt) LnsInt {
    if num < 2{
        return num
    }
    return miniGo_fib_lns_1025_(num - 2) + miniGo_fib_lns_1025_(num - 1)
}

関数名が長くなっていますが、 LuneScript のコードがそのまま go に変換されていることがわかります。 関数を実行する際も次のように普通に関数コールしているだけです。

 miniGo_fib_lns_1025_(miniGo_step)

一方で Lua の fib_lua() 関数は、 miniGo_fib_lua = miniGo_loadLua_1009_(miniGo_code) によって Lua のコードを実行するための関数がロードされていて、 実行する際も次のように関数コールしています。

Lns_getVM().RunLoadedfunc(miniGo_fib_lua,Lns_2DDD(miniGo_step))

このように、=fib_lns()= と fib_lua() では全く処理が異なることが分かります。

ビルド

go へトランスコンパイルしてビルドするには次の step が必要です。

  • go.mod を生成する
  • LuneScript ランタイムを go.mod に登録する
  • main.go を生成する
  • .lns から .go を生成する
  • go.mod を更新する
  • go build する

.lns を更新した場合は、 「 .lns から .go を生成する」以降の処理を繰り返し実行します。

各 step を説明します。

go.mod を生成する

lune.js があるディレクトリで、以下のコマンドを実行します。

$ go mod init test # <--- test は環境に合せて指定してください

LuneScript ランタイムを go.mod に登録する

以下のコマンドで、LuneScript のランタイムを go.mod に登録します。

$ go get github.com/ifritJP/LuneScript/src@latest

以降の作業で go mod tidy を実行しますが、必ず go get を先に実行してください。

main.go を生成する

go には、エントリ関数の main() が必要です。

以下のコマンドで、go の main() 関数を定義する main.go を生成します。

$ lnsc hoge.lns mkmain main.go

ここで hoge.lns は、 lns で __main を定義しているメインモジュールを指定します。 main.go は出力先のパスを指定します。

go にトランスコンパイルする場合、 __main 関数を定義するモジュールが必須です。

このコマンドで生成した main.go には、 lns のランタイムを初期化するコードが含まれます。

.lns から .go を生成する

以下のコマンドで .go を生成します。

$ lnsc hoge.lns save -langGo

__main 関数を含むメインモジュールの場合は、さらに –main オプション付加します。

$ lnsc hoge.lns save -langGo --main hoge

ここで –main hoge の hoge は、 メインモジュールの import パスです。

例えばメインモジュールが foo/bar/hoge.lns の場合、

$ lnsc foo/bar/hoge.lns save -langGo --main foo.bar.hoge

となります。

LuneScript を go にトランスコンパイルする場合、__main 関数が必要です。

go.mod を更新する

以下のコマンドで go.mod を更新します。

$ go mod tidy

go build する

全ての lns ファイルの変換と main.go の生成ができたら、go build を実行します。

$ go build

上記がエラーする場合は、以下を試してください。

$ go build -tags gopherlua

これにより、トランスコンパイルしたモジュールが go でビルドされます。

go へトランスコンパイル後は Lua ライブラリのリンクが必要

追記

Lua ライブラリのリンクを回避する方法を用意しました。

../lua_runtime

上記の通り、 LuneScript は Lua と密接に連携して動作しています。 そして、go へのトランスコンパイル後もその連携動作をサポートしています。

この連携動作を実現するために、 go へのトランスコンパイル後のコードでは、 lua の処理を行なうための lua VM を必要とします。

細かいことを言うと、この Lua との連携動作以外にも、 LuneScript には Lua VM を必要とする処理があります。 具体的にはマクロ展開の処理や、一部の組込み関数の処理に Lua VM を利用しています。

その lua VM は、公式の lua-5.3.4 を前提としていて、 liblua5.3.so をリンクします。

go 言語には、 「環境に依存しない 1 つの実行ファイルを生成できる」という利点がありますが、 残念ながら LuneScript を go へトランスコンパイルする場合、 liblua5.3.so へのリンクが必要になります。

なお、実行時に liblua5.3.so が必要になるだけではなく、 ビルド時には lua-5.3.4 のインクルードファイルも必要になります。