C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

Rust で cdylib/wasm を吐く crate を分割したら依存先の機能を呼べなくなり、なんとなく extern crate を明示してみたら can't find されて5分くらい悩んだメモ

だいじな事:

  • crate を分割したら、お呼ばれされる側の Cargo.toml で [lib]crate-type が明示的に rlib を吐かない定義になっていないか確認しよう!

期待動作する例

# crate aaa に依存される側の crate bbb の Cargo.toml
# ☆ ↓ src/lib.rs ありの crate では書かなくても同義扱いなのだけど、今回のメモの本質的な部分なのであえて明示しました。
[lib]
crate-type = [ "rlib" ]
# crate bbb をに依存する側の crate A の Cargo.toml
[dependencies]
bbb = { path = "../bbb" }
// crate bbb に依存する crate aaa の main.rs
// ◎ Rust を edition = "2018" で使う場合は extern carate は不要です; あっても問題ないけど
extern crate bbb;
// ◎ use しなくてもシンボルへの完全なパスを書けば使えます
use bbb::some_module::some_sub_module::awesome_feature;
// ◎ crate bbb に分割した何かを使う的な模擬コード
let my_hoge = awesome_feature::hoge();

5分くらい悩んだダメな例

# crate aaa に依存される側の crate bbb の Cargo.toml
[lib]
crate-type = [ "cdylib" ] # ☆ rlib 出力が無いと依存してくれる側の .rs から extern して密結合できないのです。うっかり

解説

分割前の crate が .wasm を吐くとか、 .so/.dll/.dylib 的なそれを吐くのがプロジェクト単位での出力の場合、 [lib]crate-type = [ "cdylib" ] とか定義しているはずです。そのような aaa から bbb を分割する際に、 Cargo.tml の内容を aaa の複製を元に書き出し、 cdylib しか出力しない crate bbb を定義してしまうと、 crate aaa から [dependencies] で依存する事はできますが、rlib が無い状態では rust のソースコードから extern して密結合的に使う事はできません。 crate bbb が cdylib を出力する定義では crate aaa のビルドでも .wasm あるいは .so/.dll/.dylib 的なそれはビルドされます。その出力「も」欲しい場合もあるかとは思いますが、今回は crate aaa を整理のために crate bbb と分割し、 crate aaa から crate bbb へ依存するのが目的のため rlib 出力を追加定義または rlib 出力のみに変更するのが期待動作する分割に必要です。

React で TypeScript な Component 内の一部のメソッドから他の定義済みのはずのプロパティーや state へのアクセスが undefined になった時に思い出したいメモ

期待動作する例

↓は button をポチると hidden で置いてあるファイルアップロード用の input の click を発火するコンポーネント的な例です。

import React, { Component } from "react";

interface IProps { }
interface IState { }

export default class FileUploader extends Component<IProps, IState> {
  // ↓ TypeScript で refs 的なことをしたい場合に増える仕込み
  private fileUploader:React.RefObject<HTMLInputElement>;

  constructor(props:IProps)
  {
   super(props);
   // ↓ ref る仕込み
   this.fileUploader = React.createRef<HTMLInputElement>();
   this.state = { my_awesome_value: undefined };
  }

  // ↓こう書くと this.fileUploader など期待動作する
  //   あえて method{} ではなく property = closure な定義方法をしているところが核心です
  handleClick = (): void => {
    // 無事 ref れていれば input 要素の click を発火できてファイルオープンダイアログが出てくれるところ
    this.fileUploader.current?.click();
    // note: この handleClick の定義方法の場合は state へのアクセスも期待動作します
  }

  render() {
    // ↓ ref= に仕込み
    return (
      <div>
        <button onClick={this.handleClick} />
        <input type="file" id="file" ref={this.fileUploader} style={{ display: "none" }} />
      </div>
    );
  }
}

ダメな例

  // ↓メソッドの定義方法を一見シンプルなただのよくあるメソッド的に記述すると…
  //   handleClick 自体は button の onClick から呼ばれてくれますが…
  handleClick() {
    // ↑このメソッドの定義方法だと↓でfileUploader が undefined で runtime に死ぬ
    this.fileUploader.current?.click();
    // note: この handleClick の定義方法の場合は state へのアクセスも undefined で runtime に死にます
  }

React/TypeScript初心者にはしばらく何が起きているのか脳内が?で満たされながらの状況調査となりましたがわかってしまえば JavaScript 界隈ではしばしば遭遇する類のトラブルだったようです。

参考

wasm-bindgen で fetch する rustwasm 公式の example の async で JsFuture な run が実行時に失敗した場合にも安全に対応できるようにする方法のメモ

