『WebAssembly 決定版ガイド』 (7) WebAssembly テーブル

4e6ad10a3ad2ea804b92467c6fe3f843.gif

この記事は、「WebAssembly の決定版ガイド」シリーズの 7 番目の記事です。シリーズの記事のリスト:

翻訳者注: この記事は、WebAssembly テーブルの概念と使用法を紹介する書籍『The Definitive Guide to WebAssembly』の第 7 章です。テーブルは関数ポインターを格納するデータ構造であり、これによりモジュールが互いの関数を動的に呼び出すことができます。この記事では、テーブルのタイプ、要素、インポートとエクスポートの特性を分析し、C/C++ と JavaScript の間の相互運用性を含む、テーブルを使用するためのサンプル コードをいくつか示します。この記事は、テーブルの制限と今後の開発の方向性についての説明で終わります。

人々は夕食の席で自分の考えやストーリーを共有することがよくあります。一人で食べるよりも、誰かと一緒に食べる方が楽しいです。あらゆる立場の人々を集めれば、おそらく話題は尽きないでしょう。誰もすべてをカバーすることはできません。同じ物語の側面を共有する人もいるかもしれません。他にも独自のバージョンがある場合があります。それでも、ある程度の礼儀、自制心、そして他の参加者が提供するものを受け入れる意欲がなければなりません。ゲストが行儀を悪くしたり、おしゃべりしたり、お互いの意見を逸脱したりすると、全員のディナーが台無しになる可能性があります。

テーブルは、最新のソフトウェア システムとなる WebAssembly の機能であり、その機能の依存関係は追加のモジュールによって満たされます。静的リンク ライブラリと比較して、動的共有ライブラリと同等の機能を提供します。すべてのモジュールが機能するためにすべての機能が必要なわけではありません。これは非常に非効率的です。代わりに、他のモジュールが実行時に要件を満たすことを約束して書かれています。これは、C および C++ の世界では動的リンクと呼ばれます。もちろん、食卓理論は単なる言葉遊びであり、食事にマナーが必要であるのと同じように、図書館間の共有にも基準が必要です。このアイデアをさらに詳しく調べて、WebAssembly がそれをどのようにサポートしているかを見てみましょう。

静的リンクと動的リンク

Twitterで私をフォローしている人なら誰でも、私の妻がどれほど素晴らしい料理人であるかを知っています。彼女は偉大なシェフの家族の出身で、多くの巨匠から学ぶ機会がありました。彼女自身の料理に関する私の投稿を見て、レシピを尋ねられることがよくあります。彼女は複数のソースからアイデアを組み合わせて、それに自分なりのひねりを加えることが多いため、これは通常、リンクを送信するほど簡単ではありません。

我が家では、彼女はレシピの宝庫を頼りにしています。「あの本のソースを使ってこれを作ります。他の本に載っているテクニックで牛肉を作ります。牛肉が好みの焼き加減になったら、これらを加えると追加の材料がさらに美味しくなると思います。」と彼女は言うことができました。

私たちの家では、彼女は既知のソースからの手順と成分リストを参照し、追加の手順でプロセスを修正できます。しかし、レシピを他の人に渡したいと思ったとき、人々がその本を持っていることを黙認することはできませんでした。この場合、レシピをソースから完全なレシピ ファイルにコピーする必要があります。この時点で、すべての手順と材料が 1 か所で定義され、レシピを他の人に送信できます。

これは基本的に、静的リンクと動的リンクの違いです。一般的なプログラムでは、ファイルの内容の読み取りと書き込み、ウィンドウのオープン、ユーザー入力の収集、またはネットワーク経由でのメッ​​セージの送信が必要です。これらは一般的なタスクであり、多くの場合、オペレーティング システムが提供するライブラリの関数として利用できます。これらの関数のいずれかを使用したい場合は、実行時リンクを許可するようにリンカーに指示します。そうしないと、シンボル参照が欠落しているというメッセージが表示されます。

実行時に、オペレーティング システムは構成パスを検索して、これらの共有ライブラリの場所を通知します。プログラムを開始する前に、ライブラリ内の機能を、コードの残りの部分に動的にリンクできるメモリの場所にマップします。これには多くの理由があります。一つ目は効率の問題です。a () 他の 12 個のプログラムから参照される という関数があるとします 。静的リンクでは、各実行可能プログラムは独自のコピーを持ちます。プログラムはより多くのディスク領域を占有します。実行時のメモリ使用量も大きくなります。これにより、ディスクとメモリのスペースが無駄になります。

