Logo
x logo

箱庭飛行場Ex

1-3.Packet、アドレスのWebAssembly化

WebAssemblyとして使えるようにするには、HTMLでどういう風に使いたいかをまず考えましょう。 前回実装したのは、Packet、アドレスを作ることと、その内容の表示を行うことでしたね。
今回は、こういう感じのUIで、Packetを作る、Packetやアドレスが確認ができる「Packet Pilot Terminal」を作ろうと思います。

まずは、WebAssemblyとして使うためにRust側のlib.rsに、インターフェースをまとめます。

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

I/Fとしてまとめるにあたり、各レイヤーのmod.rsを整理しておきましょう。

[packet-pilot]
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── layer1
│   │   ├── mod.rs
│   │   └── packets
│   │       ├── mod.rs
│   │       └── physical_layer_frame.rs
│   ├── layer2
│   │   ├── mod.rs
│   │   ├── address
│   │   └── packets
│   │       ├── ethernet_frame.rs
│   │       └── mod.rs
│   └── lib.rs

まずこの中の、Layer1から、 [Layer1] packets/mod.rs

pub(crate) mod physical_layer_frame;

pub use physical_layer_frame::PhysicalLayerFrame;

これで、use crate::layer1::address:: PhysicalLayerFrame; という風に使えるようになります。 layerX::address::XXXX layerX::packets::XXXX と、それぞれのlayer毎にaddress,packetsでその中のどれを使うという形でアクセスできます。

layerX::XXXX という風にaddressなのかpacketsなのかも省略したい場合は、 layer1/mod.rsに

pub(crate) mod packets;

pub use packets::PhysicalLayerFrame;

と書けば使えます。われわれとしては、addressなのかpacketsなのか意識できるように先述の方式で統一していきます。

[Layer2] packets/mod.rsは、

pub(crate) mod ethernet_frame;

pub use ethernet_frame::EthernetFrame;

address/mod.rsは、

pub(crate) mod mac_address;

pub use mac_address::MacAddress;

[layer3] address/mod.rsは、

pub(crate) mod ipv4_address;
pub(crate) mod ipv6_address;

pub use ipv4_address::IPv4Address;
pub use ipv6_address::IPv6Address;

とこうやって整理することで、 大元のlib.rsで、

use layer1::packets::PhysicalLayerFrame;
use layer2::packets::EthernetFrame;
use layer2::address::MacAddress;
use layer2::address::IPv4Address;
use layer2::address::IPv6Address;

と利用できるようになります。

lib.rsに入る前に、WebAssemblyにするにあたり依存関係を追加しておきましょう。

[package]
name = "packet-pilot"
version = "0.1.0"
edition = "2021"

[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
rand = "0.8"
js-sys = "0.3.72"
console_error_panic_hook = "0.1.7"

[lib]
crate-type = ["cdylib"]

前回からの変更点は、 console_error_panic_hook を追加しておきました。 rustでpanic(重大なエラー)が起こった時に、ブラウザの開発者ツールのconsoleにその旨を出力してくれるクレートです。

さて、いよいよlib.rsを作っていきましょう。 まずはエントリーポイントinit()を作ります。

// Initialize function for wasm-bindgen
#[wasm_bindgen(start)]
pub fn init() {
    // 初期化
    console_error_panic_hook::set_once();
}

#[wasm_bindgen(start)]について説明しておきます。 このアトリビュートが付与された関数は、Wasmモジュールが読み込まれた直後に自動的に実行されます。モジュールの初期化処理を行うのに適しています。 これがついた関数は、JavaScriptからWebAssemblyをロードした時点で実行されます。 このお話しは、わが編集部の調査班に調べてもらいましたので、こちらを読んでください。

さて、全体の流れは同じようになりますので、まず例として、MACアドレスを例として書きます。

//////////////////////////////////////////////
// MACアドレスのWebAssembly対応ラッパー構造体
//////////////////////////////////////////////

/// WebAssemblyからMACアドレスを扱うためのラッパー構造体
/// inner_mac: 内部に保持する実際のMacAddressインスタンス
#[wasm_bindgen]
pub struct WasmMacAddress {
    inner_mac: MacAddress  // MacAddressインスタンスを明示的な名前で保持
}

#[wasm_bindgen]
impl WasmMacAddress {
    /// 新しいMACアドレスインスタンスを作成
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let mac = new WasmMacAddress();
    /// ```
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        // 内部のMacAddressインスタンスを作成して保持
        WasmMacAddress {
            inner_mac: MacAddress::new()
        }
    }

    /// 文字列からMACアドレスを生成
    /// 
    /// ### 引数
    /// * `mac_str` - "00:11:22:33:44:55" 形式のMACアドレス文字列
    /// 
    /// ### 戻り値
    /// * `Result<WasmMacAddress, JsValue>` - 成功時はWasmMacAddress、失敗時はエラーメッセージ
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let mac = WasmMacAddress.from_string("00:11:22:33:44:55");
    /// ```
    #[wasm_bindgen]
    pub fn from_string(mac_str: &str) -> Result<WasmMacAddress, JsValue> {
        // 文字列をMacAddressに変換を試みる
        match MacAddress::from_string(mac_str) {
            // 変換成功時は新しいWasmMacAddressインスタンスを作成
            Ok(mac_address) => Ok(WasmMacAddress {
                inner_mac: mac_address
            }),
            // 変換失敗時はエラーメッセージをJavaScript用の値に変換
            Err(error_message) => Err(JsValue::from_str(error_message))
        }
    }

    /// MACアドレスを文字列形式で取得
    /// 
    /// ### 戻り値
    /// * `String` - "00:11:22:33:44:55" 形式のMACアドレス文字列
    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        // 内部のMacAddressインスタンスの文字列表現を取得
        self.inner_mac.to_string()
    }

    /// MACアドレスをバイト配列として取得
    /// 
    /// ### 戻り値
    /// * `Uint8Array` - 6バイトのMACアドレスデータ
    #[wasm_bindgen]
    pub fn to_bytes(&self) -> Uint8Array {
        // 内部のMacAddressインスタンスからバイト配列を取得
        let mac_bytes = self.inner_mac.to_array();
        // バイト配列をJavaScript用のUint8Arrayに変換
        Uint8Array::from(&mac_bytes[..])
    }
}
#[wasm_bindgen]
pub struct WasmMacAddress {
    inner_mac: MacAddress  // MacAddressインスタンスを明示的な名前で保持
}

