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 の
run
にrustwasm/wasm-bindgen
のような、実装上GitHub APIで取得可能なポジトリーが実在する場合は問題ありません。 - リポジトリーが存在しないパターンを
run
に与えてfetch
させるとasync
/JsFuture
処理系の都合か二度とrun
を使用できない .wasm が生成されてしまいます。
問題は 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())
解決方法
match
で json
の Err<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_serde
が Err
だった場合は死んでしまいますが、 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
のダサさを Result
の map
と map_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
の場合にmap
でfrom_serde
の結果をunwrap
:=Ok<JsValue>
またはErr<serde_json::error::Error>
です - (3.
map_err
): (a) または (b) の何れかがErr<serde_json::error::Error>
の場合はmap_err
でErr<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
- Firefox https://addons.mozilla.org/ja/firefox/addon/react-devtools/
- Chrome https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
- Edge https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil
これをブラウザー側へ入れられる場合は、導入後に React のページを読み込むと $r
で React の Root にぶちこんだ App
のインスタンスにアクセスできるようになります。今回のメモの主題とは異なりますが、外から中を見たいだけなら素直に React Developer Tools を入れればよいです。
React プロジェクトの src 内のコンポーネントから諸事情によりウェブサイト外部の SCRIPT を読む方法のメモ
諸事情の例
- コンポーネントの render 時に CDN やどこかしらから
<script src="https://example.com/hogehoge.js" crossorigin></script>
とかしたい - コンポーネントの render 時に .jsx/.tsx の翻訳時には有効ではない ECMAScript コードをちょろまかしたい
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 )- おまけ情報:
react-helmet
の代わりに<script>
をreact-script-tag
へ変更する際に<title>
についても代替が必要な場合は react-document-title & @types/react-document-title が便利です。
- おまけ情報:
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.react
を tsc
が認識できなかった
諸事情により ./src
ではなく ./src.react
に react 用のソースを配置していたが、 tsconfig.json
の include
では "." 文字の入ったディレクトリーは認識しない仕様らしく困りました。
考えた解決方法:
- symlink を作って渡す
./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(≈rendy≈gfx-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 Engine で HTML5 出力は現実的に考えない方がいいです(UE5移行へWebGPUと併せて期待したいけど、期待しないでおいた方がいいかも)
- 豊富なエコシステムやコミュニティーもあるので Unity で WebGL2 バックエンドにするのが現時点だけを見るなら現実的で良いかもしれません
- Web 親和性の点では playcanvas もよいかもしれません
- Rust 系は...
- unrust は現時点で Web へきっちり出力できるたぶん唯一の3D-GPU描画系です
- Amethyst は wasm+GL サポートを開発途中ですが、このメモの作成時点では全ての要素技術にかなり強い技師でも辛うじてバギーながらChromeで動くような動かないような微妙な出力に辿り着ける程度の状態です
- harmony は描画バックエンドのポテンシャル的には WebGPU 対応、 OpenGL 対応により Web 出力できますが、採用する ECS バックエンドやマルチスレッドに最適化しまくっている事から WebWorker の制限と性能の懸念などもあり今の所 Author は Web 出力にそれほど積極的ではないようです。(Webよりエンジンとしての機能実装を進めたい状況でもあります)
- oxidator は RTS ゲームエンジンに特化していますが、 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.js を babylon.rs で取り込み、アプリ本体の処理、状態やリソースの管理、Entity-Component-System、などなどは pure-Rust または wasm 対応が面倒ではない範囲の Rust のエコシステムを用いて作り、 wasm-bindgen で .wasm に固めるとか。