Logo
x logo

箱庭飛行場Ex

2-4.EthernetCableのWASM化

前回、Rustで実装したEthernetCableを、WASM化していきます。 今回も、「Packet Pilot Terminal」で確認できるようにしましょう。

今回から、ソースをgithubで公開しています。 このセクションまでのソースは、「v.0.2.4」というtagに紐づいていますので、tagをv.0.2.4にしてご覧ください。下記のバナーをクリックでレポジトリーに移動します。

ぜひ、ソースを見ながら進めてみてください。

では、はじめましょう。


まずは、どういうI/FをWASM用として用意するかをまとめます。

  • cable([id]) 指定したidでケーブルを作成
  • remove_cable([id]) 指定したidのケーブルを削除
  • is_valid_cable([id]) 指定したidのケーブルは使っていいものか?
  • connect_cable(cable_id,component_id1,component_id2) cable_idのケーブルをネットワークコンポーネント1と2に接続する
  • get_connect_id1(cable_id) 指定したidのケーブルの端1に接続されているネットワークコンポーネントのIDを取得
  • get_connect_id2(cable_id) 指定したidのケーブルの端2に接続されているネットワークコンポーネントのIDを取得

このようなコマンドを実行できるようにWASM用のI/Fを用意していこうと思います。

WebAssembly用のI/Fとしてまとめる

I/Fとしてまとめるにあたり、レイヤー1のmod.rsを整理しておきましょう。 イーサーネットケーブルは、前回このような構成で src/layer1/component/ethernet_cable.rsに実装しました。

[packet-pilot]
├── Cargo.lock
├── Cargo.toml
├── css
├── index.html
├── js
├── pkg
├── src
│   ├── layer1
│   │   ├── component
│   │   │   ├── ethernet_cable.rs (イーサネットケーブルのコンポーネントをここに)
│   │   │   └── mod.rs
│   │   ├── mod.rs <-このファイルにethernet_cableを追加
│   │   ├── packets
│   │   │   ├── mod.rs
│   │   │   └── physical_layer_frame.rs
│   │   └── receive_callback.rs (callbackの定義はココ)

src/layer1/mod.rsを開き

pub(crate) mod packets;
pub(crate) mod component;
pub(crate) mod receive_callback;

pub use receive_callback::PhysicalLayerCallback;
pub use component::EthernetCable;

としておきましょう。これで、layer1::EthernetCableというふうにアクセスできるようになります。

ここからは、「1-3.Packet、アドレスのWebAssembly化」で ご説明したのと同じ流れで、WasmEthernetCableを作成していきます。

コマンドとして用意する下記のものをI/Fとして実装していきます。

I/F
cable([id])
remove_cable([id])
is_valid_cable([id])
connect_cable(cable_id,component_id1,component_id2)
get_connect_id1(cable_id)
get_connect_id2(cable_id)

src/lib.rsを開いて、下記のコードを追加しましょう。

//////////////////////////////////////////////
// イーサネットケーブルのWebAssembly対応ラッパー構造体
//////////////////////////////////////////////

/// WebAssemblyからイーサネットケーブルを扱うためのラッパー構造体
/// inner_cable: 内部に保持する実際のEthernetCableインスタンス
#[wasm_bindgen]
pub struct WasmEthernetCable {
    inner_cable: Option<EthernetCable>,
}

#[wasm_bindgen]
impl WasmEthernetCable {
    /// 新しいイーサネットケーブルを作成
    /// 
    /// ### 引数
    /// * `id` - ケーブルのId(タグのようなものケーブルを識別できるように。なくても良い)
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let cable = new WasmEthernetCable(id);
    /// ```
    /// 
    #[wasm_bindgen(constructor)]
    pub fn new(id:Option<String>) -> Self {
        // 新しいEthernetCableインスタンスを作成
        WasmEthernetCable {
            inner_cable: Some(EthernetCable::new(id))
        }
    }
    /// イーサーネットケーブルを削除する
    /// Noneを指定することで明示的に削除することになる
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// cable.remove();
    /// ```
    #[wasm_bindgen]
    pub fn remove(&mut self) {
        // Optionを使って、内部のケーブルリソースを明示的に破棄
        self.inner_cable = None;
    }
    /// イーサーネットケーブルが有効かどうかをチェック
    /// 
    /// ### 戻り値
    /// * `bool` - ケーブルが有効かどうか。
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// if(cable.is_valid()){...有効な時の処理...}else{...無効な時の処理...}
    /// ```
    /// 
    #[wasm_bindgen]
    pub fn is_valid(&self) -> bool {
        self.inner_cable.is_some()
    }
    /// そのイーサネットケーブルのIdを取得
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let id = cable.getId();
    /// ```
    /// 
    #[wasm_bindgen]
    pub fn get_id(&self) -> String {
         self.inner_cable.as_ref().map(|cable| cable.get_id()).unwrap_or_default()
    }