このように、WasmMacAddressとして、Rustで実装したMacAddress型をラッピングしています。 これは、WebAssemblyはJavaScriptと連携して動作しますが、Rustのデータ型はWebAssemblyやJavaScriptで直接扱えないという理由があります。 たとえば、RustのMacAddress型は、WebAssemblyやJavaScriptがその内部構造を理解できないため、直接操作するのが困難です。

ラッパー構造体を導入することで、Rust内部の複雑なデータ型(ここではMacAddress)を隠蔽し、WebAssemblyに適したインターフェース(メソッドや簡単な型)を提供できるようになるのです。 逆に言うと、JavascriptやWebAssemblyに合わせてRust側を作るとなると想像してみてください。 かなり制限を受けて、WebAssemblyだけでしか使えない型を作ることになりますね。 先にRust側で実装していたのは、今後WebAssembly以外でも使えるようにするためです。 そして、ここで、WebAssembly化と言うことでラッピングするようにすることでRustの自由さをそのまま使えるようにするためなのです。

impl WasmMacAddress 

と言うことで、ここでもWasmMacAddressとしての実装をします。 そして、MacAddress構造体でimplした関数を呼び出すようにしましょう。

と、ここで、2つほど、先週実装したMacAddressについて修正しないといけない部分があります。 まず1つ目は、 layer2/address/mac_address.rsの中のfrom_string()関数です。

/// ":"区切りの文字列からMACアドレスを生成する関数
    pub fn from_string(mac_str: &str) -> Result<MacAddress, &'static str> {
        let bytes: Vec<u8> = mac_str.split(':')
                                    .map(|s| u8::from_str_radix(s, 16))
                                    .collect::<Result<Vec<u8>, _>>()
                                    .map_err(|_| "Invalid MAC address format")?;
    
        if bytes.len() == 6 {
            let mut mac_array = [0u8; 6];
            mac_array.copy_from_slice(&bytes);
            Ok(Self(mac_array))
        } else {
            Err("MAC address must contain exactly 6 bytes")
        }
    }

この関数だけ、

pub fn from_string(mac_str: &str) -> Result<[u8; 6], &'static str> {

としてしまっていました。IPv4AddressやIPv6Addressでは

pub fn from_string(s: &str) -> Result<IPv4Address, &'static str> {
pub fn from_string(s: &str) -> Result<IPv6Address, &'static str> {

としていたのに、返却値は、MacAddressにしておきたかったのですが忘れておりました。 ここで修正しておきます。 そうすることで、WASM用関数として、

#[wasm_bindgen]
pub fn from_string(mac_str: &str) -> Result<WasmMacAddress, JsValue> {
    // 文字列をMacAddressに変換を試みる
    match MacAddress::from_string(mac_str) {
        // 変換成功時は新しいWasmMacAddressインスタンスを作成
        Ok(mac_address) => Ok(WasmMacAddress {
            inner_mac: mac_address
        }),
        // 変換失敗時はエラーメッセージをJavaScript用の値に変換
        Err(error_message) => Err(JsValue::from_str(error_message))
    }
}

ができるようになります。

さて、ここで出てきた、JsValueについて、少々。
Rustのデータ型とJavaScriptのデータ型は異なるため、直接的に値を渡すことができません。 JsValueを使うことで、Rust側で扱う値をJavaScriptが理解できる形式に変換できます。 逆に、JavaScript側の値をRustで使える形式に変換することもできます。 そんな便利な型です。 Rust->JS

メソッド 用途
JsValue::from_str(&str) Rustの文字列をJavaScriptの文字列に変換する
JsValue::from_f64(f64) Rustの浮動小数点数をJavaScriptのnumberに変換する
JsValue::from_bool(bool) Rustのブール値をJavaScriptのbooleanに変換する
JsValue::null() JavaScriptのnullを表現
JsValue::undefined() JavaScriptのundefinedを表現

JS->Rust

