Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

slidenumber: true autoscale: true

最近のSwiftのWASM対応

@omochimetaru

Emscripten & WebAssembly night !! #8


right

自己紹介

  • 名前: omochimetaru

  • 趣味: Swiftコンパイラいじり

  • SNS: Twitter^1, Qiita^2, Speaker Deck^3, GitHub^4


Swift^5

  • 特徴: ネイティブコンパイル(LLVM), 静的型付け, null安全, 値型, 関数型, 検査例外, ...

  • 作者: Apple, クリスラトナー

  • 用途: iOS開発


SwiftのWASMコンパイル

  • Q: LLVMに落ちるならemscriptenでなんとかならんのか?

  • A: ならなかった[^6]

[^6]: Unable to compile a simple Swift file (maybe not possible?)


swiftwasm


swiftwasm.org

ブラウザ上で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依存バイナリ

WASIを利用する形にビルドされていて、WasmtimeやLucetで動くらしい。^7


ソース

関連ソースが一通り公開されている

https://github.com/swiftwasm


SwiftコンパイラのWASM対応改造

必要な作業の解説がPR上にある

https://github.com/apple/swift/pull/24684


重要な作業

  • メタデータのRelative PointerをAbsolute Pointerに変更 これのおかげで動くようになった

  • Swift Calling Conventionへの対応 これがまだなので未完成 特定のコードでクラッシュする


WASM対応コンパイラを使う


一通りのソースがあるので自分で試すことができる。環境は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};
    }
    /* ... */
}

WASM向け改造の詳細


Relative Pointer

アドレスの代わりに、そこからのオフセットで表現するポインタ。


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の動機

(僕の理解)

ビット幅が小さくて済む

その部分が静的に再配置可能になる


リンカとRelative Pointer

オブジェクトファイルにおけるRelative Pointer定数の定義は、シンボル間オフセットとして記述できる。つまり、リンカがサポートするギミック。

しかしWASMが対応していない。


Relative Pointerの使用箇所

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>

Calling Convention

  • 呼び出し規約

  • 関数を呼び出しの引数や返り値を、メモリやレジスタをどのように使用して受け渡しするかの規約


Swift Calling Convention (swiftcc)

  • Swiftは、Loweringした後は概ねC言語の呼び出し規約を踏襲する。Objective-CやCとの相互呼び出しが簡単。

  • ただし、Context pointerと例外ハンドリングについては、専用の規約を追加している。

  • この2つに関してWASMでの対応が困難。


Context pointer

  • メソッド呼び出しにおいてレシーバ(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オフセットにコピーしている


Swiftのクロージャ

  • クロージャは、クロージャ関数とコンテキストオブジェクトのペアで表現される。

  • コンテキストオブジェクトはキャプチャした変数を保持するストレージ。

  • クロージャの呼び出しにおいては、このコンテキストオブジェクトが、ペアのクロージャ関数に渡される。


キャプチャなしクロージャ

  • キャプチャをしないクロージャはコンテキストオブジェクトを持たない単体の関数で表現される。その関数はコンテキストオブジェクトを受け取る引数を持たない。

キャプチャなしクロージャの互換性

  • 型システム的には、キャプチャをしないクロージャは、キャプチャをするクロージャと同様に扱いたい。

  • パフォーマンスのため、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の例外ハンドリング

  • 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の直後に%r12cmpqしている


例外を投げない例

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
}

クロージャ自身は引数のエラーダブルポインタが無いが動く。


WASMとswiftcc

  • 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への対応が待たれる

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.