ダイナミック ライブラリが共有メモリ領域にロードされている場合、ディスク上に必要なのはファイルのコピー 1 つだけです。オペレーティング システムの複雑さによっては、メモリ内にコピーが 1 つだけ必要になる場合があります。

通常、ダイナミック リンク ライブラリには独自のリリース サイクルがあります。実行可能プログラムのシステム ライブラリを使用している場合は、オペレーティング システムを更新し、セキュリティ パッチを適用した新しいバージョンのライブラリを入手できます。番号付けメカニズムが機能し、下位互換性がある限り、他に何もせずにパッチを当てたバージョンを使用することでアプリケーションのセキュリティを強化できます。

例 7-1 を参照してください。これは独立した関数であり、 main () 関数はありません。ライブラリとして使用することを目的としています。これを静的ライブラリにコンパイルすることもできますが、ここではオブジェクト コードを作成して main () プログラムをそれにリンクするだけです。この関数は header にも依存する printf ()ため、 stdio.h ヘッダーをインポートする必要があることに注意してください。

例 7-1. 関数呼び出しを含むライブラリ

#include <stdio.h>

void sayHello (char *message) {
  printf ("% s\n", message);
}

例 7-2 では、 main () 関数が最初に呼び出され printf ()、次に関数が呼び出されます。この関数も呼び出されます printf ()

例 7-2. ライブラリ関数を呼び出すmain () 方法の例

#include <stdio.h>

extern void sayHello (char *message);

int main () {
  printf ("Hello, world.\n"); 
  sayHello ("How are you?"); 
  return 0;
}

デフォルトでは、これら 2 つのファイルを Clang でコンパイルすると、出力ファイルが生成されます。デフォルトの名前を使用します。実行すると、期待どおりの動作が見られます。デフォルトでは、コンパイラはシステム ライブラリに動的リンクを使用し、リストしたすべての要件を満たします。

brian@tweezer ~/g/w/s/ch07> clang main.c library.c brian@tweezer ~/g/w/s/ch07> ls
a.out* library.c main.c
brian@tweezer ~/g/w/s/ch07> ./a.out
Hello, world.
How are you?

nm コマンドを使用して、動的リンクが使用されていることを確認できます。まず、バイナリは main () と の定義を提供しますsayHello () が、 を提供しないこと がわかりますprintf ()これは標準ライブラリから再利用された関数です。

brian@tweezer ~/g/w/s/ch07> nm a.out 
0000000100008008 d __dyld_private 
0000000100000000 T __mh_execute_header
0000000100003f10 T _main
                 U _printf
0000000100003f50 T _sayHello
                 U dyld_stub_binder

Linux では、同じビルド ステップで追加機能を備えたバイナリが生成されることがわかります。ランタイムもバイナリ形式も異なるオペレーティング システムであるため、これは当然のことです。際立っているのは、私たちのメソッドはバイナリで提供されています printf () が、。

brian@bbfcfm:~/src/hello$ nm a.out
0000000000404030 B __bss_start
0000000000404030 b completed.8060
0000000000404020 D __data_start
0000000000404020 W data_start
0000000000401080 t deregister_tm_clones
0000000000401070 T _dl_relocate_static_pie
00000000004010f0 t __do_global_dtors_aux
0000000000403e08 d __do_global_dtors_aux_fini_array_entry 0000000000404028 D __dso_handle
0000000000403e10 d _DYNAMIC
0000000000404030 D _edata
0000000000404038 B _end
0000000000401218 T _fini
0000000000401120 t frame_dummy
0000000000403e00 d __frame_dummy_init_array_entry
000000000040216c r __FRAME_END__
0000000000404000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000402024 r __GNU_EH_FRAME_HDR
0000000000401000 T _init
0000000000403e08 d __init_array_end
0000000000403e00 d __init_array_start
0000000000402000 R _IO_stdin_used
0000000000401210 T __libc_csu_fini
00000000004011a0 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000401130 T main
                 U printf@@GLIBC_2.2.5
00000000004010b0 t register_tm_clones
0000000000401170 T sayHello
0000000000401040 T _start
0000000000404030 D __TMC_END__

otool コマンドは、macOS で使用できるもう 1 つのコマンドで、バイナリを正常に実行するために必要な動的ライブラリを示します。システム ライブラリの macOS バージョンを示します。