メソッド 用途
js_value.is_null() JsValueがJavaScriptのnullかどうかを確認
js_value.is_undefined() JsValueがJavaScriptのundefinedかどうかを確認
js_value.as_string() JsValueを文字列に変換(文字列でない場合はNone
js_value.as_f64() JsValueを浮動小数点数に変換(数値でない場合はNone
js_value.as_bool() JsValueをブール値に変換(ブール値でない場合はNone

というように、Javascript側と値のやりとりができるようになっています。 今後進んで行った時に、Javascript側とJSONでRustの構造体のやりとりについても出てきますのでまたその時に。

2つ目の修正は、それぞれのRust構造体のimplにto_string()関数を書いていましたが、
impl fmt::Display for XXXで、文字列表現を定義していたので不要になります。
各構造体のimplの方からto_string()関数がある場合は消しておいてください。

では、このようなカタチで、それぞれをWebAssembly化しましょう。一気にいきます。

//////////////////////////////////////////////
// IPv4/IPv6アドレスのWebAssembly対応ラッパー構造体
//////////////////////////////////////////////

/// WebAssemblyからIPv4アドレスを扱うためのラッパー構造体
/// inner_ip: 内部に保持する実際のIPv4Addressインスタンス
#[wasm_bindgen]
pub struct WasmIPv4Address {
    inner_ip: IPv4Address  // IPv4Addressインスタンスを明示的な名前で保持
}

#[wasm_bindgen]
impl WasmIPv4Address {
    /// 新しいIPv4アドレスインスタンスを作成
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let ipv4 = new WasmIPv4Address();
    /// ```
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        // 内部のIPv4Addressインスタンスを作成して保持
        WasmIPv4Address {
            inner_ip: IPv4Address::new()
        }
    }

    /// 文字列からIPv4アドレスを生成
    /// 
    /// ###引数
    /// * `ip_str` - "192.168.1.1" 形式のIPv4アドレス文字列
    /// 
    /// ### 戻り値
    /// * `Result<WasmIPv4Address, JsValue>` - 成功時はWasmIPv4Address、失敗時はエラーメッセージ
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let ipv4 = WasmIPv4Address.from_string("192.168.1.1");
    /// ```
    #[wasm_bindgen]
    pub fn from_string(ip_str: &str) -> Result<WasmIPv4Address, JsValue> {
        // 文字列をIPv4Addressに変換を試みる
        match IPv4Address::from_string(ip_str) {
            // 変換成功時は新しいWasmIPv4Addressインスタンスを作成
            Ok(ip_address) => Ok(WasmIPv4Address {
                inner_ip: ip_address
            }),
            // 変換失敗時はエラーメッセージをJavaScript用の値に変換
            Err(error_message) => Err(JsValue::from_str(error_message))
        }
    }

    /// IPv4アドレスを文字列形式で取得
    /// 
    /// ### 戻り値
    /// * `String` - "192.168.1.1" 形式のIPv4アドレス文字列
    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        // 内部のIPv4Addressインスタンスの文字列表現を取得
        self.inner_ip.to_string()
    }

    /// IPv4アドレスをバイト配列として取得
    /// 
    /// ### 戻り値
    /// * `Uint8Array` - 4バイトのIPv4アドレスデータ
    #[wasm_bindgen]
    pub fn to_bytes(&self) -> Uint8Array {
        // 内部のIPv4Addressインスタンスからバイト配列を取得
        let ip_bytes = self.inner_ip.to_array();
        // バイト配列をJavaScript用のUint8Arrayに変換
        Uint8Array::from(&ip_bytes[..])
    }
}

/// WebAssemblyからIPv6アドレスを扱うためのラッパー構造体
/// inner_ip: 内部に保持する実際のIPv6Addressインスタンス
#[wasm_bindgen]
pub struct WasmIPv6Address {
    inner_ip: IPv6Address  // IPv6Addressインスタンスを明示的な名前で保持
}

#[wasm_bindgen]
impl WasmIPv6Address {
    /// 新しいIPv6アドレスインスタンスを作成
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let ipv6 = new WasmIPv6Address();
    /// ```
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        // 内部のIPv6Addressインスタンスを作成して保持
        WasmIPv6Address {
            inner_ip: IPv6Address::new()
        }
    }

    /// 文字列からIPv6アドレスを生成
    /// 
    /// ### 引数
    /// * `ip_str` - "2001:db8::1" 形式のIPv6アドレス文字列
    /// 
    /// ### 戻り値
    /// * `Result<WasmIPv6Address, JsValue>` - 成功時はWasmIPv6Address、失敗時はエラーメッセージ
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let ipv6 = WasmIPv6Address.from_string("2001:db8::1");
    /// ```
    #[wasm_bindgen]
    pub fn from_string(ip_str: &str) -> Result<WasmIPv6Address, JsValue> {
        // 文字列をIPv6Addressに変換を試みる
        match IPv6Address::from_string(ip_str) {
            // 変換成功時は新しいWasmIPv6Addressインスタンスを作成
            Ok(ip_address) => Ok(WasmIPv6Address {
                inner_ip: ip_address
            }),
            // 変換失敗時はエラーメッセージをJavaScript用の値に変換
            Err(error_message) => Err(JsValue::from_str(error_message))
        }
    }

    /// IPv6アドレスを文字列形式で取得
    /// 
    /// ### 戻り値
    /// * `String` - "2001:db8::1" 形式のIPv6アドレス文字列
    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        // 内部のIPv6Addressインスタンスの文字列表現を取得
        self.inner_ip.to_string()
    }

    /// IPv6アドレスをバイト配列として取得
    /// 
    /// ### 戻り値
    /// * `Uint8Array` - 16バイトのIPv6アドレスデータ
    #[wasm_bindgen]
    pub fn to_bytes(&self) -> Uint8Array {
        // 内部のIPv6Addressインスタンスからバイト配列を取得
        let ip_bytes = self.inner_ip.to_array();
        // バイト配列をJavaScript用のUint8Arrayに変換
        Uint8Array::from(&ip_bytes[..])
    }
}

//////////////////////////////////////////////
// イーサネットフレームと物理層フレームのWebAssembly対応ラッパー構造体
//////////////////////////////////////////////

/// WebAssemblyからイーサネットフレームを扱うためのラッパー構造体
/// inner_frame: 内部に保持する実際のEthernetFrameインスタンス
#[wasm_bindgen]
pub struct WasmEthernetFrame {
    inner_frame: EthernetFrame
}

#[wasm_bindgen]
impl WasmEthernetFrame {
    /// 新しいイーサネットフレームを作成
    /// 
    /// ### 引数
    /// * `dst_mac` - 宛先MACアドレス
    /// * `src_mac` - 送信元MACアドレス
    /// * `ethertype` - イーサタイプ (例: 0x0800 for IPv4)
    /// * `data` - ペイロードデータのバイト配列
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let frame = new WasmEthernetFrame(dstMac, srcMac, 0x0800, new Uint8Array([...]));
    /// ```
    #[wasm_bindgen(constructor)]
    pub fn new(dst_mac: &WasmMacAddress, src_mac: &WasmMacAddress, ethertype: u16, data: &[u8]) -> Self {
        // 新しいEthernetFrameインスタンスを作成
        WasmEthernetFrame {
            inner_frame: EthernetFrame::new(
                Some(dst_mac.inner_mac.clone()),  // 宛先MACアドレスをクローン
                Some(src_mac.inner_mac.clone()),  // 送信元MACアドレスをクローン
                Some(ethertype),                  // イーサタイプ
                Some(data.to_vec())               // データをベクターにコピー
            )
        }
    }
    /// イーサーネットフレームを文字列形式で取得
    /// 
    /// ### 戻り値
    /// * `String` - "#dst_mac,#src_mac,#type,#data" 形式の文字列
    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        // 内部のEthernetFrameインスタンスの文字列表現を取得
        self.inner_frame.to_string().replace("\n","\r\n")
    }

    /// フレームの合計長を取得(バイト単位)
    /// 
    /// ### 戻り値
    /// * `usize` - イーサーネットフレームの総バイト長
    #[wasm_bindgen]
    pub fn total_length(&self) -> usize {
        // 内部のEthernetFrameインスタンスの長さを取得
        self.inner_frame.total_length()
    }

    /// イーサネットフレーム全体をバイト配列として取得
    /// 
    /// ### 戻り値
    /// * `Uint8Array` - フレーム全体のバイトデータ
    /// (宛先MAC + 送信元MAC + イーサタイプ + データ)
    #[wasm_bindgen]
    pub fn to_bytes(&self) -> Uint8Array {
        let mut bytes = Vec::new();
        
        // フレームの各フィールドをバイト配列に追加
        bytes.extend_from_slice(&self.inner_frame.dst_mac.to_array());  // 宛先MAC
        bytes.extend_from_slice(&self.inner_frame.src_mac.to_array());  // 送信元MAC
        bytes.extend_from_slice(&self.inner_frame.ethertype.to_be_bytes());  // イーサタイプ
        bytes.extend_from_slice(&self.inner_frame.data);  // ペイロードデータ
        
        // バイト配列をJavaScript用のUint8Arrayに変換
        Uint8Array::from(&bytes[..])
    }
}