問題

  • この example の runrustwasm/wasm-bindgen のような、実装上GitHub APIで取得可能なポジトリーが実在する場合は問題ありません。
  • リポジトリーが存在しないパターンを run に与えて fetch させると async/JsFuture 処理系の都合か二度と run を使用できない .wasm が生成されてしまいます。
    • 実際にやってみると、一度非実在リポジトリーを叩いてエラーを出すと実在のリポジトリーを叩こうと叩くまいと run は二度と動作しなくなります。

問題は example の run の最後の2行:

    // Use serde to parse the JSON into a struct.
    let branch_info: Branch = json.into_serde().unwrap();

    // Send the `Branch` struct back to JS as an `Object`.
    Ok(JsValue::from_serde(&branch_info).unwrap())

解決方法

matchjsonErr<serde_json::error::Error>Err<JsValue> に射る:

 // 解決方法①段階
 match json.into_serde() as Result<Branch, serde_json::error::Error> {
 Ok(branch_info) => Ok(JsValue::from_serde(&branch_info).unwrap()),
 Err(e) => Err(JsValue::from_str(&format!("{}", e))),
 }

↑だとまだ json.into_serde()Ok だけど JsValue::from_serdeErr だった場合は死んでしまいますが、 GitHub API は実在しないリポジトリーに対しても JSON を返してはくれるので、さしあたりは問題にはなりません。そこも match すると↓:

 // 解決方法②段階
 match json.into_serde() as Result<Branch, serde_json::error::Error> {
  Ok(branch_info) => match JsValue::from_serde(&branch_info) {
   Ok(jsvalue) => Ok(jsvalue),
   Err(e) => Err(JsValue::from_str(&format!("{}", e))),
  },
  Err(e) => Err(JsValue::from_str(&format!("{}", e))),
 }

↑のそこはかとない多段 match のダサさを Resultmapmap_err で整理すると:

 // 解決方法③段階
 (json.into_serde() as Result<Branch, serde_json::error::Error>)
  .map(|branch_info| JsValue::from_serde(&branch_info).unwrap())
  .map_err(|e| JsValue::from_str(&format!("{}", e)))
  • (1. json.into_serde()): json.into_serde() := Ok<Branch>Err<serde_json::error::Error> です
  • (2. map): (a) が Ok の場合に mapfrom_serde の結果を unwrap := Ok<JsValue> または Err<serde_json::error::Error> です
  • (3. map_err): (a) または (b) の何れかが Err<serde_json::error::Error> の場合は map_errErr<JsValue> に変換されます

こうすると run の内部で Err<serde_json::error::Error> が発生しても Uncaught (in promise) missing filed `name` at line 1 column 104 のような console.error が吐かれるだけで、 run が使用不能には至らなくなります。

この他にも unwrap() により panic が発生する可能性がある部分はありますが、 GitHub が 404 になったり JSON レスポンスを廃止しない限りは事実上死には至らないです。学習用途ではなく実用する場合はより厳密に async/JsFuture の絡む内部で panic が発生しないように気にする必要はあります。

諸事情により React で TypeScript な web アプリの中から window に global な変数を write access する方法のメモ

Window をオレオレ派生した interface をでっちあげてキャストすれば window の任意のプロパティーに write access できる。後は野となれ山となれ。

↓例、React の state 持ちの Component な App のインスタンスwindow.app な global 変数として登録:

App.tsx:

// 凡例 ☆ := 本質的な部分
// 凡例 ◎ := おまけ, state を扱う部分

// ☆Window を派生して app 変数を持ったインターフェースをでっちあげる
interface IGlobalWindow extends Window{ app?: App; }

// ◎ これらは constructor で state を定義したい場合に必要なインターフェース
interface IProps {}
interface IState { aaa?: any; }

export default class App extends Component<IProps, IState> {

 constructor( props: IProps ) {
  super(props);
  // ◎ ctor ではトリアエズ {} な aaa を this: App の .state 放り込んでおいてみます
  this.state = { aaa: {} }
  // ☆ window をオレオレ派生したインターフェースに明示的にキャストした上で .app 変数に書き込み; このメモの本質的な部分です
  (window as IGlobalWindow).app = this;
 }

 componentDidMount()
 {
  // ◎ 適当なタイミングで App の state を設定してみます
  this.setState( { aaa: 123 } );
 }

}

↑こうすると、ウェブブラウザーの inspector ツール的なそれの Console からも↓のように App のインスタンスやそのプロパティーにアクセスできるようになります:

> app.state.aaa
< 123

おまけメモ: React Developer Tools

これをブラウザー側へ入れられる場合は、導入後に React のページを読み込むと $r で React の Root にぶちこんだ Appインスタンスにアクセスできるようになります。今回のメモの主題とは異なりますが、外から中を見たいだけなら素直に React Developer Tools を入れればよいです。

f:id:USAGI-WRP:20200729151138p:plain