brian@tweezer ~/g/w/s/ch07> otool -L a.out 
a.out:
   /usr/lib/libSystem.B.dylib (compatibility vers 1.0.0, current vers 1292.60.1)

otool は Linux には存在しませんが、objdump を使用すると同様の結果が得られます。スペースを節約するために出力の一部を削除しましたが、関連する部分を以下のスニペットに示します。Windows には、DLL の依存関係を確認するための同様のツールがあります。ご覧のとおり、 libc.so.6 バイナリのニーズを満たす必要があります。

brian@bbfcfm:~/src/hello$ objdump -x a.out
    a.out:     file format elf64-x86-64
    a.out
    architecture: i386:x86-64, flags 0x00000112:
    EXEC_P, HAS_SYMS, D_PAGED
    start address 0x0000000000401040
...
Dynamic Section:
  NEEDED         libc.so.6
  INIT           0x0000000000401000
  FINI           0x0000000000401218
  INIT_ARRAY     0x0000000000403e00
  INIT_ARRAYSZ   0x0000000000000008
  FINI_ARRAY     0x0000000000403e08
  FINI_ARRAYSZ   0x0000000000000008
  HASH           0x00000000004002e8
  GNU_HASH       0x0000000000400310
  STRTAB         0x0000000000400390
  SYMTAB         0x0000000000400330
  STRSZ          0x000000000000003f
  SYMENT         0x0000000000000018
  DEBUG           0x0000000000000000
  PLTGOT         0x0000000000404000
  PLTRELSZ       0x0000000000000018
  PLTREL         0x0000000000000007
  JMPREL         0x0000000000400428
  RELA           0x00000000004003f8
  RELASZ         0x0000000000000030
  RELAENT         0x0000000000000018
  VERNEED         0x00000000004003d8
  VERNEEDNUM     0x0000000000000001
  VERSYM         0x00000000004003d0
Version References:
  required from libc.so.6:
    0x09691a75 0x00 02 GLIBC_2.2.5
...

WebAssembly は明らかにオペレーティング システムと同じものではありませんが、同様の概念の恩恵を受けています。オプションは同じです。すべての関数定義をモジュールに入れてスタンドアロンできるようにするか、ニーズに合わせて別のモジュールから動作を呼び出します。WebAssembly モジュールをネットワーク経由でダウンロードすることが多いことを考慮すると、モジュールを小さくしておくことが望ましいです。これは、ディスク ストレージ、モジュールの検証、メモリ内のインスタンスのロードなどにも影響します。このために、Table インスタンスを使用します。

モジュール内にテーブルを作成する

Table インスタンスには、第 4 章で紹介した Memory インスタンスと同様の特性がいくつかあります[1] 。現在、モジュールごとに 1 つしか存在できませんが、モジュール内で定義することも、インポートされたオブジェクトを介して渡すこともできます。モジュールごとに 1 つのインスタンスという制限は将来的に解除される可能性がありますが、現時点ではこれを遵守する必要があります。

WebAssembly では、単に Memory インスタンスを使用するのではなく、この構造を使用します。その理由の 1 つは、後者はモジュールで操作できるためです。夕食時の会話では、参加者個人が行動規範を書き換えることは望ましくありません。共有モジュールについても同様です。テーブル インスタンスを介して関数をエクスポートするモジュールをロードして検証した場合、別のモジュールが他のモジュールに問題を引き起こすことは望ましくありません。したがって、できることは、テーブルに格納されている関数参照に対して間接的な関数呼び出しを行うことだけです。現在、テーブル インスタンスに格納できるのは関数参照のみですが、これも将来的には変更されることが予想されます。

この時点では、物事を複雑にしすぎず、Wat の単純な関数定義に戻って、テーブル インスタンスを作成してエクスポートする方法を説明します。

例 7-3 では、2 つの関数を作成しました。$add この関数は 2 つの引数を受け取り、それらを加算して結果を返します。$sub この関数は 2 つのパラメーターを受け取り、最初のパラメーターから 2 番目のパラメーターを減算し、結果を返します。だから何?これは単なる前の章の復習です。ここでの違いは、次に何が起こるかです。

例 7-3. テーブルインスタンスをエクスポートするモジュール

(module
  (func $add (param $a i32) (param $b i32) (result i32)
      local.get $a
      local.get $b
      i32.add)

  (func $sub (param $a i32) (param $b i32) (result i32)
      local.get $a
      local.get $b
      i32.sub)

  (table (export "tbl") funcref (elem $add $sub))
)