/// WebAssemblyから物理層フレームを扱うためのラッパー構造体
/// inner_frame: 内部に保持する実際のPhysicalLayerFrameインスタンス
#[wasm_bindgen]
pub struct WasmPhysicalLayerFrame {
    inner_frame: PhysicalLayerFrame
}

#[wasm_bindgen]
impl WasmPhysicalLayerFrame {
    /// 新しい物理層フレームを作成
    /// 
    /// ### 引数
    /// * `ethernet_frame` - カプセル化するイーサネットフレーム
    /// 
    /// ### 使用例(JavaScript):
    /// ```javascript
    /// let phyFrame = new WasmPhysicalLayerFrame(ethernetFrame);
    /// ```
    #[wasm_bindgen(constructor)]
    pub fn new(ethernet_frame: &WasmEthernetFrame) -> Self {
        // 新しいPhysicalLayerFrameインスタンスを作成
        WasmPhysicalLayerFrame {
            inner_frame: PhysicalLayerFrame::new(
                Some(ethernet_frame.inner_frame.clone())  // イーサネットフレームをクローン
            )
        }
    }

    /// 物理層フレームを文字列形式で取得
    /// 
    /// ### 戻り値
    /// * `String` - "#preamble,#sfd,#ethernet_frame" 形式の文字列
    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        // 内部のEthernetFrameインスタンスの文字列表現を取得
        self.inner_frame.to_string().replace("\n","\r\n")
    }

    /// フレームの合計長を取得(バイト単位)
    /// プリアンブル、SFD、イーサネットフレーム、FCSを含む
    /// 
    /// ### 戻り値
    /// * `usize` - フレームの総バイト長
    #[wasm_bindgen]
    pub fn total_length(&self) -> usize {
        // 内部のPhysicalLayerFrameインスタンスの長さを取得
        self.inner_frame.total_length()
    }

    /// 物理層フレーム全体をバイト配列として取得
    /// 
    /// ### 戻り値
    /// * `Uint8Array` - フレーム全体のバイトデータ
    /// (プリアンブル + SFD + イーサネットフレーム + FCS)
    #[wasm_bindgen]
    pub fn to_bytes(&self) -> Uint8Array {
        // 内部のPhysicalLayerFrameインスタンスからバイト配列を取得
        let bytes = self.inner_frame.to_bytes();
        // バイト配列をJavaScript用のUint8Arrayに変換
        Uint8Array::from(&bytes[..])
    }
}

これに、先ほどの初期化のinit()関数を加えて完成です。

// wasm-bindgenの初期化関数
// このマクロは、WebAssemblyモジュールが読み込まれた時に自動的に実行される関数を指定します
#[wasm_bindgen(start)]
pub fn init() {
    // Rustのパニック時のエラーメッセージをブラウザのコンソールに表示するように設定
    // これにより、デバッグが容易になります
    console_error_panic_hook::set_once();
}

1つ、気になる点があるかもしれません。各WasmXXXの中のto_string()関数ですね。 捕捉しておきます。今回HTML側で、terminalに表示する際にわかったのですが、 terminalでは改行コードが\r\nでないと表示がズレズレになってしまいました。 それで、WebAssembly用としてこのto_string()インターフェースでは、返すときに 改行コードを変換するようにしておきました。

pub fn to_string(&self) -> String {
    // 内部のEthernetFrameインスタンスの文字列表現を取得
    self.inner_frame.to_string().replace("\n","\r\n")
}

   これで、WebAssembly化の完了です。 やったことをまとめると、こう言う感じです。

Rustで実装した内容を、WebAssembly用(JS用)の構造体でラッピングして、 Javascript側からはこのWebAssembly用の構造体に用意されたメソッド(関数)経由 Rustの機能を使えるようになります。 その際、JsValueを使い、JS側から渡されたデータをRust用のデータにしたり、 Rust側のデータ型をJavascript用に変換処理をして、受け渡ししている。

これだけです。この構造だけ覚えておけば、なんでもWebAssembly化できますよ。

WebAssemblyにビルド

さて、コードを書いただけでは動きませんので、WebAssemblyにビルドしましょう。WebAssemblyにするには、

  1. WebAssembly向けのコンパイルには、Rustの 「wasm32-unknown-unknown」ターゲットを追加する必要があります。これは、WASMバイナリを生成するためのプラットフォームターゲットです。次のように入力することで、インストールされます。
rustup target add wasm32-unknown-unknown

どういうターゲットがインストールされているかリストを見ることもできます。

rustup target list --installed
  1. wasm-packは、RustコードをWebAssemblyにビルドし、JavaScriptと統合するための便利なツールです。
cargo install wasm-pack

このwasm-packでビルドします。インストールされたか確認するには、

wasm-pack --version

では、上記2つのインストールが完了したら、いよいよビルドしていきます。

wasm-pack build

・・・。勢いよくbuildしようとしましたが、エラーが出ますね。。

error: the wasm*-unknown-unknown targets are not supported by default, you may need to enable the "js" feature. For more information see: https://docs.rs/getrandom/#webassembly-support
(途中省略)
error[E0433]: failed to resolve: use of undeclared crate or module `imp`