    /// イーサーネットケーブルの内容表示
    /// 
    /// ### 戻り値
    /// * `String` - ケーブルの情報を表す文字列
    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        // 内部のEthernetFrameインスタンスの文字列表現を取得
        self.inner_cable.as_ref().map(|cable| cable.to_string().replace("\n","\r\n")).unwrap_or_default()
    }
   
    /// イーサネットケーブルをつなげる
    /// 
    /// ### 引数
    /// * `ep1_connect_id` - 端1に繋げるコンポーネントのId
    /// * `ep2_connect_id` - 端2に繋げるコンポーネントのId
    ///
    #[wasm_bindgen]
    pub fn connect(&self, ep1_connect_id: Option<String>, ep2_connect_id: Option<String>) {
        self.inner_cable.as_ref().map(|cable| {
            cable.connect(ep1_connect_id, ep2_connect_id);
        }).unwrap_or_else( || showTerminal("このケーブルは無効です。"));
    }
    /// endpoint1の方にイーサネットケーブルをつなげる
    /// 
    /// ### 引数
    /// * `ep1_coonect_id` - 端1に繋げるコンポーネントId
    /// ケーブルの片方を別のポートに差し替えたり、別のコンポーネントに繋げ直すような時
    /// 
    #[wasm_bindgen]
    pub fn connect_endpoint1(&self, ep1_connect_id: Option<String>) {
        self.inner_cable.as_ref().map(|cable| {
            cable.connect_endpoint1(ep1_connect_id);
        }).unwrap_or_else( || showTerminal("このケーブルは無効です。"));
    }
    /// endpoint1に繋がっているコンポーネントのIdを取得
    /// 
    /// ### 戻り値
    /// * `Option<String>` - "endpoint1に繋がっているコンポーネントがあったらそのコンポーネントのIdが返される
    /// 
    #[wasm_bindgen]
    pub fn get_endpoint1_component_id(&self) -> Option<String> {
        //self.inner_cable.as_ref().?.get_endpoint1_component_id()
        self.inner_cable.as_ref().and_then(|cable| {
            cable.get_endpoint1_component_id()
        }).or_else(|| {
            showTerminal("このケーブルは無効です。");
            None
        })
    }

    /// endpoint2の方にイーサネットケーブルをつなげる
    /// 
    /// ### 引数
    /// * `ep2_coonect_id` - 端1に繋げるコンポーネントId
    /// ケーブルの片方を別のポートに差し替えたり、別のコンポーネントに繋げ直すような時
    /// 
    #[wasm_bindgen]
    pub fn connect_endpoint2(&self, ep2_connect_id: Option<String>) {
        self.inner_cable.as_ref().map(|cable| {
            cable.connect_endpoint2(ep2_connect_id);
        }).unwrap_or_else( || showTerminal("このケーブルは無効です。"));
    }
    /// endpoint2に繋がっているコンポーネントのIdを取得
    /// 
    /// ### 戻り値
    /// * `Option<String>` - "endpoint2に繋がっているコンポーネントがあったらそのコンポーネントのIdが返される
    /// 
    #[wasm_bindgen]
    pub fn get_endpoint2_component_id(&self) -> Option<String> {
        self.inner_cable.as_ref()?.get_endpoint2_component_id()
    }
}

それぞれのコマンドと対比すると、