新しいワットキーワードテーブルを導入しました。これは関数参照のコレクションを定義します。インライン エクスポート コマンドに注意してください。これにより、ホスト環境の呼び出し $add と $sub 関数が許可されますが、関数名による呼び出しは許可されません。ホストは、テーブル インスタンスを通じてのみこれら 2 つの関数を呼び出すことができます。前に指摘したように、Anyfunc 型は現在、この構造体に許可されている唯一の型です。要素参照の順序に従って、$add それは 0 番目の位置になり、$sub それは 1 番目の位置 [^1] になります。

以下に示すように、Wat ファイルを Wasm モジュールに変換し、その内容を検査できます。テーブルセクション、タイプセクション、およびエクスポートセクションに注意してください。

brian@tweezer ~/g/w/s/ch07> wat2wasm math.wat 
brian@tweezer ~/g/w/s/ch07> wasm-objdump -x math.wasm
    math.wasm:      file format wasm 0x1
    Section Details:
    Type [1]:
     - type [0] (i32, i32) -> i32
    Function [2]:
     - func [0] sig=0
     - func [1] sig=0
    Table [1]:
     - table [0] type=funcref initial=2 max=2
    Export [1]:
     - table [0] -> "tbl"
    Elem [1]:
     - segment [0] flags=0 table=0 count=2 - init i32=0
      - elem [0] = func [0]
      - elem [1] = func [1]
    Code [2]:
     - func [0] size=7
     - func [1] size=7

例 7-4 の JavaScript は、前の章で行ったのと同じように、モジュールをインスタンス化します。そこから、モジュールのエクスポートされたセクションから Table インスタンスを抽出します。

例 7-4. JavaScript からエクスポートされたテーブル インスタンスの使用

<!doctype html>

<html>
  <head>
      <title>WASM Table test</title>
      <meta charset="utf-8">
      <script src="utils.js"></script>
      <link rel="icon" href="data:;base64,=">
  </head>

  <body>
    <script>
      var t;

      fetchAndInstantiate ('math.wasm').then (function (instance) {
      var tbl = instance.exports.tbl;
      t = tbl;
      console.log ("3 + 1 =" + tbl.get (0)(3,1));
      console.log ("3 - 1 =" + tbl.get (1)(3,1));
      });
    </script>
  </body>
</html>

参照を取得したら、位置 0 に関連付けられた関数を取得して呼び出すことができます。get () 呼び出しから返されるのは関数への参照であることに注意してください。これを呼び出すには、かっこ内の 2 番目のパラメーター セットを送信し、結果をコンソールに出力します。次に、位置 1 の関数に対して同じことを行います。

HTTP 経由で HTML を送信し、JavaScript コンソールを開きます。ブラウザがこのコードを実行すると、図 7-1 のように表示されるはずです。

cc302f8dddd722625e700b50069af587.png

図 7-1. テーブル インスタンスでメソッドを呼び出した出力

テーブル インスタンスは 2 つの参照のみを持つことができます。それを超える場所にアクセスしようとすると tbl.length 、例外が発生します。

WebAssembly の動的リンク

最後の例は、WebAssembly での動的リンクの使用です。2 つのモジュールを定義します。$add 1 つは、事前定義されたメソッドと メソッドを含みます $sub 。最初のモジュールは例 7-5 にあります。前に見たものとの主な違いは、このモジュールがホストからテーブルをインポートすることです。elem 命令を使用して、このテーブルに算術関数を挿入します。加算関数は位置 0 に格納され、減算関数は位置 1 に格納されます。

例 7-5. 動的にリンクされたモジュール

(module
  (import "js" "table" (table 2 funcref))

  (func $add (param $a i32) (param $b i32) (result i32)
      local.get $a
      local.get $b
      i32.add)

  (func $sub (param $a i32) (param $b i32) (result i32)
      local.get $a
      local.get $b
      i32.sub)

  (elem (i32.const 0) $add)
  (elem (i32.const 1) $sub)
)

2 番目のモジュールは、myadd と mysub という 2 つの関数をエクスポートします。2 つの数値を加算および減算できる機能を顧客に宣伝しています。内部的には、インポートされたテーブル インスタンス内の関数参照を呼び出します。これもホストの JavaScript 環境からインポートしました。

