slidenumber: true autoscale: true
Swift5
-
特徴: ネイティブコンパイル(LLVM), 静的型付け, null安全, 値型, 関数型, 検査例外, ...
-
作者: Apple, クリスラトナー
-
用途: iOS開発
-
Q: LLVMに落ちるならemscriptenでなんとかならんのか?
-
A: ならなかった6
ブラウザ上でSwiftが動かせる
- 作者: @zhuowei
オンラインのコンパイラAPIにソースを投げて、WASMバイナリを落として実行
async function runClicked() {
// ...
try {
const compileResult = await compileCode(code);
populateResultsArea(compileResult);
if (compileResult.output.success) {
runWasm(compileResult.binary);
}
} catch (e) {
console.log(e);
}
// ...
}
async function compileCode(code) {
// ...
const fetchResult = await fetch(kCompileApi, {
method: "POST",
body: JSON.stringify({
src: code
}),
headers: {
"Content-Type": "application/json"
}
});
const resultBuffer = await fetchResult.arrayBuffer();
return parseResultBuffer(resultBuffer);
}
WASIを利用する形にビルドされていて、WasmtimeやLucetで動くらしい。7
関連ソースが一通り公開されている
必要な作業の解説がPR上にある
-
メタデータのRelative PointerをAbsolute Pointerに変更 これのおかげで動くようになった
-
Swift Calling Conventionへの対応 これがまだなので未完成 特定のコードでクラッシュする
一通りのソースがあるので自分で試すことができる。環境はUbuntu。
なんかtodo
みがある8
utils/build-script --release --wasm \
--llvm-targets-to-build "X86;WebAssembly" \
--llvm-max-parallel-lto-link-jobs 1 --swift-tools-max-parallel-lto-link-jobs 1 \
--wasm-wasi-sdk "/opt/wasi-sdk" \
--wasm-icu-uc "todo" \
--wasm-icu-uc-include "$sourcedir/icu_out/include" \
--wasm-icu-i18n "todo" \
--wasm-icu-i18n-include "todo" \
--wasm-icu-data "todo" \
--build-swift-static-stdlib \
--install-swift \
--install-prefix="/opt/swiftwasm-sdk" \
--install-destdir="$sourcedir/install" \
--installable-package="$sourcedir/swiftwasm.tar.gz"
いろいろダウンロードが必要9
wget https://github.com/swiftwasm/swiftwasm-sdk/releases/download/20190503.1/swiftwasm.tar.gz
wget https://github.com/swiftwasm/wasi-sdk/releases/download/20190421.6/wasi-sdk-3.19gefb17cb478f9.m-linux.tar.gz
wget https://github.com/swiftwasm/icu4c-wasi/releases/download/20190421.3/icu4c-wasi.tar.xz
wget https://github.com/WebAssembly/wabt/releases/download/1.0.10/wabt-1.0.10-linux.tar.gz
デプロイもややこしい10
#!/bin/bash
set -e
rm -r compiler || true
mkdir compiler
cd compiler
for i in ../prebuilt/*
do
tar xf $i
done
cd ..
mv "compiler/wasi-sdk-"* "compiler/wasi-sdk"
mv "compiler/wabt-"* "compiler/wabt"
bash remove-swift-extra-files.sh || true
bash remove-wasi-extra-files.sh || true
bash remove-wabt-extra-files.sh || true
mkdir compiler/extralib
cp libatomic.so.1 compiler/extralib/
cp compiler/opt/swiftwasm-sdk/lib/swift/wasm/wasm32/glibc.modulemap ./
bash generateModulemap.sh \
"$PWD/compiler/wasi-sdk/opt/wasi-sdk/share/sysroot" \
>compiler/opt/swiftwasm-sdk/lib/swift/wasm/wasm32/glibc.modulemap
サービスのソースが参考になる。11
async function compileOneFile(appPath, folder, sourcePath) {
const objectPath = path.join(folder, "source.o");
const outputPath = path.join(folder, "program.wasm");
var output = "";
try {
const compileOutput = await execFile(path.join(appPath, "compiler/opt/swiftwasm-sdk/bin/swiftc"), [
"-target", "wasm32-unknown-unknown-wasm",
"-sdk", path.join(appPath, "compiler/wasi-sdk/opt/wasi-sdk/share/sysroot"),
"-o", objectPath,
"-O", "-c", sourcePath], execArg(appPath, {
"timeout": 4000,
}));
output = compileOutput.stderr;
} catch (e) {
output = e.stderr;
return {success: false, output: output};
}
/* ... */
}
アドレスの代わりに、そこからのオフセットで表現するポインタ。
int main() {
int a = 3;
intptr_t abs_ptr = (intptr_t)&a;
intptr_t rel_ptr = 0;
rel_ptr = (intptr_t)&a - (intptr_t)&rel_ptr;
// 0x7fff826e3f24, 0x12
printf("0x%lx, 0x%ld\n", abs_ptr, rel_ptr);
}
(僕の理解)
ビット幅が小さくて済む
その部分が静的に再配置可能になる
オブジェクトファイルにおけるRelative Pointer定数の定義は、シンボル間オフセットとして記述できる。つまり、リンカがサポートするギミック。
しかしWASMが対応していない。
Swiftは型定義に関する情報などをメタデータとしてバイナリに埋め込む。 フィールドのメモリレイアウトなどを含んでいて、ジェネリクスを動かすときなどに使っている。 ここでRelative Pointerが多く使われている。
struct Stone {
var weight: Int = 3
}
let stoneType = Stone.self
Stoneのメタタイプ → Stoneのfull type metadata(FTM) → Stoneのnominal type descriptor(NTD)
普通のSwiftで、LLVM-IRにコンパイルしてメタタイプの中身を見る
$ swiftc -emit-ir -parse-as-library a.swift
@"$s1a5StoneVN" = hidden alias %swift.type,
bitcast (
i64* getelementptr inbounds (
<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, [4 x i8] }>,
<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, [4 x i8] }>* @"$s1a5StoneVMf",
i32 0, i32 1
)
to %swift.type*
)
メタタイプはfull type metadataのオフセットしたポインタ
@"$s1a5StoneVMf" = internal constant <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, [4 x i8] }>
<{
i8** @"$sBi64_WV",
i64 512,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0,
[4 x i8] zeroinitializer
}>,
align 8
full type metadataはフィールドとしてnominal type descriptorへのポインタを持つ
@"$s1a5StoneVMn" = hidden constant <{ i32, i32, i32, i32, i32, i32, i32 }>
<{
i32 81,
i32 trunc (
i64 sub (
i64 ptrtoint (<{ i32, i32, i32 }>* @"$s1aMXM" to i64),
i64 ptrtoint (
i32* getelementptr inbounds (
<{ i32, i32, i32, i32, i32, i32, i32 }>,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0, i32 1
)
to i64
)
)
to i32
),
i32 trunc (
i64 sub (
i64 ptrtoint ([6 x i8]* @1 to i64),
i64 ptrtoint (
i32* getelementptr inbounds (
<{ i32, i32, i32, i32, i32, i32, i32 }>,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0, i32 2
)
to i64
)
)
to i32
),
i32 trunc (
i64 sub (
i64 ptrtoint (%swift.metadata_response (i64)* @"$s1a5StoneVMa" to i64),
i64 ptrtoint (
i32* getelementptr inbounds (
<{ i32, i32, i32, i32, i32, i32, i32 }>,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0, i32 3
)
to i64
)
)
to i32
),
i32 trunc (
i64 sub (
i64 ptrtoint ({ i32, i32, i16, i16, i32, i32, i32, i32 }* @"$s1a5StoneVMF" to i64),
i64 ptrtoint (
i32* getelementptr inbounds (
<{ i32, i32, i32, i32, i32, i32, i32 }>,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0, i32 4
)
to i64
)
)
to i32
),
i32 1,
i32 2
}>,
section "__TEXT,__const", align 4
nominal type descriptor。6つのi32の定数式。
@"$s1a5StoneVMn" = hidden constant <{ i32, i32, i32, i32, i32, i32, i32 }>
<{
i32 81,
i32 trunc (
i64 sub (
i64 ptrtoint (<{ i32, i32, i32 }>* @"$s1aMXM" to i64),
i64 ptrtoint (
i32* getelementptr inbounds (
<{ i32, i32, i32, i32, i32, i32, i32 }>,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0, i32 1
)
to i64
)
)
to i32
),
; ...
}>,
section "__TEXT,__const", align 4
自己参照して相対アドレスを計算している
アセンブリにコンパイルして見てみる
$ swiftc -emit-assembly -parse-as-library a.swift
.set _$s1a5StoneVN, _$s1a5StoneVMf+8
_$s1a5StoneVMf:
.quad _$sBi64_WV
.quad 512
.quad _$s1a5StoneVMn
.long 0
.space 4
メタタイプはFTMのオフセットポインタ FTMはNTDをフィールドに持つ
_$s1a5StoneVMn:
.long 81
.long (_$s1aMXM-_$s1a5StoneVMn)-4
.long (l___unnamed_2-_$s1a5StoneVMn)-8
.long (_$s1a5StoneVMa-_$s1a5StoneVMn)-12
.long (_$s1a5StoneVMF-_$s1a5StoneVMn)-16
.long 1
.long 2
.section __DATA,__const
.p2align 3
相対アドレスの計算
バイナリを見てみる
$ swiftc -emit-object -parse-as-library a.swift
$ objdump -x -s a.o
SYMBOL TABLE:
00000000000000d0 g O __TEXT,__const _$s1a5StoneVMn
00000000000000f0 l O __DATA,__const _$s1a5StoneVMf
00000000000000f8 g O __DATA,__const _$s1a5StoneVN
メタタイプはFTMのオフセットポインタ
Contents of section __const:
00b8 61000000 00000000 00000000 f8ffffff a...............
00c8 53746f6e 65000000 51000000 fcffffff Stone...Q.......
00d8 f8ffffff f4ffffff f0ffffff 01000000 ................
00e8 02000000 0300 ......
Contents of section __const:
00f0 00000000 00000000 00020000 00000000 ................
0100 00000000 00000000 00000000 00000000 ................
00d0と00f0のダンプ
0x00F0 _$s1a5StoneVMf:
0x00F0 00000000 00000000 .quad _$sBi64_WV
0x00F8 00020000 00000000 .quad 512
0x0100 00000000 00000000 .quad _$s1a5StoneVMn
0x0108 00000000 .long 0
0x010C 00000000 .space 4
FTMをアセンブリと並べる
RELOCATION RECORDS FOR [__const]:
0000000000000000 X86_64_RELOC_UNSIGNED _$sBi64_WV
ポインタを書き込む部分はRelocation Recordで表現される
0x00D0 _$s1a5StoneVMn:
0x00D0 51000000 .long 81
0x00D4 fcffffff .long (_$s1aMXM-_$s1a5StoneVMn)-4
0x00D8 f8ffffff .long (l___unnamed_2-_$s1a5StoneVMn)-8
0x00DC f4ffffff .long (_$s1a5StoneVMa-_$s1a5StoneVMn)-12
0x00E0 f0ffffff .long (_$s1a5StoneVMF-_$s1a5StoneVMn)-16
0x00E4 01000000 .long 1
0x00E8 02000000 .long 2
NTDをアセンブリと並べる オフセットが初期値になっている
// セクションは0x00B8開始, 0x00B8 + 0x001C = 0x00D4
RELOCATION RECORDS FOR [__const]:
0000000000000028 X86_64_RELOC_SUBTRACTOR _$s1a5StoneVMF-_$s1a5StoneVMn
0000000000000024 X86_64_RELOC_SUBTRACTOR _$s1a5StoneVMa-_$s1a5StoneVMn
0000000000000020 X86_64_RELOC_SUBTRACTOR l___unnamed_2-_$s1a5StoneVMn
000000000000001c X86_64_RELOC_SUBTRACTOR _$s1aMXM-_$s1a5StoneVMn
相対アドレスを構築するRelocation Recordがある
この仕様のドキュメント
https://opensource.apple.com/source/cctools/cctools-773/include/mach-o/x86_64/reloc.h
* .quad _foo - _bar + 4
* r_type=X86_64_RELOC_SUBTRACTOR, r_length=3, r_extern=1, r_pcrel=0, r_symbolnum=_bar
* r_type=X86_64_RELOC_UNSIGNED, r_length=3, r_extern=1, r_pcrel=0, r_symbolnum=_foo
* 04 00 00 00 00 00 00 00
// class ConstantAggregateBuilderBase
void addRelativeAddress(llvm::Constant *target) {
assert(!isa<llvm::ConstantPointerNull>(target));
+ if (IGM().TargetInfo.OutputObjectFormat == llvm::Triple::Wasm) {
+ // WebAssembly: hack: doesn't support PCrel data relocations
+ add(llvm::ConstantExpr::getPtrToInt(target, IGM().RelativeAddressTy, false));
+ return;
+ }
addRelativeOffset(IGM().RelativeAddressTy, target);
}
addRelativeAddress
の実装を絶対アドレスに差し替えてる
WASM改造Swiftで見てみる
args = [
"swiftc",
"-emit-ir",
"-target", "wasm32-unknown-unknown-wasm",
"-sdk", "/work/wasi-sdk/share/sysroot",
"a.swift"
]
@"$s1a5StoneVN" = hidden alias %swift.type,
bitcast (
i32* getelementptr inbounds (
<{ i8**, i32, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32 }>,
<{ i8**, i32, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32 }>* @"$s1a5StoneVMf",
i32 0, i32 1
)
to %swift.type*
)
メタタイプ→FTMは変化なし
@"$s1a5StoneVMf" = internal constant <{ i8**, i32, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32 }>
<{
i8** @"$sBi32_WV",
i32 512,
<{ i32, i32, i32, i32, i32, i32, i32 }>* @"$s1a5StoneVMn",
i32 0
}>,
align 4
FTM→NTDは変化なし
@"$s1a5StoneVMn" = hidden constant <{ i32, i32, i32, i32, i32, i32, i32 }>
<{
i32 81,
i32 ptrtoint (<{ i32, i32, i32 }>* @"$s1aMXM" to i32),
i32 ptrtoint ([6 x i8]* @1 to i32),
i32 ptrtoint (%swift.metadata_response (i32)* @"$s1a5StoneVMa" to i32),
i32 ptrtoint ({ i32, i32, i16, i16, i32, i32, i32, i32 }* @"$s1a5StoneVMF" to i32),
i32 1,
i32 2
}>,
section ".rodata", align 4
相対アドレスの計算がなくなりアドレスをそのまま格納
アセンブリを見てみる
args = [
"swiftc",
"-emit-assembly",
"-target", "wasm32-unknown-unknown-wasm",
"-sdk", "/work/wasi-sdk/share/sysroot",
"a.swift"
]
.set $s1a5StoneVN, ($s1a5StoneVMf)+4
$s1a5StoneVMf:
.int32 ($sBi32_WV)
.int32 512
.int32 ($s1a5StoneVMn)
.int32 0
$s1a5StoneVMn:
.int32 81
.int32 ($s1aMXM)
.int32 .L__unnamed_2
.int32 ($s1a5StoneVMa)@FUNCTION
.int32 ($s1a5StoneVMF)
.int32 1
.int32 2
オブジェクトファイルを見てみる
args = [
"swiftc",
"-emit-object",
"-target", "wasm32-unknown-unknown-wasm",
"-sdk", "/work/wasi-sdk/share/sysroot",
"a.swift"
]
$ wasm-objdump -s -x a.o
Custom:
- name: "linking"
- symbol table [count=27]
- 26: D <$s1a5StoneVN> segment=3 offset=4 size=12 binding=global vis=hidden
- 11: D <$s1a5StoneVMf> segment=3 offset=0 size=16 binding=local vis=default
- 16: D <$s1a5StoneVMn> segment=1 offset=12 size=28 binding=global
- segment[3] <.data.rel.ro.$s1a5StoneVMf> size=16 - init i32=52
- 0000034: 0000 0000 0002 0000 1000 0000 0000 0000 ................
000034: R_WASM_MEMORY_ADDR_I32 18 <$sBi32_WV>
00003c: R_WASM_MEMORY_ADDR_I32 16 <$s1a5StoneVMn>
- segment[1] <.rodata> size=40 - init i32=4
- 0000004: 0000 0000 0000 0000 0000 0000 5100 0000 ............Q...
- 0000014: 0400 0000 2c00 0000 0200 0000 5400 0000 ....,.......T...
- 0000024: 0100 0000 0200 0000 ........
00000c: R_WASM_MEMORY_ADDR_I32 13 <.L__unnamed_1>
000014: R_WASM_MEMORY_ADDR_I32 14 <$s1aMXM>
000018: R_WASM_MEMORY_ADDR_I32 15 <.L__unnamed_2>
00001c: R_WASM_TABLE_INDEX_I32 10 <$s1a5StoneVMa>
000020: R_WASM_MEMORY_ADDR_I32 17 <$s1a5StoneVMF>
-
呼び出し規約
-
関数を呼び出しの引数や返り値を、メモリやレジスタをどのように使用して受け渡しするかの規約
-
Swiftは、Loweringした後は概ねC言語の呼び出し規約を踏襲する。Objective-CやCとの相互呼び出しが簡単。
-
ただし、Context pointerと例外ハンドリングについては、専用の規約を追加している。
-
この2つに関してWASMでの対応が困難。
-
メソッド呼び出しにおいてレシーバ(
self
)を指すポインタ -
クロージャにおいてコンテキストオブジェクトを指すポインタ
- キャプチャしたオブジェクトを配置するストレージのこと
-
これを専用のレジスタに割り当てる規約になっている
struct Stone {
var weight: Int = 3
}
プロパティアクセサが作られる
LLVM-IR
; a.Stone.weight.setter : Swift.Int
define hidden swiftcc void @"$s1a5StoneV6weightSivs"(
i64,
%T1a5StoneV* nocapture swiftself dereferenceable(8)) #0
{
; ...
}
LLVMは、swiftself
のついた引数を専用のレジスタに割り当てる。x86_64なら%r13
。
[.code-highlight: 1, 8]
_$s1a5StoneV6weightSivs:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movq %rdi, (%r13)
popq %rbp
retq
.cfi_endproc
.private_extern _$s1a5StoneV6weightSivM
.globl _$s1a5StoneV6weightSivM
.p2align 4, 0x90
第1引数(%rdi
)をself(%r13
)の0オフセットにコピーしている
-
クロージャは、クロージャ関数とコンテキストオブジェクトのペアで表現される。
-
コンテキストオブジェクトはキャプチャした変数を保持するストレージ。
-
クロージャの呼び出しにおいては、このコンテキストオブジェクトが、ペアのクロージャ関数に渡される。
- キャプチャをしないクロージャはコンテキストオブジェクトを持たない単体の関数で表現される。その関数はコンテキストオブジェクトを受け取る引数を持たない。
-
型システム的には、キャプチャをしないクロージャは、キャプチャをするクロージャと同様に扱いたい。
-
パフォーマンスのため、thunkでの変換を挟まずにそのまま使いたい。
-
キャプチャなしクロージャの関数を、クロージャ関数として直接呼び出すと、受け取る引数の無いコンテキストオブジェクトが不正に渡される事になる。
-
それを専用のレジスタで渡す事により、通常の引数群と独立させる。
-
受け取らない関数においては、呼び出され側で単にそのレジスタに触れないことで、ABI互換性が実現される。
func f(_ g: (Int) -> Int) { g(3) }
func main() {
let a = 2
f { $0 * a }
}
fに渡すクロージャにaをキャプチャさせる
[.code-highlight: 1-2, 10-17]
; main
define hidden swiftcc void @"$s1a4mainyyF"() #0 {
entry:
%0 = alloca i8, i64 24, align 16
%1 = bitcast i8* %0 to %swift.opaque*
%2 = bitcast %swift.opaque* %1 to <{ %swift.refcounted, %TSi }>*
%3 = getelementptr inbounds <{ %swift.refcounted, %TSi }>,
<{ %swift.refcounted, %TSi }>* %2, i32 0, i32 1
%._value = getelementptr inbounds %TSi, %TSi* %3, i32 0, i32 0
store i64 2, i64* %._value, align 8
call swiftcc void @"$s1a1fyyS2iXEF"(
i8* bitcast (
i64 (i64, %swift.refcounted*)* @"$s1a4mainyyFS2iXEfU_TA"
to i8*
),
%swift.opaque* %1
)
ret void
}
f($s1a1fyyS2iXEF
)に渡すクロージャが、関数ポインタ($s1a4mainyyFS2iXEfU_TA
)とコンテキストオブジェクト(%1
)で表現されている。
[.code-highlight: 1-2, 17]
; f(caller)
define hidden swiftcc void @"$s1a1fyyS2iXEF"(i8*, %swift.opaque*) #0 {
entry:
%g.debug = alloca %swift.noescape.function, align 8
%2 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 16, i1 false)
%3 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.lifetime.start.p0i8(i64 16, i8* %3)
%g.debug.fn = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 0
store i8* %0, i8** %g.debug.fn, align 8
%g.debug.data = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 1
store %swift.opaque* %1, %swift.opaque** %g.debug.data, align 8
%4 = bitcast i8* %0 to i64 (i64, %swift.refcounted*)*
%5 = bitcast %swift.opaque* %1 to %swift.refcounted*
%6 = call swiftcc i64 %4(i64 3, %swift.refcounted* swiftself %5)
ret void
}
mainから渡ってきたコンテキストオブジェクト(%1
, %5
)を、そのままクロージャ関数(%0
, %4
)に渡している。
[.code-highlight: 2-4, 12]
; クロージャ(callee)
define internal swiftcc i64 @"$s1a4mainyyFS2iXEfU_TA"(
i64,
%swift.refcounted* swiftself) #0
{
entry:
%2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, %TSi }>*
%3 = getelementptr inbounds <{ %swift.refcounted, %TSi }>,
<{ %swift.refcounted, %TSi }>* %2, i32 0, i32 1
%._value = getelementptr inbounds %TSi, %TSi* %3, i32 0, i32 0
%4 = load i64, i64* %._value, align 8
%5 = tail call swiftcc i64 @"$s1a4mainyyFS2iXEfU_"(i64 %0, i64 %4)
ret i64 %5
}
コンテキストから値を取り出して本体関数に転送。
func f(_ g: (Int) -> Int) { g(3) }
func main() {
f { $0 * 2 }
}
; main
define hidden swiftcc void @"$s1a4mainyyF"() #0 {
entry:
call swiftcc void @"$s1a1fyyS2iXEF"(
i8* bitcast (
i64 (i64)* @"$s1a4mainyyFS2iXEfU_"
to i8*
),
%swift.opaque* null)
ret void
}
コンテキストオブジェクトがnull。
[.code-highlight: 1-2, 17]
; f(caller)は全く同じ
define hidden swiftcc void @"$s1a1fyyS2iXEF"(i8*, %swift.opaque*) #0 {
entry:
%g.debug = alloca %swift.noescape.function, align 8
%2 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %2, i8 0, i64 16, i1 false)
%3 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.lifetime.start.p0i8(i64 16, i8* %3)
%g.debug.fn = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 0
store i8* %0, i8** %g.debug.fn, align 8
%g.debug.data = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 1
store %swift.opaque* %1, %swift.opaque** %g.debug.data, align 8
%4 = bitcast i8* %0 to i64 (i64, %swift.refcounted*)*
%5 = bitcast %swift.opaque* %1 to %swift.refcounted*
%6 = call swiftcc i64 %4(i64 3, %swift.refcounted* swiftself %5)
ret void
}
当然fは同じコードになるのでコンテキストオブジェクトをクロージャ関数に転送しているが、動く。
[.code-highlight: 2]
; クロージャ(callee)
define internal swiftcc i64 @"$s1a4mainyyFS2iXEfU_"(i64) #0 {
entry:
%"$0.debug" = alloca i64, align 8
%1 = bitcast i64* %"$0.debug" to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %1, i8 0, i64 8, i1 false)
store i64 %0, i64* %"$0.debug", align 8
%2 = call { i64, i1 } @llvm.smul.with.overflow.i64(i64 %0, i64 2)
%3 = extractvalue { i64, i1 } %2, 0
%4 = extractvalue { i64, i1 } %2, 1
br i1 %4, label %6, label %5
; <label>:5: ; preds = %entry
ret i64 %3
; <label>:6: ; preds = %entry
call void @llvm.trap()
unreachable
}
クロージャはコンテキストオブジェクトを受け取る引数を持たない。
-
Swiftは例外を返す可能性があるかどうかを、関数の型として静的に検査する。
-
例外の送出はエラーのダブルポインタ型を引数に受ける間接返し。
-
呼び出し側でエラーポインタへのポインタを引数に渡し、呼び出され側でエラーオブジェクトを書き込む。
-
型システム的には、例外を投げない関数であっても、例外を投げうる関数として扱いたい。
-
パフォーマンスのため、thunkでの変換を挟まずにそのまま使いたい。
-
例外を投げない関数を、例外を投げうる関数として直接呼び出すと、受け取る引数の無いダブルポインタが不正に渡されることになる。
-
それを専用のレジスタで表現する事により、通常の引数と独立させる。
-
例外を投げない関数においては、単にそのレジスタを使わないコードとして、ABI互換性が実現される。
-
レジスタが特定されているのでダブルポインタを使う必要もなくなる。
extension Int : Error { }
func e() throws {
throw 999
}
func f(_ g: () throws -> Void) rethrows {
try g()
}
func main() {
try! f(e)
}
例外を投げる関数を、高階関数に渡すコード。
[.code-highlight: 1-2, 6-14]
; main
define hidden swiftcc void @"$s1a4mainyyF"() #0 {
entry:
%swifterror = alloca swifterror %swift.error*, align 8
store %swift.error* null, %swift.error** %swifterror, align 8
call swiftcc void @"$s1a1fyyyyKXEKF"(
i8* bitcast (
void (%swift.refcounted*, %swift.error**)* @"$s1a1eyyKF"
to i8*
),
%swift.opaque* null,
%swift.refcounted* swiftself undef,
%swift.error** noalias nocapture swifterror dereferenceable(8) %swifterror
)
%0 = load %swift.error*, %swift.error** %swifterror, align 8
%1 = icmp ne %swift.error* %0, null
br i1 %1, label %3, label %2
; <label>:2: ; preds = %entry
ret void
; <label>:3: ; preds = %entry
%4 = phi %swift.error* [ %0, %entry ]
store %swift.error* null, %swift.error** %swifterror, align 8
call swiftcc void @swift_unexpectedError(
%swift.error* %4,
i8* getelementptr inbounds ([8 x i8], [8 x i8]* @0, i64 0, i64 0),
i64 7, i1 true, i64 8)
unreachable
}
呼び出し側では、エラーポインタをスタックに確保して(%swifterror
)、fに渡す。呼び出し後、エラーを取り出して(%0
)分岐する
[.code-highlight: 1-6, 21-26]
; caller(f)
define hidden swiftcc void @"$s1a1fyyyyKXEKF"(
i8*,
%swift.opaque*,
%swift.refcounted* swiftself,
%swift.error** noalias nocapture swifterror dereferenceable(8)) #0 {
entry:
%g.debug = alloca %swift.noescape.function, align 8
%4 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %4, i8 0, i64 16, i1 false)
%5 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.lifetime.start.p0i8(i64 16, i8* %5)
%g.debug.fn = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 0
store i8* %0, i8** %g.debug.fn, align 8
%g.debug.data = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 1
store %swift.opaque* %1, %swift.opaque** %g.debug.data, align 8
%6 = bitcast i8* %0 to void (%swift.refcounted*, %swift.error**)*
%7 = bitcast %swift.opaque* %1 to %swift.refcounted*
call swiftcc void %6(
%swift.refcounted* swiftself %7,
%swift.error** noalias nocapture swifterror dereferenceable(8) %3)
%8 = load %swift.error*, %swift.error** %3, align 8
%9 = icmp ne %swift.error* %8, null
br i1 %9, label %11, label %10
; <label>:10: ; preds = %entry
ret void
; <label>:11: ; preds = %entry
%12 = phi %swift.error* [ %8, %entry ]
store %swift.error* null, %swift.error** %3, align 8
store %swift.error* %12, %swift.error** %3, align 8
ret void
}
例外の再送では、自身に渡されたダブルポインタ(%3)
をそのまま内側の関数に渡している。
呼び出した直後、エラーを取り出して(%8
)分岐する。
[.code-highlight: 1-4, 15]
; e(throw)
define hidden swiftcc void @"$s1a1eyyKF"(
%swift.refcounted* swiftself,
%swift.error** noalias nocapture swifterror dereferenceable(8)) #0
{
entry:
%2 = call i8** @"$sS2is5Error1aWl"() #6
%3 = call swiftcc { %swift.error*, %swift.opaque* }
@swift_allocError(%swift.type* @"$sSiN", i8** %2, %swift.opaque* null, i1 false)
%4 = extractvalue { %swift.error*, %swift.opaque* } %3, 0
%5 = extractvalue { %swift.error*, %swift.opaque* } %3, 1
%6 = bitcast %swift.opaque* %5 to %TSi*
%._value = getelementptr inbounds %TSi, %TSi* %6, i32 0, i32 0
store i64 999, i64* %._value, align 8
store %swift.error* %4, %swift.error** %1, align 8
call swiftcc void @swift_willThrow(
i8* swiftself undef,
%swift.error** noalias nocapture readonly swifterror dereferenceable(8) %1) #3
store %swift.error* null, %swift.error** %1, align 8
store %swift.error* %4, %swift.error** %1, align 8
ret void
}
例外を投げる処理では、引数で受けたダブルポインタ(%1)
の参照先に、生成したエラーオブジェクト(%4)
を書き込んでいる。
LLVMは、swifterror
のついた引数を専用のレジスタに割り当てる。x86_64なら%r12
。
[.code-highlight: 1, 19-22]
_$s1a4mainyyF:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
pushq %r13
pushq %r12
subq $48, %rsp
.cfi_offset %r12, -32
.cfi_offset %r13, -24
xorl %ecx, %ecx
movl %ecx, %edx
leaq _$s1a1eyyKF(%rip), %rdi
movq %rdx, %rsi
movq %rdx, %r12
movq %rax, -32(%rbp)
callq _$s1a1fyyyyKXEKF
cmpq $0, %r12
movq %r12, -40(%rbp)
jne LBB3_2
addq $48, %rsp
popq %r12
popq %r13
popq %rbp
retq
LBB3_2:
; ...
call
の直後に%r12
をcmpq
している
func e() {}
func f(_ g: () throws -> Void) rethrows {
try g()
}
func main() {
try! f(e)
}
[.code-highlight: 1-2, 6-10]
; main
define hidden swiftcc void @"$s1a4mainyyF"() #0 {
entry:
%swifterror = alloca swifterror %swift.error*, align 8
store %swift.error* null, %swift.error** %swifterror, align 8
call swiftcc void @"$s1a1fyyyyKXEKF"(
i8* bitcast (void ()* @"$s1a1eyyF" to i8*),
%swift.opaque* null,
%swift.refcounted* swiftself undef,
%swift.error** noalias nocapture swifterror dereferenceable(8) %swifterror)
%0 = load %swift.error*, %swift.error** %swifterror, align 8
%1 = icmp ne %swift.error* %0, null
br i1 %1, label %3, label %2
; <label>:2: ; preds = %entry
ret void
; <label>:3: ; preds = %entry
%4 = phi %swift.error* [ %0, %entry ]
store %swift.error* null, %swift.error** %swifterror, align 8
unreachable
}
fを呼び出す側は同じような形。
[.code-highlight: 1-6, 21-26]
; f(caller)
define hidden swiftcc void @"$s1a1fyyyyKXEKF"(
i8*,
%swift.opaque*,
%swift.refcounted* swiftself,
%swift.error** noalias nocapture swifterror dereferenceable(8)) #0 {
entry:
%g.debug = alloca %swift.noescape.function, align 8
%4 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.memset.p0i8.i64(i8* align 8 %4, i8 0, i64 16, i1 false)
%5 = bitcast %swift.noescape.function* %g.debug to i8*
call void @llvm.lifetime.start.p0i8(i64 16, i8* %5)
%g.debug.fn = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 0
store i8* %0, i8** %g.debug.fn, align 8
%g.debug.data = getelementptr inbounds %swift.noescape.function,
%swift.noescape.function* %g.debug, i32 0, i32 1
store %swift.opaque* %1, %swift.opaque** %g.debug.data, align 8
%6 = bitcast i8* %0 to void (%swift.refcounted*, %swift.error**)*
%7 = bitcast %swift.opaque* %1 to %swift.refcounted*
call swiftcc void %6(
%swift.refcounted* swiftself %7,
%swift.error** noalias nocapture swifterror dereferenceable(8) %3)
%8 = load %swift.error*, %swift.error** %3, align 8
%9 = icmp ne %swift.error* %8, null
br i1 %9, label %11, label %10
; <label>:10: ; preds = %entry
ret void
; <label>:11: ; preds = %entry
%12 = phi %swift.error* [ %8, %entry ]
store %swift.error* null, %swift.error** %3, align 8
store %swift.error* %12, %swift.error** %3, align 8
ret void
}
クロージャを呼び出すfは全く同じ。
; e(callee)
define hidden swiftcc void @"$s1a1eyyF"() #0 {
entry:
ret void
}
クロージャ自身は引数のエラーダブルポインタが無いが動く。
-
Swiftの呼び出し規約は、swiftselfとswifterrorに関して、専用レジスタを用いた効率的な実装をもつ。
-
しかし、WASMはレジスタを持たないスタックマシンであるため、専用レジスタを割り当てることができない。
-
さらに、ランタイムが関数呼び出し時に渡した引数の個数をチェックをするため、そこでクラッシュしてしまう。
@inline(never)
func f(_ g: () throws -> Void) { try! g() }
f { }
呼び出し側f
はエラーダブルポインタをg
に渡すが、g
は例外を投げない空のクロージャなので、そのダブルポインタを受け取る引数を持たない。
Running WebAssembly...
Error: call_indirect to a signature
that does not match (evaluating 'obj.instance.exports._start()')
<?>.wasm-function[33]@[wasm code]
<?>.wasm-function[31]@[wasm code]
<?>.wasm-function[29]@[wasm code]
wasm-stub@[wasm code]
_start@[native code]
https://swiftwasm.org/home_polyfill/polyfill.js:2205:263
promiseReactionJob@[native code]
シグネチャの合わない関数を呼び出そうとした感じのエラー。
- SwiftコアチームのJoe Groffが、すべてのswiftcc関数の引数にswiftselfとswifterrorを与えるという対応を提案している。
-
SwiftのWASM対応が進んでいる
-
Relative Pointerの利用箇所がとりあえず修正された
-
swiftccへの対応が待たれる
Footnotes
-
Unable to compile a simple Swift file (maybe not possible?) ↩
-
https://github.com/swiftwasm/swiftwasm-sdk/blob/master/ci-build.sh ↩
-
https://github.com/swiftwasm/swiftwasm-compile-service/blob/master/downloadPrebuilts.sh ↩
-
https://github.com/swiftwasm/swiftwasm-compile-service/blob/master/unpackPrebuilts.sh ↩
-
https://github.com/swiftwasm/swiftwasm-compile-service/blob/master/service.js ↩