改訂版を書きました。
追記 R1.6.28
記事を書いた頃から色々変化してしまい、そのままで動きません。
flang でコンパイルしてできた fn.ll ファイルを wasm 形式に直すには、以下の blog が参考になります。この通りやれば、前より少ない手順でできます。
https://dassur.ma/things/c-to-webassembly/
なお、ウェブサーバーは python -m SimpleHTTPServer 8080 でよし。 /etc/mime.types に application/wasm wasm を足しておくことで、上記記事中の短い HTML で足ります。F12 の console にしか出力されませんが。
#WebAssembly
WebAssembly とはブラウザを仮想機械として、機械語・アセンブリコードを高速に実行しようという共通規格のようです。昔の p-code 機械がブラウザに乗った感じでしょうか?
[追記R1.7.19:実際はブラウザ上で各プロセッサ毎の機械語にもう一度コンパイルされるようです。]
たまたま WebAssembly のページにたどり着いて、 llvm 系のコンパイラならすぐにもやれる感じだったので、flang (llvm 系の Fortran コンパイラ)でも一丁やってみるかと軽い気持ちでやり始めたら、地獄めぐりになってしまいました。
一応は動いたので、地獄めぐり旅行記をメモっておきます。地獄はいくつかに分けていますが、実際は flang 生成以外の地獄には、同時にはまって彷徨しています。なお、本内容は bash on Ubuntu on windows 上で実行しました。
なお今のところ数学関数は使えず、動的メモリー確保(割り付け変数)もできてません。また intrinsic module も使えません。READ/WRITE 系の I/O もできません。
###webassembly のアドベント・カレンダー
https://qiita.com/advent-calendar/2017/webassembly
救い主は来るのか?
#第一地獄 flang
flang とは llvm をバックエンドにもつ Fortran コンパイラです。
https://github.com/flang-compiler/flang
###flang のコンパイル
flang のコンパイルは、下記の flang のページの手引き従って実行すれば、一応出来ます。しかし指示にあるように、flang のコンパイルには flang が要ります。 鬼の所業です。
[追記:flang のページの手引きに従う時に、1~4まで順番に実行することと、sudo make install のあと bash 立ち上げ直しをすると、flang として2番で生成された clang が使われるようになり、コンパイルできるようです。鬼の目にも涙w]
試しに gcc/g++/gfortran (Ver. 7.2.0) の組み合わせで試みましたが、うまくいきませんでした。また clang/clang++/gfortran の組み合わせもダメでした。(なお make -j 8 で 8 スレッド実行で早く終わります。)
- gcc-7/g++-7/gfortran-7 の -7 を消す方法。
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 10
参考ページ:https://www.scivision.co/selecting-compiler-versions-with-update-alternatives/
結局、 flang のページにある代替手段 Spack による方法で、一度 flang を作り、それを使って今一度 flang をコンパイルして、以降これを用いることとして、Spack を消去しました。
Spack とは、LLNL がスパコン用に作ったバージョン管理システムで、root権限なしで?あらゆる依存性の組み合わせ環境が作られる反面、すべてを1から構築し始めるので昼飯前に始めて、晩飯の後ぐらいに終わるレベルの時間がかかります。パスはハッシュ生成された謎数字のついたものとなります。
###flang を WebAssembly 対応へ
なお、指示通りに作ると WebAssembly には対応しないので、指示の 1 番 llvm の build のところで cmake にオプションを足してやる必要があります。これにより、flang の出力ターゲットに WebAssembly が加わります。
cmake -LLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly ..
参考ページ:https://qiita.com/Hiroki_M/items/89975a9e8205ced3603f
なお参考ページでは flang の指示に無い compiler-rt 等を入れていますが、無くても大丈夫でした。
#第二地獄 WebAssembly
上記の作業により flang は、WebAssembly に変換できる llvm コードを出せるようになります。次にこの出力を WebAssembly に変換するプログラムを用意します。それが binaryen です。
###binaryen のインストール
[追記:H31.3.22]
去年の夏ぐらいから WebAssembly で s2wasm がサポートされなくなったようです。ソースから削除されています。以下から公式バイナリを取ってきて、/usr/local/bin とか適当なパスの通ったところにコピーすれば当面は乗り切れます。
https://github.com/WebAssembly/binaryen/releases/tag/1.38.5
これは問題なくゆきます。
$ git clone --depth=1 https://github.com/WebAssembly/binaryen.git
$ cd binaryen
$ cmake .
$ make -j 8
$ sudo make -j 8 install
###flang から wasm へ
ソースプログラムを test.f90 として以下のような4段階をへて変換します。
$ flang -emit-llvm --target=wasm32 -S -Oz test.f90
$ llc test.ll -march=wasm32
$ s2wasm test.s > test.wast
$ wasm-as test.wast -o sample.wasm
ここで、**重要なのは flang によるコンパイル時に最適化オプション -Oz を加えておくことです。**これがないと、関数の頭部に余計なコードが生成されその部分が実行時エラーを引き起こします。これが分かるまで、毎回生成されたアセンブリコードから手動でエラーの出る部分を修正していました。
エラーの出るところは、実際は何も有効なことをしていないので、最適化をかけると削除されてエラーも出なくなります。鬼の所業です。
参考ページ:
https://qiita.com/Hiroki_M/items/89975a9e8205ced3603f
https://qiita.com/umamichi/items/c62d18b7ed81fdba63c2
#第三地獄 Web
WebAssembly を実際に動かすには HTML からの javascript による呼び出しの記述が必要となります。しかし HTML といえば 1.0/2.0、CERN の文書システムという知識レベルではいささか心もとなく、丸写しにたよります。
Web サーバー
Web サーバーを立てて、サーバー側に HTML ファイルと wasm ファイルの二つを置いてやる必要があるようです。以下のページに依って、Windows 上で KantanWEBserver を用いて解決しました。
参考ページ:https://qiita.com/massie_g/items/2913066e596dae197539
ブラウザでの実行時にはファンクションキー F12 を押して、コンソール窓を開いておくと甚だ便利です。
世知辛いことに、HTML ファイルと wasm ファイルの二つをローカルにおいて、ファイルとして実行することはできないようです。Chrome/IE/Edge などで試しましたがダメでした。また、Chrome の --allow-file-access-from-files オプションも WebAssembly は適用外でした。鬼の所業です。
HTML ファイル
fetch('./pai.wasm') のところに生成した wasm ファイル名を指定します。また、const result = ex.pai(65) の ex. 以下 pai(65) のところにソースコードで定義した関数名および引数を書きます。
fetch のところでは、WebAssembly の機械語ファイルを読み込み、const result = ex.pai(65) のことろで関数呼び出しをして、実行結果を result 変数に返しています。
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<body>
<script>
fetch('./pai.wasm').then(response => response.arrayBuffer()
).then(bytes => {
return WebAssembly.instantiate(bytes, {})
}).then( r => {
const ex = r.instance.exports
console.log(ex)
const result = ex.pai(65)
console.log(result)
document.querySelector("#root").innerHTML = result
})
</script>
<div id="root"></div>
</body>
</html>
恥ずかしながら、肝心の**丸パクリ引用元が分からなくなるという大失態を犯してしまいました。**中々うまくゆかなかったので色々な HTML を次々と丸写ししたためです。この先、見つけることができたら参考サイトとして引用したいと思います。
#第四地獄 Fortran
最近の Fortran は C 言語とのインターフェース整備も進んでいるため、C 言語で出来ていれば、Fortran でも苦労なくできることが多いです。
今回も WEB サーバーを立てて、関数を呼び出すところまでは C 言語での例を参考に進むことができました。エラーは出るものの関数呼び出しまで実行されていることは、ブラウザのデバッガーで追うことが出来ました。また中途生成されるアセンブリを手で書き換えれば、エラーを出さずに実行できることも分かりました。第二地獄 WebAssembly で書いたように、これは最適化をかけることで抑止できることも分かりました。
以下では実例により Fortan/flang 固有の問題を挙げます。
###基本事項
何らかの返り値が要求されているようなので、function 文による記述をします。呼び出し HTML 側から見える function 名は bind(c, name = '何某') によって明示的に指定することにします。Fortran はオブジェクト名を処理系ごとに適当に修飾するので(よくあるのはアンダースコアの付加、大文字化)、bind 指定によって固定します。特にモジュール内のサブルーチン/関数のオブジェクト名は、モジュール名で修飾することが多いので、bind しておくと安心です。
引数無し 野良ルーチン
最も簡単な例で、アセンブリコードを眺めるのに丁度よいです。
function test() result(ires) bind(c, name = 'test')
integer :: ires
ires = 2
end function test
引数あり Module 内変数・関数
Fortran での引数は参照呼出しがデフォルトですので、ここでは引数の値呼び出しの指定をしてやる必要があります。アセンブリを見ると、引数にどんな型を与えても整数型に変換されているので、ドはまりしました。参照呼出し以外を使うのは、鬼の所業です。
module m_mod
integer, parameter :: kd = kind(1.0d0)
real(kd), parameter :: pi = 4 * atan(1.0_kd)
contains
real(kd) function test(x) result(res) bind(c, name = 'test')
real(kd), value :: x
res = pi * x
end function test
end module m_mod
###制限事項
以上の基本事項のほかに、様々な制限事項があるようです。それが本質的なものなのか、私のオプション指定の誤りなどによるものなのかよく分かっていません。コンパイル時はエラーは出ませんが、実行時に外部モジュールが見つからないといって叱られます。
- 各種数学関数 sin,cos,gamma 等利用不可
- 割り付け変数の allocate 不可
- 引数に依存するサイズの自動変数の配列 不可
- use, intrinsic :: ISO_C_BINDING 不可
- READ/WRITE 系 I/O 不可
また flang は Nvidia の Fortran コンパイラ (PGI Fortran) のフロントエンドを用いているため、Fortran 2008 規格への対応状況は遅れています。
実際の実行例
ここでは数学関数を用いないで計算できる例として、円周率の近似値 355/113 と円周率 π の差を、積分による厳密な式
$$ \int_0^1{x^8(1-x)^8(25+816x^2)\over3164(1+x^2)}dx = {355\over113}-\pi$$
に従って計算してみます。
###ソースプログラム
ここでプログラムでは、左辺を台形積分で求める関数 pai(n) [但し引数 n は積分区間の分割の数]、および右辺を求める関数 pai2() を定義しています。なお配列を固定長で確保する必要があったので、積分区間の分割は 129 までしか取れません。
参考サイト:http://fortran66.hatenablog.com/entry/2017/02/23/023021
module m_pai
implicit none
integer, parameter :: kd = kind(1.0d0)
real(kd), parameter :: pi = 4 * atan(1.0_kd)
contains
real(kd) function pai(n) bind(c, name = 'pai')
integer, value :: n
real(kd) :: x(129), y(129), h
integer :: i
h = 1.0_kd / (n - 1)
forall (i = 1:n) x(i) = (i - 1) * h
y = f(x(1:n))
pai = h * ( sum(y(1:n)) - 0.5_kd *(y(1) + y(n)) )
end function pai
real(kd) pure elemental function f(x)
real(kd), intent(in) :: x
f = x**8 * (1.0_kd - x)**8 * (25.0_kd + 816.0_kd * x**2) / (3164.0_kd * (1.0_kd + x**2))
end function f
real(kd) function pai2() bind(c, name = 'pai2')
pai2 = 355.0_kd / 113.0_kd - pi
end function pai2
end module m_pai
###コンパイル
flang は warning を出しますが無視しています。
$ flang -emit-llvm --target=wasm32 -S -Oz pai.f90
warning: overriding the module target triple with wasm32 [-Woverride-module]
1 warning generated.
$ llc pai.ll -march=wasm32
$ s2wasm pai.s > pai.wast
$ wasm-as pai.wast -o pai.wasm
出来あがった pai.wasm ファイルと上に示した HTML ファイルを WEB サーバーソフトで指定したディレクトリにコピーします。
###実行結果
実行結果を見ると理論値通り小数点以下 6 桁まであっているようで、単精度くらいの精度があります。まぁ 355 と 113 と 6 個の数字を与えているのだから 6 桁まであっていて当然と言えば当然ですが、奇数 1,3,5 の順の繰り返しと思えば霊験あらかたかも。