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

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

Rust で XML パーサー使いたいならどの crate を使うと嬉しいかもしれないか、のメモ

状況

crates.io には執筆時点では複数の XML お取り扱いらしい crate が存在しています:

などなど。他にもいくつも登録されています。

1つの性能指標: quick-xml vs. sxd-document vs. xml5ever vs. xml-rs

quick-xml, sxd-document, xml5ever, xml-rs の4つについて性能指標を計測する crate があるようです:

note: RazrFalcon/choose-your-xml-rs では RazrFalcon/roxmltree#alternatives を事実上の移行先として案内していますが、残念ながら RazrFalcon/roxmltree は git clone からとりあえず cargo bench したところ大量のエラーでビルドできず面倒くさい気配がしたのでベンチマークとしては触れない事にしました。( roxmltree 本体の cargo build は問題ないのだけど )

リポジトリーは既に archived ですが動作は可能でした。とりあえず clone してそのまま試してみると:

cargo bench # xml-rs-0.7 quick-xml-0.10 xml5ever-0.11 sxd-document-0.2
running 11 tests
test quick_xml_large     ... bench:   1,922,040 ns/iter (+/- 53,752)
test quick_xml_medium    ... bench:     519,440 ns/iter (+/- 20,166)
test quick_xml_small     ... bench:       8,187 ns/iter (+/- 262)
test sxd_document_medium ... bench:   2,767,680 ns/iter (+/- 295,128)
test sxd_document_small  ... bench:      46,143 ns/iter (+/- 6,440)
test xml5ever_large      ... bench:   9,244,405 ns/iter (+/- 629,496)
test xml5ever_medium     ... bench:   7,367,160 ns/iter (+/- 1,179,293)
test xml5ever_small      ... bench:      53,068 ns/iter (+/- 11,841)
test xmlrs_large         ... bench:  25,043,130 ns/iter (+/- 1,684,648)
test xmlrs_medium        ... bench:  11,846,650 ns/iter (+/- 2,163,808)
test xmlrs_small         ... bench:      87,464 ns/iter (+/- 16,859)

↑は依存が最新版に設定されていないので:

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

依存する XML crate 群を最新版に再定義し、 quick-xml と xml5ever のバージョンアップに伴う仕様変更に対応するパッチを充てて:

cargo bench # xml-rs-0.8.3 quick-xml-0.18.1 xml5ever-0.16.1 sxd-document-0.3.2
running 11 tests
test quick_xml_large     ... bench:   1,959,025 ns/iter (+/- 109,147)
test quick_xml_medium    ... bench:     510,790 ns/iter (+/- 87,736)
test quick_xml_small     ... bench:       7,367 ns/iter (+/- 154)
test sxd_document_medium ... bench:   2,809,025 ns/iter (+/- 323,460)
test sxd_document_small  ... bench:      44,845 ns/iter (+/- 1,459)
test xml5ever_large      ... bench:   8,118,040 ns/iter (+/- 569,471)
test xml5ever_medium     ... bench:   6,755,910 ns/iter (+/- 168,634)
test xml5ever_small      ... bench:      47,349 ns/iter (+/- 2,106)
test xmlrs_large         ... bench:  24,848,280 ns/iter (+/- 1,742,538)
test xmlrs_medium        ... bench:  12,093,030 ns/iter (+/- 978,933)
test xmlrs_small         ... bench:      92,197 ns/iter (+/- 6,255)

巨大なXMLを扱う、パース速度が大事、そのような場合はこれらの選択肢から選べば quick-xml がとても優秀っぽい事がわかります。

minidom ≈ quick-xml vs. roxmltree ≈ xmlparser

xmlparser は↑のベンチマークの Author の RazrFalcon が書いた crate です。↑のベンチマークで"事実上の移行先"としてリンクされていた roxmltreexmlparser をラップした高レベルパーサーという位置づけのようです。ここでちょっとした XML パーサー crate 群の整理:

high low
roxmltree xmlparser
sdx-document (独自の実装詳細)
xmltree xml-rs
minidom quick-xml

↑こんな高レベルのパーサーと低レベルパーサーの関係になっていたようです。その上で、巨大な XML を大量を扱いたい場合にはやはり速度は大事なので、 quick-xml vs. xmlparser あるいは minidom vs. roxmltree を中心に比較すると、

  • パース速度:
    • 高レベルのパース: roxmltree(xmlparser) が minidom(quick-xml) より 1.59 倍くらい高速
    • 低レベルのパース: quick-xml が xmlparser より 1.35 倍くらい高速
  • 列挙速度:
    • 任意要素の文字列マッチング: xmltree が roxmltree より 1.07 倍くらい高速、 minidom より 1.79 倍くらい高速
    • 特定名要素の検索: romxltree が xmltree より 4.58 倍くらい高速、 minidom より 5.86 倍くらい高速

らしい。また、 roxmltree について RazrFalcon によると:

  • xmlparser が quick-xml より遅い部分はより厳密なパースによるもの
  • roxmltree は設計上は panic を起こさないし内部で unsafe も使わない
  • roxmltree では XPath/XQuery, 変更や書き出し、仕様上完璧なXMLのサポートをする気は無いよ

と README に明記されています。

とりあえずの結論

  1. 高速な低レベルパーサーが必要な場合: quick-xml または次点で xmlparser
  2. 高速で安全安定っぽい高レベルサーバーが必要な場合: roxmltree
  3. もし特定の機能サポート都合で roxmltree ≈ xmlparser を使い難い場合:
    • 高レベル向け: xmltree, minidom, sdx-document, または別の何かを探すか作るか
    • 低レベル向け: quick-xml, xml-rs, または別の何かを探すか作るか

実際に使ってみてより詳細な気づきがあればその時にまたメモを追加しようと思います。

参考

VSCode via WSL2 で rust-analyzer が OUTPUT に Assertion failed: We don't expect to receive commands in CodeActions エラーを盛り盛り上げてきたら思い出すメモ

症状

WSL2 経由で動作する VSCode の rust-analyzer が一見動作しているように見えて OUTPUT に大量のエラーメッセージ:

Assertion failed: We don't expect to receive commands in CodeActions

を吐く。発症するとエラーが吐かれる度、作業中やログ監視などしている TERMINAL や PROBLEMS のタブからから OUTPUT タブへ表示も切り替わり不便です。

解決方法

さしあたり rust-analyzer を手インストールすると発生しなくなるかもしれません:

cargo install rust-analyzer --git https://github.com/rust-analyzer/rust-analyzer/

参考

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");
}

参考