Rust+Wasm+TypeScript
Hello World!

投稿日:

JavaScriptでのプログラミングが増えてきたので、TypeScriptを仲間に加えることにしました。Rust側からprintln!と同じ書式でブラウザのコンソールに出力するHello Worldプログラムの作成します。Windows10+VSCode環境でのTypeScript導入の説明です。Rustの環境構築は前回のHello Worldを見てください。インストールでnpmを使う以外は、TypeScript単独での利用となります。他の開発ツール(npm、webpackなど)は使用していません。

 

プロジェクト作成

前回のHello Worldと同じようにcargo newでプロジェクト作成、設定ファイルなど必要なファイルを用意します。それに加えてTypeScriptのソースコード用のフォルダを作成(ここではts/)。

TypeScript環境作成

インストール

npmを使ってTypeScriptをインストールします。(Node.js、npmは有名なので説明省略)VSCodeのTerminalかコマンドプロンプトでコマンド実行します。

> npm install -g typescript

インストールの確認、バージョンが表示されれば成功です。

> tsc --version
Version 3.6.3

tsconfig.jsonの作成

プロジェクトのフォルダで

> tsc init

デフォルトの設定ファイルtsconfig.jsonが作成されます。詳しい説明は省略します。自分で用意すれば実行する必要はありません。今回は、下記のような設定で行います。入出力フォルダ、コンパイル対象のファイル名などの設定のみ。

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "outDir": "./www",                        /* Redirect output structure to the directory. */
    "rootDir": "./ts",                        /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    
    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    
    /* Module Resolution Options */
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
  },
  "files" : ["ts/index.ts"]
}

コーディング

今回作成した、ソースコードです。

HTML

スクリプトを呼び出すだけです。ブラウザのコンソールだけだと分かりにくいので画面にも表示されるようにpタグ追加。

www/index.html<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Rust+Wasm+Ts Hello World</title>
    <script src="./index.js" type="module"></script>
  </head>
  <body>
    <p id="log-window"></p>
  </body>
</html>

TyepScript

TyepScriptによるwasm読み込み、Rust関数呼び、Rustからのconsole_log呼び出し対応。文字列の受け渡しがあるためちょっと長いです。コンパイル結果はwww/index.jsに出力。エラーが出たらとりあえずanyで対応。これから使い方を覚えていきます。

ts/index.ts
const imports = {
    env: {
        //rust側の関数名 : jsの関数
        js_console_log : console_log,   
    },
}

let WasmInst : any;

// wasm読み込み
fetch('./hello_world_ts.wasm')
    .then(response => {
        if(!response.ok)throw Error("file error -> hello_world_ts.wasm" );
        return response.arrayBuffer(); })
    .then(bytes => WebAssembly.instantiate(bytes, imports))
    .then(results => { init(results.instance); })
    .catch(err=>console.log(err));

function init(wasm : any){
    console.log("wasm stand-by");
    WasmInst = wasm;
    WasmInst.exports.hello_rust();
}

// rustから呼ばれる関数
// 文字列をUTF16(&[u16])で受け取る
function console_log(str_ptr : number, str_byte_len : number)
{
    //byte > u16(2byte)
    str_ptr /= 2;
    let str_len = str_byte_len/2;

    let buff16 = new Uint16Array(WasmInst.exports.memory.buffer);
    let str_buff = buff16.subarray(str_ptr, str_ptr + str_len);
    let str = u16_to_string(str_buff);
    console.log( str );
    
    let p = document.getElementById("log-window");
    if(p)p.textContent = str;
}

// Uint16Arrayをstringに変換
function u16_to_string(u16 : Uint16Array) : string {
    let str = "";
    let str_len = u16.length;
    
    for(let i=0;i<str_len;i+=512) {
        let end = i+512;
        if(end > str_len)end = str_len;
        let slice : any = u16.slice(i,end);
        // 引数の数に制限があるため分割
        str += String.fromCharCode.apply("", slice);
    }
    return str;
}

Rust

println!と同じ書式でブラウザーのコンソールへ文字の出力。RustはUTF8なのでUTF16に変換してからJSの関数へ渡しています。マクロ使ってます。

// JavaScript側の関数
extern "C" {
    // Vec<u16>のポインタを渡す
    fn js_console_log(ptr : usize, byte_size : usize);
}

//ブラウザのconsole表示へ println!と同じ使い方
macro_rules! console_log {
    ($a0:expr   $(,$an:expr)* ) => {
        rust_console_log(
            &format!(
                $a0
                $( ,$an )*
            )
        )
    };
}

//JavaScriptから呼ばれる関数
#[no_mangle]
fn hello_rust() {
    let log = "World!";
    console_log!("Hello {}", log);
}

// UTF8をUTF16に変換してJSのconsole.log呼び出し
fn rust_console_log(log : &str) {
    let log = string_to_utf16(log);
    unsafe{
        js_console_log(log.as_ptr() as usize, log.len()*2);
    }
}

// String(UTF8) ⇒ Vec<u16>(UTF16)
fn string_to_utf16(s : &str) -> Vec<u16> {
    s.encode_utf16().collect::<Vec<u16>>()
}

TypeScriptコンパイル

> tsc

これでtsconfig.jsonで設定されたtsファイルをコンパイルし、指定の場所に出力します。環境によって変わると思いますが、短いコードでも数秒ほどかかるので、JSの時のような手軽さはなくなります。ですが次に説明するwatch機能を使えば、かなり改善できます。

ファイル更新の監視 tsc -w

TypeScriptにはファイル更新を監視して自動的にコンパイルする機能があります。

> tsc -w

これで起動します。ただしTerminalを占有するため、rustのコンパイルのために別のTerminalを用意します。デフォルトではプロジェクトフォルダ(tsc -wを起動したフォルダ)の全ファイルを監視します。 tsconfig.jsonのfiles設定があれば、そのファイルだけ監視するようになります。tscは起動してコンパイル開始するまでが長いため、短いコードでも数秒かかります。ですが-wを使えば、コンパイル開始可能な状態で待機するため、短いコードであればコンパイルが一瞬で終わります。JSの時と同じ感覚で開発できます。これがなければ使うのを諦めていたところです。

ダウンロード

今回作成したプロジェクトです。バッチコマンドでのビルドになります。

“Rust+Wasm+TypeScript Hello World!” をダウンロード hello_world_ts.zip – 4 回のダウンロード – 15 KB

コメントを残す

メールアドレスが公開されることはありません。

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.