これは、new()するときに使っていた、randクレートによるものです。 通常のライブラリを作るときはそのままで大丈夫なのですが、 WebAssemblyにビルドするときは注意が必要です。
randクレートは乱数生成のためにgetrandomクレートを使用します。 getrandomは、「動作するプラットフォームに応じて異なる方法で乱数を生成」します。 例えば: ネイティブ環境(Linux, macOS, Windowsなど)では、OSが提供する乱数APIを使用(例: /dev/random, Windows API)。
WebAssembly環境では、JavaScriptのcrypto.getRandomValuesを使用。
WebAssembly環境では、wasm32-unknown-unknownターゲットがデフォルトではgetrandomに適切なバックエンドを提供しません。そのため、このエラーが発生します。

この場合、使えないと言うわけではなく、Cargo.tomlでWebAssembly用に使うよ。と指定してあげれば大丈夫です。randのすぐ下に、getrandomをこのように追加してあげてくだださい。

rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }

このように、Rustで他のターゲット用にはビルドできても、WebAssembly用にビルドしようとすると エラーが出てくる場合がありますが、まぁまぁのクレートにはWebAssembly用の対応が入っていたりしますので、Docを見て見てください。それでもダメな場合は、別のクレートに変えるしかないですね。。

さて、気を取り直して再度、

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がWebAssemblyファイルです。 ここまで順番に読み進めてこられた方は、もう大丈夫だと思いますが、このページから入った人で、 それぞれのファイルについての詳細を知りたい人は、0-1.開発環境の構築と「Hello, WebAssembly!」や、0-2.Hello, WebAssembly!の謎(by 調査班)をご覧ください。

HTML側

ということで、WebAssemblyが出来上がりましたので、はやくこれをHTML/Javascript側から操作したいですね。 まずは、HTMLを書きます。HTMLは、rust側のディレクトリに書いて大丈夫です。

今回HTML側の構成はこうなります。

index.html : HTML styles.css : スタイルシートをまとめる main.js : JS側の総まとめ的役割 terminal.js: HTMLに表示するterminalの制御 commands.js: terminalに入力された値を元にコマンド解析&実行

このような形で、commands.jsがコマンドを解析してWebAssemblyとのやりとりをしていきます。 今後WebAssembly側が増えてここでテストしたい時は、このファイルにコマンドを追加していくだけでよくなります。

index.html css/styles.css, js/main.js, js/terminal.js, js/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

index.html

まずは、HTMLから、 terminalの表示には、xterm.jsを使います。index.htmlは、

<!DOCTYPE html>
<html>
<head>
    <title>Packet Pilot Terminal</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.js"></script>
    <link rel="stylesheet" href="css/styles.css" />
    <script type="module" src="js/main.js"></script>
</head>
<body>
    <div class="terminal-container">
        <!-- タイトルバー -->
        <div class="terminal-title-bar">
            <div class="terminal-drag-indicator"></div>
            <span class="terminal-title">Packet Pilot Terminal</span>
            <!-- クリアボタン -->
            <button id="clearButton" class="clear-button" title="Clear Terminal">
                <i class="fa fa-refresh"></i> <!-- リロードアイコン -->
            </button>
        </div>
        <!-- ターミナル本体 -->
        <div id="terminal"></div>
    </div>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</body>
</html>

scriptのところはmoduleにして、importなど行えるようにしています。 これは前々回くらいにやりましたね。覚えていますか?

styles.css

スタイルは、このようにしています。ここはご自分で好きな配色にしてください。 style/styles.cssは、

body {
    margin: 0;
    padding: 10px;
    background-color: #afd4fa;
    color: #fff;
    font-family: monospace;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

.terminal-container {
    width: 100%;
    max-width: 600px;
    height: 380px;
    border-radius: 6px;
    background-color: #1e1e1e; /* 外枠の色 */
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); /* 影 */
    overflow: hidden;
}