I/F WASM側の関数
cable([id]) new
remove_cable([id]) remove
is_valid_cable([id]) is_valid
connect_cable(cable_id,component_id1,component_id2) connect
get_connect_id1(cable_id) get_endpoint1_component_id
get_connect_id2(cable_id) get_endpoint2_component_id

になります。   今回JS側のコマンドとしては実装していませんが、一気にendpoint1/2に接続するI/Fだけでなく、今後のために、connect_endpoint1,connect_endpoint2と順番にイーサーネットケーブルを接続するためのI/Fも用意しておきました。

特に実装内容は単純で、前回Rustで実装したものをラッピングしているだけです。

これで、前回Rustで実装した内容を、WASM用(JS用)の構造体でラッピングして、Javascript側からはこのWASM用の構造体に用意されたメソッド(関数)経由Rustの機能を使えるようになります。

WebAssemblyにビルド

まだ、wasm32-unknown-unknownや、wasm-packをインストールしていない人は、 こちらを参照してください。

では、

wasm-pack build --target web

で、 ビルドが成功すると、./pkgというディレクトリができています。 この中に、

[pkg]
├── package.json
├── packet_pilot.d.ts
├── packet_pilot.js
├── packet_pilot_bg.js
├── packet_pilot_bg.wasm
└── packet_pilot_bg.wasm.d.ts

といったファイルが出来上がっています。packet_pilot_bg.wasmがWASMファイルです。

HTML側

ということで、WASMが出来上がりましたので、Javascript側の実装に入りましょう。 HTML+JS側の、全体構成をおさらいしておきます。

  • index.html : HTML
  • styles.css : スタイルシートをまとめる
  • main.js : JS側の総まとめ的役割
  • terminal.js: HTMLに表示するterminal部分の制御
  • commands.js: ターミナルに入力された値を元にコマンド解析&実行
[packet-pilot]
├── index.html   // 今回のメインのHTML
├── css
│   └── styles.css // スタイルシート
├── js
│   ├── commands.js // コマンド解析部分
│   ├── main.js     // mainのJS
│   └── terminal.js // ターミナルの挙動をまとめた部分
├── pkg // WebAssemblyファイルと、WebAssemblyとのやりとり用のJS自動生成されたもの
│   ├── package.json
│   ├── packet_pilot.d.ts
│   ├── packet_pilot.js
│   ├── packet_pilot_bg.js
│   ├── packet_pilot_bg.wasm
│   └── packet_pilot_bg.wasm.d.ts
├── src // rust部分
│   ├── layer1
│   ├── layer2
│   ├── layer3
│   └── lib.rs // Wasm用のI/F
├── Cargo.toml

このような形で、commands.jsがコマンドを解析してWASMとのやりとりをしていきます。 今回は、このcommands.jsにイーサーネットケーブル関係のコマンドを追加するだけでOKです。

js/commands.jsを開き、WasmEthernetCableを使えるようにimportしましょう。

import  init,
        {
            WasmMacAddress,
            WasmIPv4Address, 
            WasmIPv6Address, 
            WasmEthernetFrame, 
            WasmPhysicalLayerFrame,
            WasmEthernetCable <---これを追加
        } from "../pkg/packet_pilot.js";

そして、CommandProcessorクラスのコンストラクタのところに、

// コンストラクタ - terminalManagerインスタンスを受け取る
constructor(terminalManager){
    this.terminal = terminalManager;  // ターミナルへの参照を保持
    
    // ネットワーク関連の状態管理
    this.currentMac = null;
    this.currentIPv4 = null;
    this.currentIPv6 = null;
    this.currentEthernetFrame = null;
    this.currentPhysicalLayerFrame = null;
    this.currentEthernetCable = {}; // 2-4.追加({id:xxx,cable:本体}で現在配置されているケーブルを持つ)
    this.wasm = this.initWasm();
}

次にコマンドを追加していきます。