私たちが宣伝している機能の実装を例 7-6 に示します。どちらの関数も call_indirect 命令を呼び出します。前の章では、現在のモジュールで定義されている関数を呼び出すための call ディレクティブの使用について見てきました。call_indirect ディレクティブは、テーブルのどの要素を呼び出すかを決定して関数を呼び出します。

例 7-6. 動的にリンクされたモジュールに依存するモジュール

(module
  (import "js" "table" (table 2 funcref))

  (type $sig (func (param $a i32) (param $b i32) (result i32)))

  (func (export "myadd") (param $a i32) (param $b i32) (result i32)
      (call_indirect (type $sig) (local.get $a) (local.get $b) (i32.const 0))
  )

  (func (export "mysub") (param $a i32) (param $b i32) (result i32)
      (call_indirect (type $sig) (local.get $a) (local.get $b) (i32.const 1))
  )
)

驚くべきことの 1 つは、型ディレクティブの使用です。これは、WebAssembly である程度の型安全性を提供する関数のシグネチャを定義します。インポートされたテーブル関数には、呼び出したいシグネチャが必要であるという考え方です。

この場合、2 つの i32 を受け取り、1 つの i32 を返す関数シグネチャを定義します。テーブルを通じてこれらのメソッドを呼び出すと、これが予期した型であることがわかります。署名後、関数のパラメータをスタックにプッシュし、最後にテーブルの位置番号にプッシュします。加算の場合、その定数値は 0 で、テーブル内の最初の位置を表します。引き算の場合は2位となります。

例 7-7 にすべてをまとめました。最初に共有テーブル インスタンスを作成します。これは importObject 両方のモジュールに渡されます。違いは、math2.wat モジュールがその関数 $add と を $sub 位置 0 と 1 にそれぞれ書き込むことです。mymath.wat モジュールはホスト JavaScript 環境から呼び出され myadd 、 mysub これらの場所から間接的に呼び出されます。呼び出しの一部として、動的にリンクされた関数に割り当てられた引数も渡します。

2 つのモジュールを扱っているため、インスタンス化メカニズムは少し異なります。Promise.all () 単一の Promise を待つ代わりに、依存するすべての Promise が満たされるのを防ぐメソッドを呼び出します 。この場合、両方のモジュールがロードされ、準備ができていることを意味します。

例 7-7. 2 つのモジュールをインスタンス化し、それらの間に動的リンクを確立する

<!doctype html>

<html>
  <head>
      <title>WASM Dynamic Linking test</title>
      <meta charset="utf-8">
      <script src="utils.js"></script>
      <link rel="icon" href="data:;base64,=">
  </head>

  <body>
    <script>
      var importObject = {
      js: {
          memory: new WebAssembly.Memory({ initial: 1 }),
          table: new WebAssembly.Table({ initial:2, element:"anyfunc" })
      }
      };

      Promise.all([
          fetchAndInstantiate('math2.wasm', importObject),
      fetchAndInstantiate('mymath.wasm', importObject)
      ]).then(function(instances) {
      console.log("4 + 3 = " + instances[1].exports.myadd(4,3));
          console.log("4 - 3 = " + instances[1].exports.mysub(4,3));
      });

    </script>
  </body>
</html>

モジュールが使用可能になると、このコードは いくつかのパラメーターを指定してmyadd および mysub メソッドを呼び出します。動作バージョンを表す 2 番目のモジュール インスタンスを選択していることに注意してください。これは単一のインスタンスではなく、インスタンスの配列です。

HTTP 経由でサービスを提供した後のブラウザの結果は、図 7-2 に示されています。1 つのモジュールは、共有 Table インスタンスを通じて別のモジュールに実装された動作を間接的に呼び出します。

10e5d9c04ecf1ac2e7c7d4c0cc4a0f4f.png

図 7-2. ダイナミック リンク関数の呼び出しによる出力

これで、プラットフォームとしての WebAssembly の主な機能要素の紹介は終わりです。この本の残りの部分では、これらの基本に基づいて、WebAssembly の使用方法とその将来についていくつかの例を示します。まだ説明していないさらに高度な機能がいくつか含まれています。

クラウド ネイティブ コミュニティの詳細については、WeChat グループに参加してください。クラウド ネイティブ コミュニティに参加し、クリックして元の記事を読んで詳細をご覧ください。

Ich denke du magst

Origin blog.csdn.net/weixin_38754564/article/details/129512034
Empfohlen
Rangfolge