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

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

rust の wasm ターゲットのプロジェクトで surf-1.x を surf-2.x へ差し替える場合に必要な対応のメモ

surf-1.x 系では↓のように Cargo.toml にバージョン番号または所在だけ書けば wasm 向けにもそのまま使えました。べんり。

# surf-1.x
[dependencies]
surf = "1.0.3"

surf-2.x 系から↓のように Cargo.toml に default-featuresfeatures を明示的に設定しないと wasm ターゲットでビルドできなくなりました。

# surf-2.x
surf = {version = "2.1.0", default-features = false, features = ["wasm-client"]}

この設定を怠ると↓のようなエラーで依存 crates のビルド中に surf のビルドがエラーで失敗になります。

   Compiling surf v2.1.0
error[E0432]: unresolved import `http_client::isahc`
  --> C:\Users\usagi\.cargo\registry\src\github.com-1ecc6299db9ec823\surf-2.1.0\src\client.rs:12:26
   |
12 |         use http_client::isahc::IsahcClient as DefaultClient;
   |                          ^^^^^ could not find `isahc` in `http_client`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0432`.
error: could not compile `surf`.

surf-2.0 のリリースノートによくみると wasm 向けには features を手設定しないとダメになるよって書いてある&wasm-test/Cargo.tomlを参考にするとわかります。READMEでどどーんとWASMで使いたければこの設定してねとは書かれていないので少し不親切かもしれません。のでメモを残しました。

`yarn licenses list --json` ではしんどい気配の場合に `npx license-checker --json` すると嬉しいメモ、とおまけで `Cargo.toml` で rust の場合のメモ

package.json -> licenses.json

package.json と npm|yarn を使っているプロジェクトでお世話になっている依存ライブラリーのライセンス表示を自動生成したい時、とりあえず yarn

yarn licenses list --json

すると、↓しんどそうなJSON↓が出力される事があります。

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

このときの1581行目は200,054文字ありました。 prettier も通らないし、しんどそうです。

そこで、

に替えてみます:

npx license-checker --json --relativeLicensePath --summary

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

必要なところだけの出力が pretty されて得られました。この出力だと目視による部分的な確認も楽で、ライセンス表示の自動生成やプロジェクトの方針などで必要な場合にはGPLの混入防止検出なども問題なく処理できそうです。

おまけ: Cargo.toml -> licenses.json

cargo install cargo-license

しておいて、

cargo-license -j

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

Rust の trait で Option<Self> や Result<Self, E> を return する関数を定義しようとして [E0277] で怒られた時に思い出すメモ

問題

// ↓これは問題ありません
trait Trait { fn function() -> Self; }

// ↓これは E0277: the size for values of type `Self` cannot be known at compilation time
trait TraitOptional { fn function() -> Option<Self>; }

// -> Result<Self, E> とかする場合も同様です(省略)

回避方法

// Self の代わりにジェネリクスを噛ませて
trait TraitOptional<T> { fn function() -> Option<T>; }

struct Struct{}

// impl するときにジェネリクスの型引数に for と同じ型を渡す
impl TraitOptional<Struct> for Struct
{ 
 fn function() -> Option<Self>
 {
  Some( Self{} )
 }
}

Rust のメモリーコンテナー的な何かをわかりやすく整理したチートシートのメモ; T, Cell, RefCell, AtomicT, Mutex, RwLock, Rc, Arc

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

作ったので GitHub ↑ しつつ、なんとなくすごい久しぶりに Qiita に初心者さんに優しそうな雰囲気を装った解説↓

も書いてみました。ちなみに reddit

にもポストしてみました。 reddit はとても参考になる議論がおおむね建設的にものすごい勢いで発生してくれるので嬉しいです。チートシートとしての意図と図面の都合による nitpick たちは README に注釈を付けるといいかなと思います。Allocationからの線の見直しなど a better なシートの提案はうまく取り込んで、できるだけウソの無い、でもチートシートの意図としてはわかりやすさを維持した調整をしたいと思います。

と、それだけ書いたのではわざわざブログにメモを残す理由が薄いので、ここには C++ と Rust でチートシートに掲載したメモリーコンテナー的なそれの大雑把なコードを、翻訳が通るソースとして書き残してみます。

おまけ: C++ の &≈ptr と (mutable)/const 系、Rust の &≈ptr と mut/const 系

// C++
#include <memory>
#include <shared_mutex>
#include <vector>
#include <iostream>
int main()
{
 using T = int; using V = std::vector<T>;
 
 // mutable value
 T valm = 1;
  
 // const ref -> mutable value
 T& refc_valm = valm; refc_valm = 10;
 // const ref -> const value
 T const& refc_valc = valm; std::clog << refc_valc << " <- It's read only.\n";

 // mutable ptr -> mutable value
 T* ptrm_valm = &valm; *ptrm_valm = 11;
 // const ptr -> mutable value
 T* const ptrc_valm = &valm; *ptrc_valm = 21;
 // const ptr -> const value
 const T* const ptrc_valc = &valm; std::clog << *ptrc_valc << " <- It's read only.\n";

 // heap, ≈ 'static lifetime of Rust
 T* ptrm_heap_static_lifetime = new int(-1);

 // heap, ≈ mut Box<T> of Rust
 std::unique_ptr<T> ptrm_heap_single_owner_valm = std::make_unique<T>(-1);
 *ptrm_heap_single_owner_valm = -100;
 // heap, ≈ Box<T> of Rust
 std::unique_ptr<const T> ptrm_heap_single_owner_valc = std::make_unique<T>(-1);

 // heap, ≈ Rc<T> of Rust
 std::shared_ptr<const T> ptrm_heap_multiple_owner_thread_unsafe_valc = std::make_shared<T>(-1);
 // heap, ≈ Rc<Cell<T>> of Rust
 std::shared_ptr<T> ptrm_heap_multiple_owner_thread_unsafe_valm = std::make_shared<T>(-1);
 // heap, ≈ Rc<RefCell<V>> of Rust
 std::shared_ptr<V> ptrm_heap_multiple_owner_thread_unsafe_refm = std::make_shared<V>(V());
 ptrm_heap_multiple_owner_thread_unsafe_refm->push_back(-2000);

 // single reader/writer control object, ≈ Mutex<T> without <T> of Rust
 std::mutex mutex_1rw_thread_safe_controller;
  
 // multiple reader/single writer control object, ≈ RwLock<T> without <T> of Rust
 std::shared_mutex mutex_mr1w_thread_safe_controller;
}
// Rust; Rc:=Reference Counted, Arc:=Automatically Reference Counted
use std::
{ rc::Rc
, sync::{Arc, Mutex, RwLock}
, cell::{ Cell, RefCell }
};
fn main() {
 type T = i32; type V = Vec<T>;
 
 // mutable value
 let mut valm: T = 1;
 let mut valm_the_other = 2;
 
 // const ref -> mutable value
 let refc_valm: &mut T = &mut valm; *refc_valm = 11;
 // const ptr -> const value
 let refc_valc: &T = &valm; println!("{} <-- It's a const value via const ref.", refc_valc);
 
 // mutable ptr -> mutable value; deref-ptr is an unsafe operation
 let mut ptrm_valm: *mut T = &mut valm as *mut T; unsafe { *ptrm_valm = 21 };
 ptrm_valm = &mut valm_the_other as &mut T; unsafe { *ptrm_valm = 22 };
 // const ptr -> mutable value
 let ptrc_valm: *mut T = &mut valm as *mut i32; unsafe { *ptrc_valm = 31 };
 // const ptr -> const value
 let ptrc_valc: *const T = &valm as *const i32; println!("{:?} <-- It's a const value via const ptr.", ptrc_valc);
 
 // heap, ≈ new of C++
 let mut ptrm_heap_static_lifetime: *mut T = unsafe{ std::alloc::alloc( std::alloc::Layout::new::<T>() ) } as *mut T;
 unsafe { *ptrm_heap_static_lifetime = -10 };
 
 // heap, ≈ std::unique_ptr<T> of C++, deref -> mutable value
 let mut ptrm_heap_single_owner_valm: Box<T> = Box::<T>::new(-1);
 *ptrm_heap_single_owner_valm = -100;
 // heap, ≈ std::unique_ptr<const T> of C++, deref -> mutable value
 let ptrm_heap_single_owner_valc: Box<T> = Box::<T>::new(-1);
 
 // heap, ≈ std::shared_ptr<const T> of C++, deref -> const value
 let ptrm_heap_multiple_owner_thread_unsafe_valc: Rc<T> = Rc::<T>::new(-1);
 // heap, ≈ std::shared_ptr<T> + std::mutex of C++, deref -> mutable value
 let ptrm_heap_multiple_owner_thread_unsafe_valm: Rc<Cell<T>> = Rc::new(Cell::<T>::new(-1));
 ptrm_heap_multiple_owner_thread_unsafe_valm.set( -200 );
 // heap, ≈ std::shared_ptr<T> + std::mutex of C++, deref -> mutable ref
 let ptrm_heap_multiple_owner_thread_unsafe_refm: Rc<RefCell<V>> = Rc::new(RefCell::new(V::new()));
 ptrm_heap_multiple_owner_thread_unsafe_refm.borrow_mut().push( -2000 );
 
 // heap, ≈ std::shared_ptr<const T> of C++, deref -> const value
 let ptrm_heap_multiple_owner_thread_unsafe_valc: Arc<T> = Arc::<T>::new(-1);
 // heap, ≈ std::shared_ptr<T> + std::shared_mutex of C++, deref -> mutable value
 let ptrm_heap_multiple_owner_thread_unsafe_valm: Arc<Mutex<T>> = Arc::new(Mutex::<T>::new(-1));
 *ptrm_heap_multiple_owner_thread_unsafe_valm.lock().unwrap() = -200;
 // heap, ≈ std::shared_ptr<T> + std::shared_mutex of C++, deref -> mutable ref
 let ptrm_heap_multiple_owner_thread_unsafe_refm: Arc<RwLock<T>> = Arc::new(RwLock::<T>::new(-1));
 {
   let r0 = *ptrm_heap_multiple_owner_thread_unsafe_refm.read().unwrap();
   let r1 = *ptrm_heap_multiple_owner_thread_unsafe_refm.read().unwrap();
   println!("r0 = {:?}, r1 = {:?}", r0, r1);
   // drop(≈release) r0, r1 then it will be writable in the out of this scope
 }
 *ptrm_heap_multiple_owner_thread_unsafe_refm.write().unwrap() = -3000;
}

参考

Rust で実行中にバッファーの次元解釈を変更できる DimensionShiftableBuffer と翻訳時に任意の次元解釈をVec<T>に追加する vec-dimension-shift を公開しました。のメモ

前提として、どちらも単一のヒープに全体が連続したメモリーアドレスを持つバッファーを扱う、という事があります。そのうえで、バッファーを任意次元に再解釈します。

dimension_shiftable_buffer

実行時に任意の次元にバッファーの解釈を変更したビューを用いてバッファーを扱える、そういうものです。こちらの実装はすべて Safe です。 unsafe せずに Vec<T> を実行時に任意次元へ再解釈する方法は思いつかなかったので getter(get,pop,remove)/setter(push,append) と for_each を用意しました。こちらの利点は「実行時に」「任意次元へ」です。

// make a 2d-empty DimensionShiftableBuffer
let mut dsb = DimensionShiftableBuffer::<u8>::new(vec![], 2).unwrap();
// push a 2d-datum
dsb.push(&[0u8, 1]).unwrap();
// push a 2d-datum
dsb.push(&[2u8, 3]).unwrap();
// append a 2d-datum sequence
dsb.append(&[4u8, 5, 6, 7, 8, 9, 10, 11]).unwrap();
for index in 0..dsb.len().unwrap()
{
 // get a 2d slice
 assert_eq!(dsb.get(index).unwrap(), &[index as u8 * 2, index as u8 * 2 + 1]);
}
// shift dimension to 3 from 2
dsb.shift_dimension(3).unwrap();
// push a 3d-datum
dsb.push(&[12u8, 13, 14]).unwrap();
// get a 3d-datum
assert_eq!(dsb.get(0).unwrap(), &[0u8, 1, 2]);
assert_eq!(dsb.get(1).unwrap(), &[3u8, 4, 5]);
assert_eq!(dsb.get(2).unwrap(), &[6u8, 7, 8]);
assert_eq!(dsb.get(3).unwrap(), &[9u8, 10, 11]);
assert_eq!(dsb.get(4).unwrap(), &[12u8, 13, 14]);
// get a linear slice
let linear_slice = dsb.as_slice();
assert_eq!(linear_slice, &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);

vec-dimension-shift

こちらは翻訳時にN次元から実際に対応する具体的な次元の次元再解釈機能を選択的に Vec<T> へ付与する trait 集です。 usize 次元のすべてのパターンを lib に埋め尽くすわけにはいかないので、2..16次元は features で選択的に使用可能に、 default で 2,3,4 次元の traits を定義としつつ、 make_vec_dimension_shift_n_dimension! マクロをユーザーが任意に呼べるように pub り、 crate のユーザーが任意に欲しい次元を選択的に扱えるようにしています。

こっちの中身は黒魔術と unsafe でできています。次元再解釈時の境界チェック、直接の変換が不可能な場合のErr|truncate|paddingなど基本的には安全に使いやすいように実装していますが、N次元から1次元化するための flatten の実装では Vec の実装とメモリーレイアウトをにゃーんしたりしていたりします。

use vec_dimension_shift::{
 VecDimensionShift2D,
 VecDimensionShift2DFlatten,
 VecDimensionShift3D,
 VecDimensionShift3DFlatten
};

fn d2_and_d3()
{
 let original = vec![0.0, 1.1, 2.2, 3.3, 4.4, 5.5];
 dbg!(&original);

 let mut d2_shifted = original.as_2d_array().unwrap();
 dbg!(&d2_shifted);
 assert_eq!(d2_shifted[0], [0.0, 1.1]);
 assert_eq!(d2_shifted[1], [2.2, 3.3]);
 assert_eq!(d2_shifted[2], [4.4, 5.5]);
 d2_shifted[1][1] = -1.0;

 let flatten = d2_shifted.as_flatten();
 dbg!(&flatten);

 let mut d3_shifted = flatten.as_3d_array().unwrap();
 dbg!(&d3_shifted);
 assert_eq!(d3_shifted[0], [0.0, 1.1, 2.2]);
 assert_eq!(d3_shifted[1], [-1.0, 4.4, 5.5]);
 d3_shifted[1][1] = -2.0;

 let flatten = d3_shifted.as_flatten();
 dbg!(&flatten);

 assert_eq!(flatten, vec![0.0, 1.1, 2.2, -1.0, -2.0, 5.5])
}

ちなみに、 1D -> 2D とした後に 1D へ flattening せず、 1D -> 2D -> 3D と次元再解釈すると、

use vec_dimension_shift::make_vec_dimension_shift_n_dimension;

fn n_dimension_macro_generator()
{
 make_vec_dimension_shift_n_dimension! { VecDimensionShift2D, VecDimensionShift2DFlatten, as_2d_array_no_check, to_2d_array_no_check, as_2d_array, to_2d_array, as_2d_array_truncate, to_2d_array_truncate, as_2d_array_padding, to_2d_array_padding, 2 }
 make_vec_dimension_shift_n_dimension! { VecDimensionShift3D, VecDimensionShift3DFlatten, as_3d_array_no_check, to_3d_array_no_check, as_3d_array, to_3d_array, as_3d_array_truncate, to_3d_array_truncate, as_3d_array_padding, to_3d_array_padding, 3 }

 let original = vec![0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10, 11.11];
 dbg!(&original);

 let d2 = original.as_2d_array().unwrap();
 assert_eq!(d2[0], [0.0, 1.1]);
 assert_eq!(d2[1], [2.2, 3.3]);
 assert_eq!(d2[2], [4.4, 5.5]);
 assert_eq!(d2[3], [6.6, 7.7]);
 assert_eq!(d2[4], [8.8, 9.9]);
 assert_eq!(d2[5], [10.10, 11.11]);
 dbg!(&d2);

 let d3 = d2.as_3d_array().unwrap();
 assert_eq!(d3[0], [[0.0, 1.1], [2.2, 3.3], [4.4, 5.5]]);
 assert_eq!(d3[1], [[6.6, 7.7], [8.8, 9.9], [10.10, 11.11]]);
 dbg!(&d3);
}

こういう多N次元(N次元×N次元×…)も作れます。

Rust のダウンキャストめんどくさいでござる問題を mopa crate ですっきりするメモ

trait T を実装した struct S について、 S から &dyn T へアップキャストした後、 &dyn T から &S へダウンキャストでき…ません。Rustでtrait継承のdynなオブジェクトを扱おうとすると悲しむポイントです。

trait T { fn hello(&self); }
struct S {}
impl T for S{ fn hello(&self){ println!("It's S!") } }
impl S { fn hi(&self) { print!( "I can do HI!" ) }

↑これが定義できる、というところまではよいとして、↓はできません。

fn main()
{
 // OOPの派生型に相当する型の元のオブジェクト
 let s: S = S{};
 // OOPの基底型に相当するトレイトオブジェクトレベルでの保持
 let rs: &dyn T = &s as &dyn T;
 // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ
 rs.hello();
 // OOPで言うところの基底型から派生型へのダウンキャスト
 let s: &S = rs as &S;
 // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず
 s.hi();
}
error[E0605]: non-primitive cast: `&dyn T` as `&S`
 --> src\main.rs:9:14
  |
9 |  let s: &S = rs as &S;
  |              ^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

error: aborting due to previous error

「にゃーん?」って言ったら助けてくれるイケメンが臨席にいればよいのですが、いません。これは↓のように std::any::Any を介するとできます:

// にゃーんだけど一応動く…と見せかけて期待した実行結果にならないやつ
trait T: std::any::Any { fn hello(&self); }
struct S { }
impl T for S{ fn hello(&self){ println!("It's S!") } }
impl S { fn hi(&self) { print!( "I can do HI!" ) } }

fn main()
{
 // OOPの派生型に相当する型の元のオブジェクト
 let s: S = S{};
 // OOPの基底型に相当するトレイトオブジェクトレベルでの保持
 let rs: &dyn std::any::Any = &s as &dyn std::any::Any;
 // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ
 match rs.downcast_ref::<&dyn T>()
 {
  // Any から T へのダウンキャスト、呼べる…ハズヨネ??
  Some(t) => t.hello(),
  None => println!("downcast to T was failed.")
 }
 // OOPで言うところの基底型から派生型へのダウンキャスト
 match rs.downcast_ref::<S>()
 {
  // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず
  Some(s) => s.hi(),
  None => println!("downcast to S was failed.")
 }
}
downcast to T was failed.
I can do HI!

Rustなのにいろいろすっとばして突然の動的型付け言語化です。お気持ちとしては Any までアップキャストしなくてもいいのよ… C++dynamic_cast 的な…と思いますが、それを trait で実装しているのが std::any::Any さんなのでにゃーんです。翻訳できて実行できて、だけど Any から T へのダウンキャストに失敗しています。悲しい。

Any から T.downcast_ref できないので、仕込みを増やしてどうにかします:

// Any 化するトレイトを追加
trait A { fn as_any(&self) -> &dyn std::any::Any; }
// T を A 派生にして
trait T: A { fn hello(&self); }
struct S { }
impl T for S{ fn hello(&self){ println!("It's S!") } }
impl S { fn hi(&self) { print!( "I can do HI!" ) } }
// ジェネリクスで T を実装する型 X で as_any を実装
impl<X:'static + T> A for X { fn as_any(&self) -> &dyn std::any::Any { self as &dyn std::any::Any } }

fn main()
{
 // OOPの派生型に相当する型の元のオブジェクト
 let s: S = S{};
 // OOPの基底型に相当するトレイトオブジェクトレベルでの保持
 let rt: &dyn T = &s as &dyn T;
 // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ
 rt.hello();
 // OOPで言うところの基底型から派生型へのダウンキャスト
 match rt.as_any().downcast_ref::<S>()
 {
  // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず
  Some(s) => s.hi(),
  None => println!("downcast to S was failed.")
 }
}
It's S!
I can do HI!

一応、出力としては理想を得られました。新たに trait A を用意するのは1回だけ、 S の仲間の T 派生型たちが増えてもしんどくなりません。 impl<X: 'static + T> A for XS に依存せず S やそのお仲間の T 派生型たちが増えてもこの1回の定義だけで対応できます。 'static は…つらいですね。

設計を enumunion へ変更できる場合はしちゃったらいい状況もありますが、それはそれ、適材適所でOOP継承風のパターンが楽な事はあります。と、いうわけで mopa crate の出番です🎉

// mopafy マクロ、かわいいですね!
use mopa::mopafy;
// B を mopa::Any 派生にして; 先の例までは T にしていましたが、 mopafy! マクロ内部で T だと事故が起こるので。通常の命名で問題になることはたぶんないでしょう。
trait B: mopa::Any { fn hello(&self); }
// おまじないを唱えて
mopafy!(B);
struct S { }
impl B for S{ fn hello(&self){ println!("It's S!") } }
impl S { fn hi(&self) { print!( "I can do HI!" ) } }

fn main()
{
 // OOPの派生型に相当する型の元のオブジェクト
 let s: S = S{};
 // OOPの基底型に相当するトレイトオブジェクトレベルでの保持
 let rt: &dyn B = &s as &dyn B;
 // トレイトオブジェクトレベルでの関数呼び出しと自動的な派生型の関数へのディスパッチ
 rt.hello();
 // OOPで言うところの基底型から派生型へのダウンキャスト
 match rt.downcast_ref::<S>()
 {
  // ダウンキャスト済みなのでダウンキャスト先の派生型にしかない関数も呼べるはず
  Some(s) => s.hi(),
  None => println!("downcast to S was failed.")
 }
}
It's S!
I can do HI!

😭mopa🙏 ありがとう。ナマスカール🙏

おまけ1: enum

設計を変えて trait 継承とアップキャストによるディスパッチを enum に変更する場合は、

trait T { fn hello(&self); }
struct S { }
impl T for S{ fn hello(&self){ println!("It's S!") } }
impl S { fn hi(&self) { print!( "I can do HI!" ) } }
enum E { S(S), /* 実際には T, U, V, ... 派生型をたくさん詰めます */ }

fn main()
{
 // OOPの派生型に相当する型の元のオブジェクト
 let s: S = S{};
 // OOPの基底型に相当するオブジェクトを enum へ変更
 let e: E = E::S(s);
 // OOPで言う基底型から派生型へのダウンキャストに相当する enum のディスパッチ
 match e
 {
  E::S(s) =>
  {
   // S は T を実装しているので共通関数も呼べる
   s.hello();
   // もちろん S の関数も呼べる
   s.hi();
  }
  /* 実際には T, U, V, ... 派生型ののディスパッチを書く */
 }
}

と、なります。一見これでいいじゃない的に見えるのはディスパッチ先が1種類しか無いお勉強向けの実装例だからです。enum を使うとディスパッチなしでは中身へアクセスできないので dyn TS など派生型の制約としては意味があるものの、共通関数を dyn T レベルから呼べるような機能は enum ではディスパッチ無しでは実現できません。つまり、ディスパッチ先が増えれば共通処理を呼ぶための退屈なディスパッチを大量に並べる事になります。うつくしくない…😭

おまけ2: union で無理やり reinterpret_cast しちゃったらどうなるの?

union だと Copy+Clone, unsafe が必要になるものの、型システムは巧い具合に(ある意味でバギーに)騙されたりするだろうか?なんて邪悪な事も試してみましたが、

// 基底型に相当させる enum を作り
union SuperTypeLikeUnion
{
 // ディスパッチ対象の派生型に相当する型をフィールドへいれる
 a: StructA,
 b: StructB,
}

trait HasHello { fn hello(&self); }
#[derive(Copy, Clone)] struct StructA {}
#[derive(Copy, Clone)] struct StructB {}
impl HasHello for StructA { fn hello(&self) { println!("It's A!!") } }
impl HasHello for StructB { fn hello(&self) { println!("It's B!!") } }

fn main()
{
 let a: StructA = StructA{};
 let e = SuperTypeLikeUnion{a};
 
 unsafe
 {
  // 実際のナカミは A, ぶっちぎりディスパッチも A
  let dangerous1 = &e.a as &dyn HasHello;
  dangerous1.hello();
 }

 let b: StructB = StructB{};
 let e = SuperTypeLikeUnion{b};
 
 unsafe
 {
  // 実際のナカミは B, ぶっちぎりディスパッチは A
  let dangerous2 = &e.a as &dyn HasHello;
  dangerous2.hello();
 }

}

この結果は

t's A!!
It's A!!

でした。型システムの実装を考えれば、まあ、そうですよね😅 C++ の RTTI による dynamic_cast や関数ポインターレベルの操作が何故かここで行われるはずもなく。

今回のメモのだいじなところ

  1. オブジェクト指向言語向けっぽい UML で設計したなコイツ…的なソフトウェアの設計を Rust で実装するはめになったら mopa crate を検討しましょう。
  2. Rust らしい enum ディスパッチは安全でうつくしーですが、OOPのクラスやインターフェースの継承構造を変換するには退屈なディスパッチを大量に書くことになるので適材適所で mopa りましょう。 Let's mopafy!

参考

Rust の tia crate で accessor とか interface 的な trait 群の impl を楽できるようになりました。のメモ

と、いうわけで tia crate を公開しました。

tia は何をしてくれる lib な crate ですか?

  • tia を使うと struct|enum|union のフィールドに Getter/Setter 的なアクセサーの impl を全自動化できます。

はじめて使用、あるいは tia の機能の概要を知りたい場合用の簡単なエグザンプルは以下のコードです:

use tia::Tia;  // 1. use

#[derive(Tia)] // 2. derive
#[tia(rg)]     // 3. tia directives
struct MyStruct
{
 foo: i32,
 bar: String
}

fn main()
{
 let mys = MyStruct{ foo: 123, bar: "Hello".into() };
 let foo = mys.get_foo(); // <-- 4. !! generated by tia automatically !!
 let bar = mys.get_bar(); // <-- 5. !! generated by tia automatically !!
 println!("foo={} bar={}", foo, bar );
}

このコードは MyStructimpl を手書きしていません。しかし、ビルドでき、実行できます。エラーも生じません。 .get_foo().get_bar() アクセサー群は tia が proc-macro により与えられたディレクティブに基づき全自動で生成してくれます。↑の例のようにもっとも単純に MyStruct へ Getter や Setter を実装する効果はやや弱いのですが、それでも tiarsi ディレクティブで Into パターンをフィールドへ自動実装したり、便利なこともあるかもしれません。

tia が作者に必要とされた理由は trait に対応したフィールドを自動生成できるようにしなければ、XMLSchemaにゲームを楽しみ料理を楽しむ時間を奪われそうになった事に起因します。起因の部分の細かい話はまた別の機会として、 trait に対応したアクセサーの自動生成は:

use tia::Tia;

trait FooGettable<T>{ fn get_foo(&self) -> T; }
trait Fruit{ fn get_bar(&self) -> &String; }
trait Sushi{ fn tuna(&self) -> u8; fn avocado(&mut self, v: u8); }

#[derive(Tia, Debug, Default)] // derive
struct MyStruct
{
 #[tia(s, "FooGettable<i32>", g)]
 foo: i32,
 #[tia("Fruit",rg,"",rsi)]
 bar: String,
 #[tia("Sushi",g*="tuna",s*="avocado")] // <- `g` and `s`: Sushi trait
 baz: u8
}

fn main()
{
 let mut mys = MyStruct::default();
 mys.set_foo(123);
 mys.set_bar("meow");
 let foo_gettable = &mys as &dyn FooGettable<i32>;
 let fruit = &mys as &dyn Fruit;
 println!("{}, {}", foo_gettable.get_foo(), fruit.get_bar() );
 let sushi = &mut mys as &mut dyn Sushi;
 sushi.avocado(32);
 println!("{}", sushi.tuna());
}

こんな具合です。たくさんの trait たちに手書きで対応しなければならないときには便利そうです。

より詳しくは README へ書いたので興味がわいたらどうぞ❤

にゃーん

tia を作るため proc-macro や [syn][] をトリアエズ扱える程度に修行できました。ソースを見るとバレそうですが、はじめはもうちょっと簡単にほいほい作れちゃうのでは…と思い大雑把でわりと簡単な設計に実験などしつつごちゃごちゃ実装になってしまいました。需要があればリファクタリングというか、一部再設計もして保守したいと思います。

ちなみに…Rustでは enum, union, Box, などで多相性(=ぽりもーふぃずむ)の要件をばばばっとやっつけてしまう設計の方が言語仕様的にも楽ですが、世の中のすべてが Rust を前提に設計されているわけではなく、オブジェクト指向パラダイムを主とした言語、例えば C#, Java, C++ など向けを当初想定して作られた、 UML で設計された、そんなような何かや複雑な XMLSchema を前提とした何かを大量に扱うための構造を実装したい場合、素直にオブジェクト指向パラダイム風に trait を interface 的に Rust の設計に落とし込み、 dyn した trait 型のオブジェクトを介してアクセサーで多相性をどうにかする、という事が必要な場合もあるかもしれません。

XMLSchema はそもそも人が手書きでパーサーやプログラミング言語の中での構造、あるいはUMLでいう汎化(≈継承)や包含の実装を全自動化できるハッピーな仕組み…なのですが、意外とまともな言語バインディングって少ないのです。UMLにせよおおよそ実質的にJava界隈を強く意識して作られているので仕方ないのだけど。Rust も XMLSchema の言語バインディングがまだまだ未成熟でごく簡単な単一ファイルの XMLSchema ならかろうじて対応できる crate もあるのですが…実質実用はまだまだ無理そうです。一般的に XMLSchema さんは Java のソースファイルのように大量の別れたファイル群で相互に複雑にリンクを持っていて、… まあ…そのはなしはまたこんど別の記事で…。

おまけメモ

  • tia に似た機能の crate には yangby-cryptape/rust-property というのがあります。trait に対応していないのと、たぶん方向性として主な用途、目標が違いそうなのでばばばっと…夜な夜な tia を作りました。
  • XMLSchema の lib crate の希望は media-io/xml-schema というのがあります。 flatten 未対応だったりまだまだ先は長いですが何年後かには実用性を獲得できるかもしれません。
    • bin な binding 生成器で estk/xmlschemer というのも見つけましたが、こちらはビルドから困難な状態で放棄されているので触るのはしんどそうです。