React プロジェクトの src 内のコンポーネントから諸事情によりウェブサイト外部の SCRIPT を読む方法のメモ

諸事情の例

4つの方法

もっとあるというのはさておき。

1. react-script-tag

この方法では DOM 構造的には in-situ な感じで <script> がぶちこまれます。

準備:

# .jsx 向けしか存在しないので .tsx では @ts-ignore して使います
yarn add react-script-tag

実用:

import React, { Component } from "react";
// note: ↓ .tsx ならこのコメントディレクティブを付けないとエラーになります。 .jsx なら不要です
// @ts-ignore
import ScriptTag from "react-script-tag";

export default class MyAwesomeMainCanvas extends Component {
  render() {
    return (
      <div>
        <canvas id="main-canvas"></canvas>
        <ScriptTag src="runner.main-canvas.something.js" />
      </div>
    );
  }
}

2. react-helmet

この方法では DOM 構造的には <head><script> (というか実際には任意の何かしら、 <title> とか <link> でも) をぶちこみます。

注意:

  • この方法を採用すると依存ライブラリーの都合か strict mode で警告↓がでて不穏になります。<script> 用途だけが目的の場合は他の方法がよいかもしれません。 ( Issue -> https://github.com/nfl/react-helmet/issues/426 )
index.js:1 Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
* Move code with side effects to componentDidMount, and set initial state in the constructor.
Please update the following components: SideEffect(NullComponent)

準備:

# for .jsx
yarn add react-helmet
# for .tsx; 両方必要になります
yarn add react-helmet @types/react-helmet

実用:

import React, { Component } from "react";
// for .jsx & .tsx
import { Helmet } from "react-helmet";

export default class MyAwesomeMainCanvas extends Component {
  render() {
    return (
      <div>
        <canvas id="main-canvas"></canvas>
        <Helmet>
          <script src="runner.main-canvas.something.js"></script>
          <title>にゃーん/title>
        </Helmet>
      </div>
    );
  }
}

3. DOM 操作でぶちこむ

いわゆるごりごり手書きした的な方法。

import React, { Component } from "react";

export default class MyAwesomeMainCanvas extends Component {
  render() {
    return (
      <div>
        <canvas id="main-canvas"></canvas>
      </div>
    );
  }
  
  componentDidMount() {
    const s = document.createElement( "script" );
    s.src = "runner.main-canvas.something.js";
    s.async = true;
    document.body.appendChild( s );
  }
}

複数箇所で使用したいとか再利用性が欲しい場合は方法(1),(2)を選択するほかか、この方法(3)も手書きオレオレコンポーネント化する方法もあります。コンポーネント化の方法はこのメモの本論ではないので省略。

4. React の Effect Hook 機能

方法(3) のごりごり手書きを若干変態気味に実装する亜種のような方法です。

カスタム・フックを作って:

import { useEffect } from 'react';

export default const myHook =
  (src) =>
  {
    useEffect(
      () =>
      {
        const s = document.createElement( "script" );
        s.src = src;
        s.async = true;
        document.body.appendChild( s );
        return () => {
          document.body.removeChild( s );
        };
      }
    , [src]
    );
  };

カスタム・フックをひっかける:

// for .jsx
// import myHook from 'myHook.jsx';
// for .tsx
import myHook from 'myHook';

const Demo = props => {
  importScript("runner.main-canvas.something.js");
}

参考

React なプロジェクトを TypeScript に移行したら3つの困りが発生したけど解決できたメモ

こまったこと:

1. カスタムソースディレクトリー ./src.reacttsc が認識できなかった

諸事情により ./src ではなく ./src.react に react 用のソースを配置していたが、 tsconfig.jsoninclude では "." 文字の入ったディレクトリーは認識しない仕様らしく困りました。

考えた解決方法:

  1. symlink を作って渡す
  2. ./src-react に変える

もんやり感はありましたが (2) でディレクトリーの名前から "." を排除して解決しました。ちなみに React のソースファイルパスのカスタムは react-app-rewired で eject-free に対応しています。

2. .tsx 化したモジュールで他の .tsx 化したモジュールを import できなかった

  • import Hoge from "./Hoge.tsx" ← ダメ🙅‍♀️
  • import Hoge from "./Hoge" ← ヨシ!🙆‍♀️

3. 独自定義のタグが JSX.IntrinsicElements に無いですよエラーで困った

独自定義のタグ、例えば GUI のルック・アンド・フィール的に Xel を使っていると <x-box> とか <x-button> とか使います。TypeScript化していない状態では一般的なHTMLタグと同様に埋め込めましたが、 TypeScript にしたら

Property 'x-box' does not exist on type 'JSX.IntrinsicElements'. TS2339

などと怒られてビルドできなくなりました。

単純なタグとしてのみの場合、かつ独自定義のタグが少ない場合は、

declare global
{ namespace JSX
 { interface IntrinsicElements
  { "my-awesome-tag": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; 
  }
 }
}

↑のようにまじめに TypeScript らしさを残したハック的なコードを <my-awesome-tag> を使いたい .tsx ファイル冒頭へ加えればよい。よいのだけど、 Xel を使いたい場合のようにたくさんのタグをぶちこみたい場合や、独自定義のタグに独自定義のプロパティーも使いたい場合は、 TypeScript にこだわって疲弊するよりは↓型は any にして "my-awesome-tag": any; のようにすると少し楽です。

参考

Web 向けの3D の GPU 描画コンテキストお取り扱いライブラリーの2020-07時点のメモ: ES 系, Unity, Unreal Engine, Rust 系

note: 2D 系(eg. phaser, ggez, etc. )は今回のメモでは含めていません。

Lang Library License WebGPU WebGL native ES & wasm
ES babylon.js Apache-2.0 Ready 2 %a n/a pure ES
ES three.js (+A-Frame) MIT(+MIT) (unknown†1) 1 + (2) n/a pure ES
ES playcanvas MIT (unknown†2) 2 %b n/a pure ES
C++ Unreal Engine *1 (unknown) 1 %c D3D12, Metal, OpenGL wasm(Emscripten)
C# Unity *2 (unknown) 2 %d D3D12, Metal, OpenGL wasm(Emscripten)
Rust unrust Apache-2.0 (unknown) 2 OpenGL wasm
Rust Amethyst(≈rendygfx-hal) Apache-2.0 or MIT (unknown; potentially maybe) (2;potentially) Vulkan, Metal, OpenGL wasm(; experimental)
Rust harmony Zlib (unknown†4; potentially yes) (2;potentially) Vulkan, Metal wasm(; help wanted)
Rust oxidator MIT (potentially yes) n/a Vulkan, D3D12 wasm(; not yet)
Rust piston MIT (unknown) n/a n/a n/a

おまけ: 所感

  • pure-ES 系で特に WebGPU を見据える場合は babylon.js で書くのが良さそう
  • リッチなエンジン系は…
    • Unreal EngineHTML5 出力は現実的に考えない方がいいです(UE5移行へWebGPUと併せて期待したいけど、期待しないでおいた方がいいかも)
    • 豊富なエコシステムやコミュニティーもあるので Unity で WebGL2 バックエンドにするのが現時点だけを見るなら現実的で良いかもしれません
    • Web 親和性の点では playcanvas もよいかもしれません
  • Rust 系は...
    • unrust は現時点で Web へきっちり出力できるたぶん唯一の3D-GPU描画系です
    • Amethyst は wasm+GL サポートを開発途中ですが、このメモの作成時点では全ての要素技術にかなり強い技師でも辛うじてバギーながらChromeで動くような動かないような微妙な出力に辿り着ける程度の状態です
    • harmony は描画バックエンドのポテンシャル的には WebGPU 対応、 OpenGL 対応により Web 出力できますが、採用する ECS バックエンドやマルチスレッドに最適化しまくっている事から WebWorker の制限と性能の懸念などもあり今の所 Author は Web 出力にそれほど積極的ではないようです。(Webよりエンジンとしての機能実装を進めたい状況でもあります)
    • oxidatorRTS ゲームエンジンに特化していますが、 Author も WebGPU での Web 出力は視野にあるようです。いまのところは WebGPU や rust と wasm 処理系の進化、ウェブブラウザーの進化待ちのようです
    • piston は Web に興味無さそうです

少なくとも、 Web 向けアプリの 3D-GPU 描画系に現時点で Rust を採用するのはわりとしんどいです。3年後に WebGPU と Rust/wasm 処理系が最高の選択肢になっている可能性はわりと高いとは思いますが、いまはまだしんどいです。 Rust を採用したい場合も、恐らく 3D-GPU 描画系はうまく機能分離し、できるだけ疎結合に近い設計にしつつ、ビジネスロジック部分は Rust で書いて wasm-unknown-unknown ターゲットで wasm を wasm-bindgen で library としてフロントエンドから叩く感じで作っておき、フロントエンドとして GUI系、 3D-GPU 描画系は pure-Web 系またはそれに近い要素技術でカバーするのが現時点では合理的かもしれません。

例えばフロントエンドは React あるいは yew と HTML と ES のライブラリーで GUI を作り、 3D-GPU 描画系は babylon.jsbabylon.rs で取り込み、アプリ本体の処理、状態やリソースの管理、Entity-Component-System、などなどは pure-Rust または wasm 対応が面倒ではない範囲の Rust のエコシステムを用いて作り、 wasm-bindgen で .wasm に固めるとか。