// -- コマンド定義 ------------------------------------
// 今後テストとしてコマンドを追加したいときはここに追加していく
// --------------------------------------------------
    commands = {  

このcommands配列に、コマンドを追加していきます。 あっ、その前にまず、helpコマンドに、今回追加するコマンドの説明を追加しておきましょうね。

  // helpコマンド - 利用可能なコマンドの一覧を表示
  help: () => {
      this.terminal.writeln('Available commands:');
      this.terminal.writeln(' mac([address]) - Create MAC address (random if no address)');
      this.terminal.writeln(' ipv4([address]) - Create IPv4 address (random if no address)');
      this.terminal.writeln(' ipv6([address]) - Create IPv6 address (random if no address)');
      this.terminal.writeln(' eth() - Create frame');
      this.terminal.writeln(' send() - Mock send: create physical layer frame');
      this.terminal.writeln(' show() - Show current packet&address&cable details');
      this.terminal.writeln(' cable([id]) - Create Cable (random if no id)')
      this.terminal.writeln(' remove_cable([id]) - Remove the Cable')
      this.terminal.writeln(' is_valid_cable([id]) - Valid check the Cable')
      this.terminal.writeln(' connect_cable(cable_id,component_id1,component_id2) - Connect cable(id) to component id1&2')
      this.terminal.writeln(' get_connect_id1(cable_id) - Get Id from cable id\'s endpoint1')
      this.terminal.writeln(' get_connect_id2(cable_id) - Get Id from cable id\'s endpoint2')
      this.terminal.writeln(' clear - Clear terminal');
      this.terminal.writeln(' help - Show this help message');
  },

そして、イーサネットケーブル関係のコマンドを追加します。

// cableコマンド - イーサーネットケーブルの生成
cable: (id = null) => {
    try {
        var cable;
        if (id) {
            id = id.replace(/['"]/g, '');
            cable = new WasmEthernetCable(id);
        } else {
            cable = new WasmEthernetCable();
        }
        
        this.currentEthernetCable = {
            ...this.currentEthernetCable,
            [cable.get_id()]: { id: cable.get_id(), cable: cable }
        };
        Object.values(this.currentEthernetCable).map((one) => {
            this.terminal.writeln(`${one.cable.to_string()}`);
        });
        
    } catch (e) {
        this.terminal.writeln(`Error: ${e.message}`);
    }
},
// remove_cableコマンド - イーサーネットケーブルの削除
remove_cable: (id = null) => {
    try {
        if(id === null){
            this.terminal.writeln('idが指定されていません');
        }
        id = id.replace(/['"]/g, '');
        const cable = this.currentEthernetCable[id]?.cable;
        if(cable !== undefined){
            cable.remove();
            this.terminal.writeln(`id:${id}のケーブルを削除しました。`);
        }else{
            message = `id:${id}は無効なケーブルIdです。`;
        }
        
        Object.values(this.currentEthernetCable).map((one) => {
            this.terminal.writeln(`${one.cable.to_string()}`);
        });
        
    } catch (e) {
        this.terminal.writeln(`Error: ${e.message}`);
    }
},
// is_valid_cableコマンド - イーサーネットケーブルが有効かどうか?
is_valid_cable: (id = null) => {
    try {
        if(id === null){
            this.terminal.writeln('idが指定されていません');
        }
        id = id.replace(/['"]/g, '');
        const cable = this.currentEthernetCable[id]?.cable;
        if(cable !== undefined){
            var message = "";
            message= cable.is_valid() === true ? `id:${id}のケーブルは、有効です。` :  `id:${id}のケーブルは、無効です。`;
        }else{
            message = `id:${id}は無効なケーブルIdです。`;
        }
        this.terminal.writeln(message);
        Object.values(this.currentEthernetCable).map((one) => {
            this.terminal.writeln(`${one.cable.to_string()}`);
        });
        
    } catch (e) {
        this.terminal.writeln(`Error: ${e.message}`);
    }
},
// connect_cableコマンド - イーサーネットケーブルをコンポーネントId1、Id2に接続する
connect_cable: (cable_id = null,id1 = null,id2 = null) => {
    try {
        if(cable_id === null){
            this.terminal.writeln('ケーブルidが指定されていません');
        }
        if(id1 === null||id2 === null){
            this.terminal.writeln('接続するコンポーネントのid1が指定されていません');
        }
        if(id2 === null){
            this.terminal.writeln('接続するコンポーネントのid2が指定されていません');
        }
        cable_id = cable_id.replace(/['"]/g, '');
        id1 = id1.replace(/['"]/g, '');
        id2 = id2.replace(/['"]/g, '');
        var message = "";
        const cable = this.currentEthernetCable[cable_id]?.cable;
        if(cable !== undefined){
            cable.connect(id1,id2);
            message = `ケーブル:${cable.get_id()}を、${id1}${id2}につなげました`
        }else{
            message = `id:${cable_id}は無効なケーブルIdです。`;
        }
        this.terminal.writeln(message);
        Object.values(this.currentEthernetCable).map((one) => {
            this.terminal.writeln(`${one.cable.to_string()}`);
        });
        
    } catch (e) {
        this.terminal.writeln(`Error: ${e.message}`);
    }
},
// get_connect_id1コマンド - イーサーネットケーブルのendpoint1に繋がっているコンポーネントIdの取得
get_connect_id1: (cable_id = null) => {
    try {
        if(cable_id === null){
            this.terminal.writeln('ケーブルidが指定されていません');
        }
        cable_id = cable_id.replace(/['"]/g, '');
        var message = "";
        const cable = this.currentEthernetCable[cable_id]?.cable;
        if(cable !== undefined){
            const endpoint1_id = cable.get_endpoint1_component_id();
            message = `ケーブル:${cable.get_id()}のendpoint1には、${endpoint1_id}がつながっています`
        }else{
            message = `id:${cable_id}は無効なケーブルIdです。`;
        }
        this.terminal.writeln(message);
        Object.values(this.currentEthernetCable).map((one) => {
            this.terminal.writeln(`${one.cable.to_string()}`);
        });
        
    } catch (e) {
        this.terminal.writeln(`Error: ${e.message}`);
    }
},
// get_connect_id2コマンド - イーサーネットケーブルのendpoint2に繋がっているコンポーネントIdの取得
get_connect_id2: (cable_id = null) => {
    try {
        if(cable_id === null){
            this.terminal.writeln('ケーブルidが指定されていません');
        }
        cable_id = cable_id.replace(/['"]/g, '');
        var message = "";
        const cable = this.currentEthernetCable[cable_id]?.cable;
        if(cable !== undefined){
            const endpoint2_id = cable.get_endpoint2_component_id();
            message = `ケーブル:${cable.get_id()}のendpoint2には、${endpoint2_id}がつながっています`
        }else{
            message = `id:${cable_id}は無効なケーブルIdです。`;
        }
        this.terminal.writeln(message);
        Object.values(this.currentEthernetCable).map((one) => {
            this.terminal.writeln(`${one.cable.to_string()}`);
        });
        
    } catch (e) {
        this.terminal.writeln(`Error: ${e.message}`);
    }
},

コマンドを実行するたびに、そのコマンドでどう状態が変わったかわかるように、

Object.values(this.currentEthernetCable).map((one) => {
    this.terminal.writeln(`${one.cable.to_string()}`);
});

で、ターミナルに表示するようにしておきました。

今後もこのような表記は、よく出てくると思いますので、分解して説明しておきます。

  • Object.values()による値の取得 this.currentEthernetCableオブジェクトから全ての値を配列として取得キーは無視され、値のみが処理対象となります。
  • mapメソッドによる処理 .map((one) => { ... }) 配列の各要素に対して処理を実行 oneは各イーサネットケーブルオブジェクトを表します
  • ターミナル出力 という、流れになります。

もう1つ、cable_idを指定するコマンドでは、指定されたcable_idがメモリ上に今存在しているか?の確認のため

const cable = this.currentEthernetCable[id]?.cable;

としています。 ここで、currentEthernetCable[id]?.cableとしているのは、 オプショナルチェイニングと呼ばれています。 指定されたidが存在しない場合、オブジェクトはundefinedになってしまいます。 通常Javascriptで、undefinedにアクセスしようとするとエラーになり処理が中断しますが、 ?をつけておくことで、そのオブジェクトのプロパティへの安全なアクセスを保証してくれるようになるのです。 ここで、this.currentEthernetCable[id]nullまたはundefinedの場合、undefinedを返してくれます。

1-3.Packet、アドレスのWebAssembly化で色々と準備して構築していましたので、 今回は、これだけでOKです。もうイーサーネットケーブルのWASMへのアクセスはできるようになりました。

それでは、いよいよ動かしてみましょう。 VSCodeで作っている場合は、LiveServer extensionをインストールしていたら、サーバを用意する必要もありません。 index.htmlを選択して右クリックで、

ブラウザが開き、HTMLが表示されます。

実験

ターミナルにコマンドを入力して実験しましょう。

cable()
$ cable()

------------
[Debug] EthernetCable::new([id]) called.
------------

###Ethernet Cable= 
#id                     : cable-9250
#endpoint1_component_id : None
#endpoint1_callback     : None
#endpoint2_component_id : None
#endpoint2_callback     : None
#connected              : false

どうですか?このように表示されたら成功です。

おや?
[Debug] EthernetCable::new([id]) called. 今回Javascript側で実装していないようなものが表示されましたね。

これは、前回Rust側で実装した際にデバッグ用として、追加した機能が動いているということです。

今回は、最後にここの流れを説明しておきます。

まず、Rust側で、src/lib.rsに、下記のようなコードを追加しました。

/// JS側にデバッグ表示のために用意された関数を呼び出す
#[wasm_bindgen]
extern "C" {
    pub fn showTerminal(s: &str);
}

extern "C"は、Rust側から外部で定義されている関数を呼び出すための定義でしたね。 この場合、JS側で用意された、showTerminalという関数を呼び出せるようにしておく。 ということです。

そして、これをEthernetcable::new()で、debug()で確かに、上記の「EthernetCable::new([id]) called.」が指定されていますね。

/// 新規にケーブルを配置したとき
pub fn new(id:Option<String>) -> Self {
    debug("EthernetCable::new([id]) called.");
    EthernetCable{
        state: Arc::new(Mutex::new(EthernetCableState::new(id))),
    }
}

そして、debug関数では、[Debug]を追加して、showTerminalを呼び出しています。

// -- for WASM debug
pub fn debug(s: &str) {
    let message= format!("\r\n------------\r\n[Debug] {}\r\n------------\r\n",s);
    showTerminal(&message);
}

Javascript側では、js/terminal.jsに、showTerminal関数を追加しておきましょう。というのを忘れておりました。 まず、TerminalManagerクラスで、initialize関数で、

//デバッグでterminalにWASM側からのメッセージを表示するために追加。2-4.
window.showTerminal = this.showTerminal.bind(this);

windowオブジェクトに紐づく関数しか、WASM側からは呼び出せないというルールになっていますので、 このようにして、windowオブジェクトに紐づけています。

次に、TerminalManagerクラスにshowTerminal関数を追加します。

showTerminal(message) {
  if (this.term) {
      this.term.writeln(message);
  } else {
      console.log('Terminal not initialized:', message);
  }
}

このように、WASM側から受け取った文字列を、terminalにwritelnしています。

はい。このようにして、JS側で用意されたshowTerminal関数を呼び出しターミナル上にdebug文をプリントしているのでした。

すみません。イヤー検証大事ですね。実験してみてよかったです。ここの説明を忘れておりました。

ということで、今回は今までの積み重ねがあり、いつもに比べてコンパクトに終わりました。 イーサーネットケーブルのWASM化もこれで終わりです。 今回から、ソースをgithubに公開することになりましたので、ソースを見ながら色々いじってみてください。下記のバナークリックでレポジトリーに移動します。

いつも最新のが置かれていますが、今回までの記事の内容をまとめたものは、 tagで、「v.0.2.4」になります。   

次回は、また渋いものを抽象化して実装していきます。 「NIC/NICドライバー」を予定していますので、楽しみにお待ちください。 では。   

ネットワークのこと、Packet、アドレスのことについて頭に入れておきたい場合は、本誌Vol.1で復習しておいてくだださい。

ネットワークを流れるPacketをプログラムから操作できるようになりたいという人には必見のマガジンです。