はじめに
プリンに醤油かけるとウニみたいになる、って話あるじゃないですか。
もし本当なら「200円未満で数千円を代替できる」というだいぶ美味しい話になると思うんですけど
正直なところ「ホンマか?」という疑念の方が強くて未だ試せずにいるんですよね。
ということで、今年の年末年始こそはトライしてみようと思う今日この頃です。
本記事について
この記事はC++ Advent Calendar 2025 の19日目の記事…だったはずの何かです(超・超・大遅刻)。
すみませんでした!!
18日目の記事はsashiさんのAngelScript を C++ プロジェクトに組み込む でした。
私は(多分)AngelScriptの存在を初めて知ったのですが、Engine作成からC++サイドのバインドまで一通り整備されているのは面白そうですね。可能ならリッチなRTOSとかに移植して踊らせてみたい…
概要
本記事では、表題へ記載の通り
- LLVMとLLDをwasmへビルドし
- 自作のLLVMターゲットなコンパイラに組み込むことで
- コンパイルからリンク、実行までをすべてブラウザ上で完結
させることを目標にします。
組み込み先のコンパイラはもちろんC++で自作します。なにせC++アドベントカレンダーですしね。
著者はLLVMリーディング未経験者であり、各所の理解や認識に誤解を持っている可能性があります。
可能な限り合理性や論拠を損なわないよう配慮しておりますが、誤りが含まれる可能性ある旨ご注意いただけますと幸いです。
想定する読者層
本記事は以下読者層を想定し記載しています。
- wasm, emscriptenをなんとなく理解している。
- LLVM周りの概念や機能をなんとなく知っている。(LLVM IR、IRBuilder等)
wasm, emscriptenはもとより
LLVM周りの機能についても、ネットの海に先人が残してくださった偉大な資料が豊富にあると思うので、もし記事の中で不明点あれば適宜そちらを参照いただければと思います。丸投げですみません。
(というか、自分も正直知らない事ばかりなので先人の恩恵に預かってました。)
今の時代ならAIに聞く手もありそうです。
そもそも「LLVM/LLDを組み込む」とは?
さて、冒頭で高々と宣言したわけですが
いきなり「LLVM/LLDを組み込みます!」と書いても、少し話が飛び過ぎている気がしますね。
ということで、改めて 組み込む とは具体的に何を表すのかを明記すると
- LLVMプロジェクトに含まれる IRBuilderとllc、lld の3つを
- wasmターゲットでライブラリ化し、コード中で呼び出す(notコマンド)
ことを指しています。
組み込み対象のコンポーネントはざっくり以下のようなものになります。
- IRBuilder: LLVM IRを生成する為のコンポーネント。直に生文字列を出力せずとも、簡単な記載でLLVM IRを生成できる。(Rustでお馴染みの inkwell なんかでも使いますね)。)
-
llc: LLVM IR(LLVM用の中間表現)をアセンブラにコンパイルするツール。
lld: llvmプロジェクトがメンテナンスしているリンカー。
本記事では、これらのコンポーネントをwasmターゲットへビルドした上で、ライブラリ経由で呼び出し実行します。
全体の流れ
流れとしてはざっくりこんな形です。
- wasm系ツールチェインで、libllvmとliblldをビルドする。
- 「1.」で作ったlibllvmとliblldを叩きながら自作言語コンパイラを実装しビルドする。
- 「2.」で作成したwasmファイルをJSサイドでキックする。
- うまく動けば成功!
次の章より、各フェイズの具体的な中身を説明します。
作業フロー
0. 事前準備
ビルド前に以下を準備します。
- emscripten
- 今回は
4.0.18を利用。 - wasi-sdkは考慮外(私には諸々のサポートが難しく、諦めてしまいました…)
- 今回は
- cmake
- 何かしらのビルドシステム(本記事はninja想定)
1. emscriptenツールチェイン向けのlibllvmとliblldを用意する
まずはlibllvmとliblldをビルドします。
1-1. LLVMをemscriptenでビルドする
LLVMプロジェクトを丸ごとgit cloneした後、以下のコマンドを実行してビルドします。
なお本執筆時にはclang21ブランチを取得しています。(詳しいコミット位置などは、最後に記載しているgitリポジトリをご参照ください)
emcmake cmake ./llvm -GNinja -B./build -DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_PROJECTS="lld"\
-DLLVM_TARGETS_TO_BUILD="WebAssembly"\
-DLLVM_INCLUDE_TESTS=OFF\
-DLLVM_BUILD_TOOLS=OFF\
-DCMAKE_CXX_FLAGS="-pthread"\
-DCMAKE_C_FLAGS="-pthread"
-DCMAKE_EXE_LINKER_FLAGS="-pthread -sINITIAL_MEMORY=256MB"
ここで、各オプションについて軽く説明すると
- LLVM_ENABLE_PROJECTS, LLVM_TARGETS_TO_BUILD : 有効化するプロジェクトやアーキテクチャのターゲットです。今回はlld+wasmだけあればいいので、上記の通りとします。
- LLVM_INCLUDE_TESTS, LLVM_BUILD_TOOLS: テストやllvm関連ツールをビルドするかの設定です。こちらも今回はいらないので無効化。
- CMAKE_XXX_FLAGS: CMAKEにおけるコンパイル/リンク時のフラグ設定です。lldはマルチスレッドで動く(?)ので、pthreadを有効にしておきます。
- -sINITIAL_MEMORY: wasm環境における初期メモリのサイズ指定です。lldは起動時にヒープからメモリをがっつり取得する?ようなので、事前に余裕を持たせておきます。
各オプションのより詳しい内容については、以下等をご参照ください。
- LLVMビルドマニュアル: https://llvm.org/docs/CMake.html
- EMScriptenビルドマニュアル: https://emscripten.org/docs/tools_reference/settings_reference.html
ビルド後、build/lib以下に各ツールの静的ライブラリ一式ができることを確認します。
実際はLLVM_BUILD_LLVM_DYLIBをonにしlibLLVM.soを作成しつつ、emscripten上で動的リンクした方がスムーズだったかもしれません。
(今回は自分が理解しきれなかったのもありスルー。誰かトライしてみていただけたら嬉しい。)
最初はwasm64を利用(-sMEMORY64=1)しようとしたのですが、clang21時点ではllvmがうまくビルドできなかったためwasm32で動かしています。
1-2. 動作確認
ビルドできたら、簡易的なプログラムを書いて動くことを確認します。
今回はIRBuilder、llc、wasm-ldの3つが動いていればいいはずなので、その辺りを中心に見てみます。
/**
* @file sample.cpp
* @brief wasm上でllc,lldを動かすサンプル(llvm系の変数名はllvmに寄せつつ、それ以外はGoogle C++ StyleGuideベースで実装)
*/
#include <memory>
#include <optional>
#include <print>
#include <string>
#include <vector>
#include "lld/Common/Driver.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Verifier.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Support/SourceMgr.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Target/TargetMachine.h"
#include "llvm/Target/TargetOptions.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
/**
* @brief IRBuilder使ってLLVM IRを生成するサンプル(10+20に該当するものを生成するだけ)
*
* @return std::optional<std::string>
*/
static std::optional<std::string> CreateIR() {
using namespace llvm;
LLVMContext context;
Module module("test", context);
IRBuilder<> builder(context);
FunctionType* functype = FunctionType::get(Type::getInt32Ty(context), false);
Function* func = Function::Create(functype, Function::ExternalLinkage, "_start", module);
BasicBlock* bb = BasicBlock::Create(context, "entry", func);
builder.SetInsertPoint(bb);
auto lhs = builder.getInt32(10);
auto rhs = builder.getInt32(20);
auto addres = builder.CreateAdd(lhs, rhs);
builder.CreateRet(addres);
llvm::verifyFunction(*func);
std::string result;
raw_string_ostream st(result);
// fixit: 本当はこんなふうにGenerator/Linkerを分断するのではなく、アセンブラ生成(linker.cppのrun)まで同じmoduleとcontextで実施した方が効率的なはず
// ...だがちょっと頭が回り切ってないので、面倒だが今回は1パス挟む
module.print(st, nullptr);
return result;
}
/**
* @brief llc相当の処理を行う関数
*
* @param program LLVM IRで記述されたプログラム
* @param program_name プログラムの識別名
* @param output_path 出力先ファイル名
* @return 成功ならtrue。そうでなければfalse
*/
static bool CompileLLVMIR(std::string_view program, const char* program_name, const std::string&& output_path) {
using namespace llvm;
// ====1.初期化====
InitializeAllTargets();
InitializeAllTargetMCs();
InitializeAllAsmPrinters();
//InitializeAllAsmParsers(); //warning: LLVM IR->asmではいらない気もするので無効化しているが、論拠を特定できていない。有効化したほうがいいかも。
// note: llc.cppでは、この後`initializeCore`等の呼び出しが続くのだが
// Rustのllvm部では呼び出されていない(3年前くらいのコード?では呼び出されていた)
// 今回のユースケースは、どちらかといえばRust(利用者サイド)なので記述していないが、実用時には留意すべきかも。
// (一応llc側には`Initialize codegen and IR passes used by llc so that the -print-after...`と書かれているが…)
// ====2.LLVM IRパース====
LLVMContext Context;
SMDiagnostic Err;
std::unique_ptr<MemoryBuffer> IRProgramBuffer = MemoryBuffer::getMemBufferCopy(
StringRef(program.data(), program.size()));
std::unique_ptr<Module> M = parseIR(IRProgramBuffer->getMemBufferRef(), Err, Context);
if (M == nullptr) {
Err.print(program_name, errs());
return false;
}
// ====3.ターゲット設定====
Triple TargetTriple(Triple::normalize("wasm32-unknown-unknown"));
M->setTargetTriple(TargetTriple);
std::string error;
const Target* Target = TargetRegistry::lookupTarget(TargetTriple.getTriple(), error);
if (Target == nullptr) {
errs() << "Error: " << error << "\n";
return false;
}
// ====4.オプション設定====
TargetOptions Options;
// note: 自作言語内では例外機構を使わないのでNone(デフォルトでNoneなので実際は不要だが、明示がてら記載)
// もしWASMの例外機構使いたければ、適切なmodelを選択の上、WasmEnableEmEH/WasmEnableEHあたりをセットする必要がありそう。
Options.ExceptionModel = ExceptionHandling::None;
Options.ThreadModel = ThreadModel::Single; // マルチスレッド使わない
Options.MCOptions.PreserveAsmComments = false; // 中で動かすだけなので、余計な情報は加えない
// note: Relocは再配置モデル。なおRustではEmScriptenがPIC、それ以外のwasmターゲットはstaticだった(libcの兼ね合い?)
TargetMachine* TheTargetMachine = Target->createTargetMachine(TargetTriple, "generic", "", Options, Reloc::Static);
if (!TheTargetMachine) {
errs() << "Failed to create TargetMachine\n";
return false;
}
M->setDataLayout(TheTargetMachine->createDataLayout());
// ====5.吐き出し先の登録と実行====
std::error_code EC;
raw_fd_ostream OS(output_path, EC);
legacy::PassManager PM;
CodeGenFileType FileType = CodeGenFileType::ObjectFile;
if (TheTargetMachine->addPassesToEmitFile(PM, OS, nullptr, FileType)) {
errs() << "TargetMachine can't emit a file of this type\n";
return false;
}
PM.run(*M);
outs() << "Successfully compiled to " << output_path << "\n";
return true;
}
LLD_HAS_DRIVER(wasm) //note: LLDにおけるwasmドライバーを有効化するための一文。これがないとlldMainでドライバーが使えない
static bool linkWithLLD(const std::string& objFile, const std::string& outputBinary) {
using namespace llvm;
std::vector<const char*> args;
// ふつーのwasm-ldオプション群
args.push_back("wasm-ld");
args.push_back(objFile.c_str());
args.push_back("-o");
args.push_back(outputBinary.c_str());
args.push_back("--entry");
args.push_back("_start");
args.push_back("--verbose");
auto res = lld::lldMain(args, outs(), errs(), {{lld::Wasm, &lld::wasm::link}});
if (!res.retCode) {
outs() << "Successfully linked binary: " << outputBinary << "\n";
} else {
errs() << "Linking failed\n";
}
return res.retCode;
}
extern "C" int EMSCRIPTEN_KEEPALIVE SampleMain() {
if (auto program = CreateIR(); program) {
auto p = program.value();
CompileLLVMIR(std::string_view{p.data(), p.size()}, "a.ll", "test.wasm");
linkWithLLD("test.wasm", "linked_wasm_binary.wasm");
} else {
std::print(stderr, "しっぱい");
}
return 0;
}
実装したら、llvm-project/build/lib以下全ての.aファイルと繋ぎ合わせて
em++でビルドします
(自分の環境では以下のようなcmakeを書いてビルドしました。)
https://github.com/kizul322/llvm_lld_on_wasm_sample/blob/main/example/CMakeLists.txt
できたら簡易的なHTTPサーバーを立てて動かしてみます。
pthreadを利用する都合、一部ヘッダーを付加しながらpythonで立てます。
import http.server
import socketserver
PORT = 8000
class SharedArrayBufferRequestHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
super().end_headers()
if __name__ == "__main__":
with socketserver.TCPServer(("", PORT), SharedArrayBufferRequestHandler) as httpd:
print(f"Serving at http://localhost:{PORT}")
try:
httpd.serve_forever()
except KeyboardInterrupt:
httpd.shutdown()
htmlはとりあえず叩ければいいので適当に。
(sample.jsの部分は必要に応じて書き換えください。)
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>sample</title>
<script src="./sample.js"></script>
<script>
function runSample() {
let result = Module.ccall('SampleMain', 'number', [], []);
if (result !== 0) {
return
}
let fileData = Module.FS.readFile("linked_wasm_binary.wasm");
let blob = new Blob([fileData], { type: 'application/octet-stream' });
let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = 'linked_wasm_binary.wasm';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
</script>
</head>
<body>
<input type="button" value="てすと" id="run" onclick="runSample()" />
</body>
</html>
準備ができたらlocalhost:8000へアクセスし、runボタンをクリックします。
その後linked_wasm_binary.wasmファイルがダウンロードされれば成功です。
ダウンロードまで終わったら、本当に正しいかの検証までにwasmerでも動かしてみましょう。
wasmer ./linked_wasm_binary.wasm
⠁ Compiling to WebAssembly
30
上記の感じで、10+20の計算結果が返っていたらOKです。
(※. wasmer: wasmをブラウザなしでターミナル等から実行可能な環境。殆どのパッケージマネージャーで配布されている…はず)
2. 自作言語コンパイラを実装する。
「1.」で作成したライブラリを用いて、自作の言語コンパイラを実装します。
…と言っても時間がなかったので、とりあえず四則演算1行だけ解析可能なものを早足で実装しました。コンパイラつうかただの解析機じゃねえかと突っ込まれる音が聞こえる…
https://github.com/kizul322/llvm_lld_on_wasm_sample/tree/main/src
(実装にはrvariant を利用しています(一部32bit向けに微変更)。めちゃめちゃ便利。 )
めちゃくちゃな雑書きレベルなので、ソースの詳細はあまり参考にしないでください。
claude codeに丸投げ生活し続けた弊害が出たか…
3. 周辺を整えてキックしてみる。
最後に、JSサイドからキックしwasmを実行するコードをhtml/jsで記述します。
適当なhtmlファイル1個置いとくだけでもいいのですが、結果だけポンと出ても面白くないのでwabtで逆アセンブル結果を出しています。あとついでにtailwindcssでちょっとだけスタイルを整理。
(web系は素人故、所々変な実装が目立つのはお許しを…(予防線))
https://github.com/kizul322/llvm_lld_on_wasm_sample/blob/main/server/index.html
4. 実行してみる
あとはhtml上で実行し
- IRBuilderを使ってLLVM IRが生成でき
- 生成したLLVM IRがllcでwasmへコンパイルでき
- wasmファイルがwasm-ldでリンクでき
- リンクされたバイナリが動かせる
ことが確認できればOKです
それではトライしてみましょう
動いていそうですね!
おわりに
ということで、wasm上でLLVMとlldを動作させてみる記事でした。
本記事で紹介したソースは全て以下に公開しています。
https://github.com/kizul322/llvm_lld_on_wasm_sample
来たる西暦2XXX年の近未来、全人類がうっかりemscriptenを除くC/C++ツールチェインを消滅させてしまった時の参考にでもなれば幸いです。
また、本記事に誤字/脱字や認識理解などありましたら、コメント等いただけると助かります。
(即答可能かは分からないのですが、情報の正確性や自信の理解促進の面からもコメント頂けるのは大変助かります。)
20日目はI(@wx257osn2)さんの 結局std::simdってどうなのさです。
そういえばstd::simdの現状よく分かってないですね。筆者は普段ARM32/aarch64の森に引きこもってる(詳しい訳ではない)理解浅いマンなので、これを機に色々見てみたいところです。
利用/参考にさせていただいたリンク、ソース、書籍様など
【LLVM、LLD周りの理解】
- My First Language Frontend with LLVM Tutorial: https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html
- Writing an LLVM Pass: https://llvm.org/docs/WritingAnLLVMPass.html#quick-start-writing-hello-world
- 作って学ぶコンピュータアーキテクチャ —— LLVMとRISC-Vによる低レイヤプログラミングの基礎: https://www.amazon.co.jp/%E4%BD%9C%E3%81%A3%E3%81%A6%E5%AD%A6%E3%81%B6%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3-%E2%80%94%E2%80%94-LLVM%E3%81%A8RISC-V%E3%81%AB%E3%82%88%E3%82%8B%E4%BD%8E%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E7%A4%8E-%E6%9C%A8%E6%9D%91-%E5%84%AA%E4%B9%8B/dp/4297129140
- LLVM APIを使ってみよう! 〜 Brainf**kコンパイラをIRBuilderで書き直してみた 〜: https://itchyny.hatenablog.com/entry/2017/03/06/100000
【実装時】
- rvariant: https://github.com/yaito3014/rvariant
※.1 本文中へすでに記載されているものも含みます。
※.2 本記事並びに本著者と関連リンクの間に一切の利害関係はありません(本初版執筆時点)。もし不適切なリンク掲載などあれば、お手数ですが連絡の程お願い致します。