.terminal-title-bar {
    height: 30px;
    background: linear-gradient(to bottom, #3c3c3c, #2c2c2c); /* グラデーション */
    display: flex;
    align-items: center;
    padding: 0 10px;
    color: #fff;
    font-size: 14px;
    font-weight: bold;
    border-bottom: 1px solid #000;
    justify-content: space-between;
}

.terminal-drag-indicator {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: #f56565; /* 閉じるボタンの色 */
    margin-right: 10px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

/* SVG アイコンのスタイル */
.terminal-title-bar svg {
    margin-right: 8px; /* タイトルとアイコンの間のスペース */
    fill: #fff; /* アイコンの色 */
}

.terminal-title {
    flex-grow: 1; /* タイトルを中央寄せに見せる */
    text-align: center;
}

/* クリアボタンのスタイル */
.clear-button {
    height: 20px;  /* アイコンの高さ */
    width: 20px;   /* アイコンの幅 */
    background: none;
    border: none;
    color: white;
    font-size: 16px; /* アイコンのサイズ */
    cursor: pointer;
    padding: 0;  
    margin-left: 10px;
    transition: background-color 0.3s ease;
}

.clear-button:hover {
    background-color: rgba(255, 255, 255, 0.1);
}

.clear-button:focus {
    outline: none;
}

#terminal {
    width: 100%;
    height: calc(100% - 30px); /* タイトルバーを除外した高さ */
    background-color: #000;
    padding: 5px;
}

main.jsについて

いよいよ、WebAssemblyとやりとりするJavascriptに入っていきます。

まずは、js/main.jsから、

import { TerminalManager } from './terminal.js';

document.addEventListener('DOMContentLoaded', () => {
    const terminalManager = new TerminalManager();
        
    terminalManager.initialize();

    // クリアボタンの取得とクリックイベントリスナーの追加
    const clearButton = document.getElementById('clearButton');
    if (clearButton) {
        clearButton.addEventListener('click', () => {
            terminalManager.clear();  // ターミナルをクリア
        });
    }
});

ここでは、まずHTMLがすべて読み込まれたら、'TerminalManager'を初期化します。

terminal.jsについて

terminal.jsの方を見ていきましょう。

import { CommandProcessor } from "./commands.js";

// TerminalManagerクラス - ターミナルの表示と入力管理を担当
export class TerminalManager {
    // コンストラクタ - クラスの初期化時に呼ばれる
    constructor() {
        // インスタンス変数の初期化
        this.term            = null;    // xterm.jsのインスタンス
        this.currentInput    = '';      // 現在の入力文字列
        this.currentPosition = 0;       // カーソル位置
        this.commandHistory  = [];      // コマンド履歴
        this.historyPosition = -1;      // 履歴内の現在位置
        this.commandProc     = null;    // Command処理クラス
    }

    // ターミナルの初期化メソッド
    initialize() {
        // xtermの設定とインスタンス化
        this.term = new Terminal({
            cursorBlink: true,      // カーソルの点滅
            cursorStyle: 'block',   // カーソルのスタイル
            fontSize: 12,           // フォントサイズ
            fontFamily: 'Consolas, "Liberation Mono", Courier, monospace',
            theme: {
                background: '#000000',
                foreground: '#ffffff'
            },
            cols: 80,              // 列数
            rows: 24               // 行数
        });

        // DOMにターミナルを追加
        this.term.open(document.getElementById('terminal'));
        
        // 初期メッセージの表示
        this.term.writeln('Packet Pilot Terminal v1.0');
        this.term.writeln('Type "help" for available commands\n');
        this.term.write('$ ');
        this.commandProc = new CommandProcessor(this.term);

        // キーボードハンドラのセットアップ
        this.setupKeyboardHandler();
    }

    // キーボード入力のハンドリング設定
    setupKeyboardHandler() {
        this.term.onKey(({ key, domEvent }) => {
            const printable = !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;
    
            // Ctrl+C の処理
            if ((domEvent.ctrlKey || domEvent.metaKey) && domEvent.key === 'c') {
                domEvent.preventDefault(); // デフォルトの挙動を防止(特にコピー操作)
                const selection = window.getSelection()?.toString();
                if (selection) {
                    navigator.clipboard.writeText(selection).then(() => {
                        console.log('Copied to clipboard:', selection);
                    }).catch(err => {
                        console.error('Failed to copy:', err);
                    });
                }
            }
            // Ctrl+V の処理
            else if ((domEvent.ctrlKey || domEvent.metaKey) && domEvent.key === 'v') {
                domEvent.preventDefault(); // デフォルトの挙動を防止(特にペースト操作)
                navigator.clipboard.readText().then(text => {
                    if (text) {
                        this.currentInput += text;
                        this.currentPosition += text.length;
                        this.term.write(text); // ターミナルにペーストした内容を表示
                    }
                }).catch(err => {
                    console.error('Failed to paste:', err);
                });
            }
            else if (domEvent.keyCode === 13) {         // Enterキー
                this.term.writeln('');
                this.commandProc.parseCommand(this.currentInput);
                this.commandHistory.push(this.currentInput);
                this.historyPosition = -1;
                this.currentInput = '';
                this.term.write('$ ');
            } 
            else if (domEvent.keyCode === 8) {     // Backspaceキー
                if (this.currentPosition > 0) {
                    this.currentInput = this.currentInput.slice(0, -1);
                    this.currentPosition--;
                    this.term.write('\b \b');
                }
            }
            // 上矢印キーの処理を追加
            else if (domEvent.keyCode === 38) {    // ↑キー
                if (this.commandHistory.length > 0) {
                    // 現在の行をクリア
                    this.clearCurrentLine();
                    // 履歴位置を更新
                    if (this.historyPosition < this.commandHistory.length - 1) {
                        this.historyPosition++;
                    }
                    // プロンプトを描画
                    this.term.write('$ ');
                    // 履歴からコマンドを取得して表示
                    this.currentInput = this.commandHistory[this.commandHistory.length - 1 - this.historyPosition];
                    this.term.write(this.currentInput);
                    this.currentPosition = this.currentInput.length;
                }
            }
            // 下矢印キーの処理を追加
            else if (domEvent.keyCode === 40) {    // ↓キー
                // 現在の行を完全にクリア
                this.clearCurrentLine();
                // 履歴位置を更新
                if (this.historyPosition > -1) {
                    this.historyPosition--;
                }
                // プロンプトを再描画
                this.term.write('$ ');
                // 履歴からコマンドを取得して表示(または空)
                if (this.historyPosition === -1) {
                    this.currentInput = '';
                } else {
                    this.currentInput = this.commandHistory[this.commandHistory.length - 1 - this.historyPosition];
                }
                this.term.write(this.currentInput);
                this.currentPosition = this.currentInput.length;
            }
            else if (printable) {                  // 通常の文字入力
                this.currentInput += key;
                this.currentPosition++;
                this.term.write(key);
            }
        });
    }

    // ユーティリティメソッド
    writeln(text) { this.term.writeln(text); }    // 改行付きで書き込み
    clear() { this.term.clear(); }                // 画面クリア
    write(text) { this.term.write(text); }        // 改行なしで書き込み
    
    // 現在の行をクリア
    clearCurrentLine() {
        // カーソルを行の先頭に移動
        this.term.write('\r');
        // 現在の行を空白で埋めて消去
        this.term.write(' '.repeat(this.term.cols));
        // カーソルを再度行の先頭に戻す
        this.term.write('\r');
        // 入力内容とカーソル位置をリセット
        this.currentInput = '';
        this.currentPosition = 0;
    }

}

terminalのsetupKeyboardHandlerが入力制御ですね。ここら辺は、同じようなものを作る際大体同じ形で行けますので、他のプロジェクトでもそのまま使えると思います。

そして、main.jsから呼んでいたinitialize()で、初期化処理をしています。 特に大事なのは、this.commandProc = new CommandProcessor(this.term);の部分ですね。

キーボード入力のハンドリング設定で、Enterキーが押された時=コマンドが入力された時、 this.commandProc.parseCommand(this.currentInput);しています。

else if (domEvent.keyCode === 13) {         // Enterキー
    this.term.writeln('');
    this.commandProc.parseCommand(this.currentInput);
    this.commandHistory.push(this.currentInput);
    this.historyPosition = -1;
    this.currentInput = '';
    this.term.write('$ ');
} 

ここで、各コマンドを解析して、実行する処理に渡しています。

Terminalを複数表示したりする場合の時も考え、それぞれのTerminalManagerのインスタンスに 紐づけて、その子供としてCommandProcessorを1個配置するような構造にしておきました。

commands.jsについて

今回、このターミナルでできることを整理しておきます。 ・mac() でMACAddressがnew()される(=ランダムなアドレスが生成される) ・mac("11:22:33:44:55:66")とアドレス指定された場合はそのアドレスでMACAdressを作る ・ipv4() でIPv4Addressがnew()される(=ランダムなアドレスが生成される) ・ipv4('192.168.1.1')とアドレス指定された場合はそのアドレスでIPv4Addressを作る ・ipv6() でIPv6Addressがnew()される(=ランダムなアドレスが生成される) ・ipv6("xx:xx:...")とアドレスが指定された場合はそのアドレスでIPv6Addressを作る ・eth() でEthernetFrameがnew()される ・send() で擬似的に物理層から送り出したものとして、EthernetFrameがnew()されて、PhysicalLayerFrameが作られる。 ・show() で、今作られたPacketやAddressの一覧が表示される。 ・clear 画面クリアー ・help で、helpを表示する この辺りにしておきましょう。 では、これらコマンドを作っていきます。

commands.jsは、

import  init,{WasmMacAddress,WasmIPv4Address, WasmIPv6Address, WasmEthernetFrame, WasmPhysicalLayerFrame } from "../pkg/packet_pilot.js";

// CommandProcessorクラス - コマンドの処理を担当
export class CommandProcessor {
    // コンストラクタ - terminalManagerインスタンスを受け取る
    constructor(terminalManager){
        this.terminal = terminalManager;  // ターミナルへの参照を保持
        
        // ネットワーク関連の状態管理
        this.currentMac = null;
        this.currentIPv4 = null;
        this.currentIPv6 = null;
        this.currentEthernetFrame = null;
        this.currentPhysicalLayerFrame = null;
        this.wasm = this.initWasm();
    }

    async initWasm() {
        try {
            const wasmInstance = await init();
            console.log("WASM initialized successfully");
            return wasmInstance;
        } catch (error) {
            console.error("Error initializing WASM:", error);
            throw error;
        }
    }
    // -- コマンド定義 ------------------------------------
    // 今後テストとしてコマンドを追加したいときはここに追加していく
    // --------------------------------------------------
    commands = {
        // 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 details');
            this.terminal.writeln(' clear - Clear terminal');
            this.terminal.writeln(' help - Show this help message');
        },

        // macコマンド - MACアドレスの生成/設定
        mac: (addr = null) => {
            try {
                if (addr) {
                    addr = addr.replace(/['"]/g, '');
                    this.currentMac = WasmMacAddress.from_string(addr).to_string();
                } else {
                    this.currentMac = new WasmMacAddress().to_string();
                }
                this.terminal.writeln(`${this.currentMac}`);
            } catch (e) {
                this.terminal.writeln(`Error: ${e.message}`);
            }
        },
        // ipv4コマンド - IPv4アドレスの生成/設定
        ipv4: (addr = null) => {
            try {
                if (addr) {
                    addr = addr.replace(/['"]/g, '');
                    this.currentIPv4 = WasmIPv4Address.from_string(addr).to_string();;
                } else {
                    this.currentIPv4 = new WasmIPv4Address().to_string();
                }
                this.terminal.writeln(`${this.currentIPv4}`);
            } catch (e) {
                this.terminal.writeln(`Error: ${e.message}`);
            }
        },
        // ipv6コマンド - IPv6アドレスの生成/設定
        ipv6: (addr = null) => {
            try {
                if (addr) {
                    addr = addr.replace(/['"]/g, '');
                    this.currentIPv6 = WasmIPv6Address.from_string(addr).to_string();
                } else {
                    this.currentIPv6 = new WasmIPv6Address().to_string();
                }
                this.terminal.writeln(`${this.currentIPv6}`);
            } catch (e) {
                this.terminal.writeln(`Error: ${e.message}`);
            }
        },
        // ethコマンド - イーサーネットフレームPacketの生成/確認
        eth: () => {
            try {
                let dstMac = new WasmMacAddress();
                let srcMac = new WasmMacAddress();
                let type = '0x0800';
                let data = [];

                let frame = new WasmEthernetFrame(dstMac,srcMac,type,data);
                let frameString = frame.to_string();
                let modifiedFrameString = frameString.replace(
                    /#ethertype\s+:\s+([0-9A-Fa-f]+)/i,  // `ethertype`の値をキャプチャ
                    (match, p1) => `#ethertype   : 0x${p1.toUpperCase()}`  // 0x+元の値に置き換える
                );
                
                this.currentEthernetFrame = modifiedFrameString;

                this.terminal.write(`${this.currentEthernetFrame}`);
                this.terminal.writeln('');
                
            } catch (e) {
                this.terminal.writeln(`Error: ${e.message}`);
            }
        },
        // sendコマンド - 物理層のイーサーネットフレームPacketの生成/確認
        send:() =>{
            try{
                let dstMac = new WasmMacAddress();
                let srcMac = new WasmMacAddress();
                let frame = new WasmEthernetFrame(dstMac,srcMac,'0x0800',[]);

                let physicalFrame = new WasmPhysicalLayerFrame(frame);
                let frameString = physicalFrame.to_string();
                let modifiedFrameString = frameString.replace(
                    /#ethertype\s+:\s+([0-9A-Fa-f]+)/i,  // `ethertype`の値をキャプチャ
                    (match, p1) => `#ethertype   : 0x${p1.toUpperCase()}`  // 0x+元の値に置き換える
                );
                let totalLength = physicalFrame.total_length();

                this.currentPhysicalLayerFrame = modifiedFrameString;
                 
                this.terminal.write(`${this.currentPhysicalLayerFrame}`);
                
                this.terminal.writeln(`total length = ${totalLength}`);

                this.terminal.writeln('');
            }catch (e) {
                this.terminal.writeln(`Error: ${e.message}`);
            }
        },
        // clear - 画面のクリアー
        clear: () => {
            this.terminal.clear();
        },

        // show - 現在のそれぞれの設定値を表示する
        show: () => {
            if (this.currentMac) this.terminal.writeln(`${this.currentMac}`);
            if (this.currentIPv4) this.terminal.writeln(`${this.currentIPv4}`);
            if (this.currentIPv6) this.terminal.writeln(`${this.currentIPv6}`);
            if (this.currentEthernetFrame) this.terminal.writeln(`Ethernet Frame:\r\n${this.currentEthernetFrame}`);
            if (this.currentPhysicalLayerFrame) this.terminal.writeln(`Physical Layer Frame:\r\n${this.currentPhysicalLayerFrame}`);
        }
    };

    // コマンドの解析と実行
    parseCommand(input) {
        input = input.trim();
        if (!input) return;

        let match;
        // コマンドのパターンマッチング
        if (input === 'help' || input === 'clear' || input === 'show()') {
            const cmd = input.replace('()', '');
            this.commands[cmd]();
        }
        // 引数付きコマンドのパターンマッチング(例:mac('00:11:22:33:44:55')) 
        else if ((match = input.match(/^(\w+)\((.*)\)$/))) {
            const [, cmd, argsStr] = match;  // コマンド名と引数を分離
            if (this.commands[cmd]) {
                try {
                    // 引数の文字列をJavaScriptの配列に変換
                    const args = argsStr.trim() ? eval(`[${argsStr}]`) : [];
                    this.commands[cmd](...args);  // コマンドを実行
                } catch (e) {
                    this.terminal.writeln(`Error: ${e.message}`);
                }
            } else {
                this.terminal.writeln(`Unknown command: ${cmd}`);
            }
        } else {
            this.terminal.writeln(`Invalid command: ${input}`);
        }
    }
}

WebAssemblyの初期化について

まず、初期化部分で大事なのは、下記の2点です。constructorの中で、 ・自分の親であるterminalへの参照を保持しておき、コマンド結果がきちんとそのTerminalにかえるように。 ・WebAssemblyの初期化をする。

constructor(terminalManager){
    this.terminal = terminalManager;  // ターミナルへの参照を保持
    ....
    this.wasm = this.initWasm();
}

async initWasm() {
    try {
        const wasmInstance = await init();
        console.log("WASM initialized successfully");
        return wasmInstance;
    } catch (error) {
        console.error("Error initializing WASM:", error);
        throw error;
    }
}

initWasm()内では、await init()として、WebAssemblyモジュールのロードが完了するのを待っています。 init()が正常に完了したら、このクラス内では、this.wasm経由でWebAssemblyにしたRustのコードにアクセスできるようになります。

ここで、わざわざinitWasm()を分けているのは、constructorではasyncが使えないという理由からです。 それでconstructorの外に、async可能なinitWasm()を用意して、constructorが呼ばれた時にinitWasm()も呼ばれるようにしておきます。 initWasm()の中で、awaitinit()を呼び出せるようにしてあります。

コマンド解析について

terminalから渡された、文字列を解析して実行するのは、parseCommand(input)関数の部分です。 超簡易版のパーサーです。 今後色々テストしたいことが増えてきたら、ここも拡充していくと思いますが、メインで増えていくのは、 その上にあるcommandsと言う配列ですね。

解析して、コマンドとして認識されたら、

this.commands[cmd]();

と言う風に呼び出せるようにしておきました。MACアドレスやIPアドレスは引数として指定された場合は、その引数を元にデータを作るようにしましたので、

this.commands[cmd](...args);

と言う風に呼び出します。

配列にコマンド名をおき、その中身は、関数というちょっとトリッキーな形で持たせてあります。 自分でコマンドを増やしたいときは、このcommands配列にキーその処理内容をセットで増やすだけで拡充していけるのです。 Javascriptではこのようにして配列に関数も持てるのです。便利ですね。

引数の解析部分は、

const args = argsStr.trim() ? eval(`[${argsStr}]`) : [];

としています。

(match = input.match(/^(\w+)\((.*)\)$/))

として、入力された文字が例えば、「mac("11:22:33:44:55:66")」だった場合、 上記の正規表現で引っ掛かり、

const [, cmd, argsStr] = match;  // コマンド名と引数を分離

で、分離されて、 cmd = mac argsStr = "11:22:33:44:55:66" となります。 その後、

const args = argsStr.trim() ? eval(`[${argsStr}]`) : [];

で、もし引数部分があったら、eval([${argsStr}])をしています。

eval()は文字列として書かれたJavaScriptのコードを、実際のJavaScriptコードとして実行する関数です。
[${argsStr}] という形にすることで、その文字列を配列の要素として解釈するように指示しているわけです。
これにより、コマンドラインで入力された mac("11:22:33:44:55:66") のような文字列が、 JavaScriptの配列 ["00:11:22:33:44:55"] に変換されます。 そして、その配列の要素が commands[cmd](...args) の部分で関数の引数として展開されます。

...argsは何をしているのか?これはスプレッド構文と呼ばれるもので、配列の要素を展開して個別の引数として渡してくれます。

例えば、argsが複数の値が入った配列だったとした場合
args = ["a","b","c"];
commands[cmd](...args) ==> commands[cmd]("a","b","c") と言う風に実行されると言うことになります。
..argsと言う書き方じゃない場合は、 commands[cmd](args[0], args[1], args[2]); と言う風に書かないといけなくなります。 コマンド毎に、引数の数が変わるとき、いちいち個別に定義を書くのは面倒ですよね。そういう時これは 非常に便利な書き方ですので覚えておくといいかもです。

ここでは、evalで作った 配列["00:11:22:33:44:55"]を commands[cmd](...args)とすることで、 commands[cmd]("00:11:22:33:44:55")としていることになります。

実験

それでは、いよいよ動かしてみましょう。 VSCodeで作っている場合は、index.htmlを選択して右クリックで、

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

どうですか?表示されましたか?

まずは、mac()と打ってみてください。ランダムに生成されたMACアドレスが表示されます。

イヤーすごく長くなりましたが、このようにして、自分でPacketやAddressを作るのってどうですか? Packetやアドレスを作って操作する。この感覚をぜひ味わってみてください。 ご自分で、コマンドを追加して、色々発展させると面白いかもですね。

Packet PilotとしてはこのようにしてPacketを作ったり操ったりできるようになることを目指していますので、この感覚が非常に楽しいと思えたら、ぜひ、引き続きこの連載を見ていってください。

われわれは、最終的には、

このようなネットワークシミュレーターに仕立てていく予定ですのでおりますので、楽しみにしていてください。

さて、次回は、本誌vol.1でもご紹介した、イーサーネットケーブルについて、WebAssembly化していきます。

その前に、Packet、アドレスのことを再度復習したい方は、本誌Vol.1で復習しておいてくだださい。

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