Delphi(10.4)でスマートポインタ
スマートポインタとカスタムマネージドレコード
Delphi10.4で実装されたカスタムマネージドレコードでスマートポインタ実装をやってみるという記事です。
公式でもカスタムマネージドレコードの利用例としてスマートポインタが例示されていた記憶があるんですが、なぜかスマートポインタ自体は実装されていないので早く実装してほしいところ。
※Delphi XE2のモバイル向けコンパイラにARCが実装されてからずっとWindows向けに来るのを待ってるのでほぼ10年・・・
動機
自アプリにwasmランタイム(wasmtime)を組み込みたくてwasm.h(C向けAPI)をDelphiに移植する作業をしているんだけどこのAPI、戻り値の扱いに二通りあって、
own wasm_functype_t* wasm_functype_new(
own wasm_valtype_vec_t* params, own wasm_valtype_vec_t* results);
const wasm_valtype_vec_t* wasm_functype_params(const wasm_functype_t*);
上のようにown
がついている戻り値は所有権がユーザーコードにあるので適切にdelete
を呼んで破棄する必要があるけど、下のようにown
無しの戻り値は所有権がないので破棄してはいけない。
このように扱いが複数あるものを適切に管理できる能力は我々人類には備わってない(やりたくない)のでown
付戻り値はスマートポインタで管理したい。
※C++向けAPIであるwasm.hhだとown
がstd::unique_ptr
で定義されてるので問題ない
色々なDelphi用スマートポインタ(的)実装
カスタムマネージドレコードを使ったスマートポインタといっても新しく追加されたFinalize()機構以外は、基本的には従来からある機能を使って実装することになります。
昔から実現可能だった順に書くと
- まずは基本的なInterfacedObjectを使った実装
type
IRc = interface
end;
TRc = class(TInterfacedObject, IRc)
end;
procedure Test();
begin
var intf : IRc := TRc.Create();
//intf経由での操作あれこれ
end; // スコープを抜ける時点でTRcのインスタンスが解放される
ざっくり言うと自動解放してもらいたいオブジェクト(TInterfacedObject
継承)をCreate
した後は必ずInterface
として扱うことで、Interface
の参照カウンタを使った自動解放の恩恵を得ることができる。
この方法を使って比較的大規模なアプリを組んでみたことがあるけど当時(20年くらい前)は[Weak]
もなく循環参照に気を使ったり、どこかでうっかりInterface
型じゃない変数に入れたのか謎のダングリングが発生したりと、我々人類が扱うには無理がある(面倒くさい)ものだった。
結局Interface
の自動解放を無効にして他言語のInterface
と同じ目的の使い方をするだけのほうが便利だという認識でこれまではやってきています。(このあたり、TOBYさんのブログがすごく参考になっていてよく見に行ってたんだけどリンクを貼ろうとしたら今は404になってしまっていて残念)
- 他にはスワンマンさんのスマートポインタ、使ってる?
これは無名メソッドがreference to function
というInterface
であることと、従来のrecord
型もInterface
等のマネージド型を含んでいる場合は見えないfinally
節で解放処理が走ることを利用したもので、今回やりたいことのほとんどがこれで良いのではという感じです。なのでかなりの部分を参考にさせていただいています。
スワンマンさんの記事は大変有用で他にも、これとかここもめちゃくちゃ参考になります。
- あとは今回のカスタムマネージドレコードを使った実装例としてpikさんの[Delphi] カスタム管理レコードを使ったスマートポインタ
もズバリそのものの解説になります。
生ポインタを扱うスマートポインタ
上に上げた例は基本的にDelphiのclassのインスタンスを扱うものなので、今回目的の他ライブラリからの戻り値として渡ってくる生ポインタを扱うためには手を加える必要があります。
投稿者は最近Rustにかぶれているので、リファレンスカウンタ型のスマートポインタをTRc<T>
、弱参照保持用としてTWeak<T>
、TRc<T>
からTWeak<T>
方向の変換をdowngrade
、逆をupgrade
、スマートポインタからデータを取り出す処理をUnwrap
と名付けることにします。
あとはC++のユニークポインタstd::unique_ptr
的なTUniq<T>
(uniqueは打ちにくいので略語化)の実装も用意しました。wasmのC++APIもown
をstd::unique_ptr
としているのでこちらが目的に近いのですが、後述の理由で使いにくいのでTRc<T>
を主に使うこととしました。
カスタムマネージドレコードを利用しますが、ベースとなるのはやはりInterface
です。
- まずは生ポインタと、そのポインタの解放処理用のメソッドをまとめて管理する
IRcContainer<T>
、TRcContainer<T>
を用意しました。こんな実装
type
IRcContainer<T> = Interface
function Unwrap : T;
End;
TRcDeleter = reference to procedure(Data : Pointer);
//TRcDeleter<T> = procedure(data : T)にすると内部エラーが出るので致し方なし
TRcContainer<T> = class(TInterfacedObject, IRcContainer<T>)
private
FData : T;
FDeleter : TRcDeleter;
public
constructor Create(Data : T; Deleter : TRcDeleter = nil);
destructor Destroy; override;
function Unwrap : T;
function Move : T; // 所有権を移動させる/移動したことを示す状態にする(FDeleterをnilにする。) ※TObjectに対しては無効
end;
implementation
{ TRcContainer<T> }
constructor TRcContainer<T>.Create(Data: T; Deleter: TRcDeleter);
begin
FData := data;
FDeleter := deleter;
end;
destructor TRcContainer<T>.Destroy;
var
p : TObject;
o : T absolute p;
begin
o := FData;
if GetTypeKind(T) = tkClass then
begin
p.Free;
end else begin
if Assigned(FDeleter) then FDeleter(Pointer(p));
end;
inherited;
end;
function TRcContainer<T>.Move : T;
begin
result := FData;
FDeleter := nil;
end;
function TRcContainer<T>.Unwrap: T;
begin
result := FData;
end;
implements
を見てもらうとわかりますが、スコープが外れて解放されるときに、classインスタンスの場合はFree
を、それ以外はdeleter
で指定された解放処理用のメソッドを呼び出します。
参照カウンタで管理されている実体はここなので、このInterface
とclass
だけでも注意力があればなんとかなります。後述のdowngrade
の際も型変換ではなく、このInterface
をやりとりして実現しています。
ですが、以下のカスタムマネージドレコードを使い、このInterface
はできるだけユーザー側からは見えないようにします。
TRc<T>
の実装
リファレンスカウンタ型のスマートポインタ TRcUnwrapProc<T> = reference to procedure(const data : T);
// 参照カウント型スマートポインタ
TRc<T> = record
private
FStrongRef : IRcContainer<T>;
public
class function Wrap(Data : T; Deleter : TRcDeleter = nil) : TRc<T>; static;
class operator Implicit(const Src : IRcContainer<T>) : TRc<T>; overload;
class operator Finalize(var Dest : TRc<T>);
class operator Positive(Src : TRc<T>) : T; // (+Src) = Unwrap
class operator Negative(Src : TRc<T>) : IRcContainer<T>; // (-Src) = downgrade
function Unwrap : T; overload;
procedure Unwrap(func : TRcUnwrapProc<T>); overload;
// procedure Drop; // Drop後にUnwrapされるのを防ぎようがないのでコメントアウトしておく。
// 意図したタイミングで解放したい場合はbegin/endでスコープを作る
end;
// TRc<>を少ない記述量で生成するための関数群
TRc = class
public
class function Wrap<T>(Data : T; Deleter : TRcDeleter = nil) : TRc<T>; static;
class function NewWrap<PT,T>(Deleter : TRcDeleter = nil) : TRc<PT>; static;
class function CreateWrap<T:class, constructor>() : TRc<T>; static;
end;
ERcDroppedException = class(Exception)
end;
ENewWrapException = class(Exception)
public
class constructor Create();
end;
implementation
{ TRc<T> }
// Drop後にUnwrapされるのを防ぎようがないのでコメントアウトしておく
{
procedure TRc<T>.Drop;
begin
FStrongRef := nil; // ref --
end;
}
class operator TRc<T>.Finalize(var Dest: TRc<T>);
begin
Dest.FStrongRef := nil; // ref --
end;
class operator TRc<T>.Implicit(const Src: IRcContainer<T>): TRc<T>;
begin
result.FStrongRef := Src;
end;
class operator TRc<T>.Negative(Src: TRc<T>): IRcContainer<T>;
begin
result := Src.FStrongRef;
end;
class operator TRc<T>.Positive(Src: TRc<T>): T;
begin
if Src.FStrongRef = nil then raise ERcDroppedException.Create('Unable to unwrap dropped references!');
result := Src.FStrongRef.Unwrap;
end;
function TRc<T>.Unwrap: T;
begin
if FStrongRef = nil then raise ERcDroppedException.Create('Unable to unwrap dropped references!');
result := FStrongRef.Unwrap;
end;
procedure TRc<T>.Unwrap(func: TRcUnwrapProc<T>);
begin
if FStrongRef = nil then raise ERcDroppedException.Create('Unable to unwrap dropped references!');
func(FStrongRef.Unwrap);
end;
class function TRc<T>.Wrap(Data: T; Deleter: TRcDeleter): TRc<T>;
begin
result.FStrongRef := TRcContainer<T>.Create(Data, Deleter); // ref ++
end;
{ TRc }
class function TRc.CreateWrap<T>(): TRc<T>;
begin
var obj := T.Create();
result := TRc<T>.Wrap(obj, nil);
end;
procedure DefaultRecordDisposer(p : Pointer);
begin
Dispose(p);
end;
class function TRc.NewWrap<PT,T>(Deleter: TRcDeleter): TRc<PT>;
type
PR = ^T;
var
p : PR;
d : PT absolute p;
begin
if GetTypeKind(PT) = tkPointer then
begin
System.New(p);
p^ := Default(T);
if Assigned(Deleter) then result := TRc<PT>.Wrap(d, Deleter)
else result := TRc<PT>.Wrap(d, DefaultRecordDisposer);
end else begin
raise ENewWrapException.Create('TRc.NewWrap<PT,T>() PT must be record pointer! ');
end;
end;
class function TRc.Wrap<T>(Data: T; Deleter: TRcDeleter): TRc<T>;
begin
result := TRc<T>.Wrap(data, Deleter);
end;
-
TRc<T>.Wrap()
スマートポインタの生成/初期化 -
TRc<T>.Implicit()
IRcContainer<T>
からTRc<T>
への暗黙的変換。IRcContainer<T>
を持つ他の型からTRc<T>
へ変換するために使われる -
TRc<T>.Finalize()
スコープが外れた際に呼ばれ、FStrongRef
の参照カウンタを--する -
TRc<T>.Positive()/Unwrap()
保持しているデータを取り出す。(+rc)
で取り出すかrc.Unwrap
で取り出すかの違い -
TRc<T>.Negative()
downgrade
するためにTWeak<T>
へ代入する際に(-rc)
と記述することでIRcContainer<T>
経由で代入する
class operator Negative(Src : TRc<T>) : TWeak<T>;
と書いて直接TWeak<T>
にできれば良いんだけどTWeak<T>
は後方にあるし、それならTWeak<T>
側に定義すればいいかと思いきや引数とTWeak<T>
の型を合わせろとエラーが出るので一旦Interface
をかませている。 docwiki
には
For unary operators, either the input parameter or the return value must be the record type.
と記述されているので本来ならできそうなもんなんだが。 ただし、別の理由もあってInterface
経由の方が都合が良いのでこうしている。
使い方
Wrap()
で生成(初期化)して、Unwrap
で取り出し。記述量低減のために単項+
演算子でも取り出し可能。後述のTWeak<T>
への代入のために単項-
演算子を使います。
生成は型推論の恩恵を受けやすくするためのユーティリティ関数のTRc.Wrap<>()
も使えます、というか使います。
TRc<T>
はrecord
なので代入や関数への引数渡しで値コピーされるときにIRcContainer<T>
の参照カウンタが増え、渡し先の変数のスコープが外れる時に参照カウンタが減ります。
こんな感じ。
// ローカルで使い捨て
begin
var rc := TRc.Wrap(TStringList.Create);
rc.Unwrap.Add('Unwrapしてstringlistを取り出してAdd()にアクセス');
(+rc).Add('+演算子でstringlistを取り出してAdd()にアクセス');
end; // ここでTStringListは解放される
// ローカルで生成、なにかしら処理してフィールド等で保持
begin
var rc := TRc.Wrap(TStringList.Create); // 参照カウンタ1
(+rc).Add('ちょっとした初期処理');
self.fieldStr := rc; // 参照カウンタ++
end; // 参照カウンタが--されるが1なのでTStringListは解放されない
// なにかしらで受け取った生ポインタを、破棄用のメソッドともに管理
var rc := TRc.Wrap(raw_pointer, disposer);
// 生ポインタを要求する関数などにはUnwrapして渡す
api_use_raw_pointer(rc.Unwrap);
api_use_raw_pointer((+rc));
// オブジェクトを生成してフィールド保持
instanceA.filedStr := TRc.Wrap(TStringList.Create); // instanceA解放時に参照カウンタが-される
// 循環参照によるリークを避けるために弱参照`TWeak<T>`のフィールドに代入
instanceB.weakStr := (-instanceA.fieldStr); // 括弧は不要だがわかりやすくするため
// あまり記述量は変わらないけどこういう書き方も
instanceA.filedStr := TRc.CreateWrap<TStringList>;
// recordをNew()で生成して、自動解放時にDispose()させる使い方
instanceA.filedRecP := TRc.NewWrap<PRecord, TRecod>;
// ^^^^^^^ ^^^^^^
// | +---- 生成するrecordの型
// +---- 管理するポインタの型
// recordをNew()で生成して、自動解放時に指定の破棄メソッドを呼ぶ使い方
instanceA.filedRecP := TRc.NewWrap<PRecord, TRecod>(recordDisposer);
TWeak<T>
の実装
弱参照保持用の // TWeak<T>からTRc<T>に昇格させる場合、既に解放済のケースがあるので解放済(IsNone)を表現可能なTOptionRc<T>に昇格させる
TOptionRc<T> = record
private
FStrongRef : IRcContainer<T>;
public
class function Wrap(Data : T; Deleter : TRcDeleter = nil) : TOptionRc<T>; static;
class operator Finalize(var Dest : TOptionRc<T>);
class operator Negative(Src : TOptionRc<T>) : IRcContainer<T>;
function IsNone() : Boolean;
function TryUnwrap(Func : TRcUnwrapProc<T>) : Boolean; overload;
function TryUnwrap(out Data : T) : Boolean; overload;
function TryUnwrap(out Data : TRc<T>) : Boolean; overload;
procedure Drop;
end;
TRcUpgradeProc<T> = reference to procedure(const rc : TRc<T>);
// TRc<>で管理しているオブジェクトへの弱い参照を管理する
// 保持している参照は既に存在しない場合があるため
// オブジェクトにアクセスするためにはTryUpgadeでTRc<>に昇格もしくは
// Upgrade()/+演算子にてTOptionRc<>に昇格させる必要がある。
TWeak<T> = record
private
[Weak] FWeakRef : IRcContainer<T>;
public
class operator Implicit(const Src : TRc<T>) : TWeak<T>; overload;
class operator Implicit(const Src : IRcContainer<T>) : TWeak<T>; overload;
class operator Positive(Src : TWeak<T>) : TOptionRc<T>;
function TryUpgrade(var Dest : TRc<T>) : Boolean; overload;
function TryUpgrade(func : TRcUpgradeProc<T>) : Boolean; overload;
function Upgrade() : TOptionRc<T>;
procedure Drop;
end;
implementation
{ TOptionRc<T> }
procedure TOptionRc<T>.Drop;
begin
FStrongRef := nil; // ref --
end;
class operator TOptionRc<T>.Finalize(var Dest: TOptionRc<T>);
begin
Dest.FStrongRef := nil; // ref --
end;
function TOptionRc<T>.IsNone: Boolean;
begin
result := FStrongRef = nil;
end;
class operator TOptionRc<T>.Negative(Src: TOptionRc<T>): IRcContainer<T>;
begin
result := Src.FStrongRef;
end;
function TOptionRc<T>.TryUnwrap(out Data: TRc<T>): Boolean;
begin
result := FStrongRef <> nil;
if result then data.FStrongRef := FStrongRef;
end;
function TOptionRc<T>.TryUnwrap(Func: TRcUnwrapProc<T>): Boolean;
begin
result := FStrongRef <> nil;
if result then func(FStrongRef.Unwrap);
end;
function TOptionRc<T>.TryUnwrap(out Data: T): Boolean;
begin
result := FStrongRef <> nil;
if result then data := FStrongRef.Unwrap;
end;
class function TOptionRc<T>.Wrap(Data: T; Deleter: TRcDeleter): TOptionRc<T>;
begin
result.FStrongRef := TRcContainer<T>.Create(Data, Deleter); // ref ++
end;
{ TWeak<T> }
procedure TWeak<T>.Drop;
begin
FWeakRef := nil;
end;
class operator TWeak<T>.Implicit(const Src: TRc<T>): TWeak<T>;
begin
result.FWeakRef := Src.FStrongRef; // ref +-0
end;
class operator TWeak<T>.Implicit(const Src: IRcContainer<T>): TWeak<T>;
begin
result.FWeakRef := Src; // ref +-0
end;
class operator TWeak<T>.Positive(Src: TWeak<T>): TOptionRc<T>;
begin
result.FStrongRef := Src.FWeakRef;
end;
function TWeak<T>.TryUpgrade(var Dest: TRc<T>): Boolean;
begin
Dest.FStrongRef := FWeakRef; // ref++
result := Dest.FStrongRef <> nil;
end;
function TWeak<T>.TryUpgrade(func: TRcUpgradeProc<T>): Boolean;
begin
var rc : TRc<T>;
if TryUpgrade(rc) then
begin
func(rc);
result := true;
end else begin
result := false;
end;
end;
function TWeak<T>.Upgrade: TOptionRc<T>;
begin
result.FStrongRef := FWeakRef;
end;
TWeak<T>
の実装といいながらいきなりTOptionRc<T>
の実装から始まっているが、これはTweak<T>
をupgrade
したときにすでに保持しているデータが解放されている=参照先が無い状態になっている可能性があるため。TRc<T>
には参照先が無い状態を持たせたくない。他の言語であればNull許容型
やRustのOption<T>
を使えるがDelphiには相当するものが無いし実装が難しい(他の目的でもOption<T>
の実装をしたいのでDelphiジェネリックで簡単に特殊化できるようなってほしい。ところでNull許容型
ってロードマップに無かったっけ?)。
よって、参照が無い状態を持つTOptionRc<T>
を用意した。TOptionRc<T>
にはUnwrap
がなく、参照が有効な場合のみデータを取り出すTryUnwrap
があるので面倒だが安全に使える。
-
TWeak<T>.Implicit()
TRc<T>
からTWeak<T>
への暗黙的変換。 -
TWeak<T>.Implicit()
IRcContainer<T>
からの暗黙的変換 -
TWeak<T>.Positive()/Upgrade()
TOptionRc<T>
へupgrade
する -
TWeak<T>.TryUpgrade()
upgrade
できる場合、TRc<T>
を引数経由で戻す。もしくは指定メソッドを実行する。 -
TOptionRc<T>
はTRc<T>
からUnwrap()/Positive()
を取り除いてIsNone()
とDrop()
、TryUnwrap()
を追加したもの。 -
TOptionRc<T>.IsNone()
保持している参照が有効か?(データを取り出せるか?)を判定する -
TOptionRc<T>.TryUnwrap()
データを取り出せる場合のみ、引数経由で取り出す。もしくは指定メソッドを実行する。 -
TOptionRc<T>.Drop()
参照を解放し、参照カウンタを--する。データが解放されるかどうかはその時の参照カウンタ次第。
TUniq<>
の実装
所有権のオーナーが一つであるようにふるまう実装してみたものの使いにくい
TUniq<T> = record
private
FStrongRef : IRcContainer<T>;
public
class function Wrap(Data : T; Deleter : TRcDeleter = nil) : TUniq<T>; static;
class operator Finalize(var Dest : TUniq<T>);
class operator Assign(var Dest : TUniq<T>; var Src : TUniq<T>);
function IsNone() : Boolean;
function TryUnwrap(var Data: T): Boolean; overload;
function TryUnwrap(Func: TRcUnwrapProc<T>): Boolean; overload;
function MoveTo : IRcContainer<T>; overload;
procedure Drop; // TRc<>とは違い、TryUnwarpでしか取り出せないのでDropも有効にした。
end;
TUniq = class
public
class function Wrap<T>(Data : T; Deleter : TRcDeleter = nil) : TUniq<T>; static;
end;
implementation
{ TUniq<T> }
class operator TUniq<T>.Assign(var Dest: TUniq<T>; var Src: TUniq<T>);
begin
Dest.FStrongRef := Src.FStrongRef;
Src.FStrongRef := nil;
end;
procedure TUniq<T>.Drop;
begin
FStrongRef := nil; // ref --
end;
class operator TUniq<T>.Finalize(var Dest: TUniq<T>);
begin
Dest.FStrongRef := nil; // ref --
end;
function TUniq<T>.IsNone: Boolean;
begin
result := FStrongRef = nil;
end;
function TUniq<T>.MoveTo: IRcContainer<T>;
begin
result := FStrongRef; // ref++
FStrongRef := nil; // ref --
end;
function TUniq<T>.TryUnwrap(var Data: T): Boolean;
begin
result := FStrongRef <> nil;
if result then Data := FStrongRef.Unwrap;
end;
function TUniq<T>.TryUnwrap(Func: TRcUnwrapProc<T>): Boolean;
begin
result := FStrongRef <> nil;
if result then func(FStrongRef.Unwrap);
end;
class function TUniq<T>.Wrap(Data: T; Deleter: TRcDeleter): TUniq<T>;
begin
result.FStrongRef := TRcContainer<T>.Create(Data, Deleter); // ref ++
end;
{ TUniq }
class function TUniq.Wrap<T>(Data: T; Deleter: TRcDeleter): TUniq<T>;
begin
result := TUniq<T>.Wrap(Data, Deleter);
end;
-
C++
のstd::unique_ptr
のように代入を禁止してmove
しかできないようにしたかったが、難しかったため代入(Assign
)したら必ず移動になるようにした。 - そのため、参照が無くなっている可能性があるので
TryUnwrap
でしか取り出せない仕様としたので使いにくい。Rustのように移動された可能性がある箇所で使用したらコンパイルエラーにできるようになってほしい。(例えば、移動が発生するメソッドに何らかのAttributeを指定しておくなど。今でもフロー解析して未初期化変数の警告を出す処理があるのだからできなくはないと思われる) -
MoveTo
はTRc<T>
に移動するためのメソッド。
力尽きたので使い方は省略。
ユニットソース (Ownership.pas)
gistを開く
オマケで作りかけのTArcMutex<T>
も入ってますが碌に動かしていません。
TArcMutex<T>
の使い方
var mutex := TArcMutex.Wrap(TStringList.Create);
として
それぞれのThreadにコピーした上記mutexを使って
begin
var lock := mutex.LockW; // 書き込み用ロックを取得
lock.Unwap.Add('write');
end; // ここでロック解除
begin
var lock := mutex.LockR; // 読み出し用ロックを取得
var cnt := lock.Unwrap.Count;
end; // ここでロック解除
// NG例1
var lockobj := mutex.LockW.Unwrap; // ロックとUnwrapを一度にやれると楽!
lockobj.Add('lock and unwrap'); // NG:ここに至るまでにロックが解除されてしまう
// NG例2
var lock := mutex.LockR; // 読み出し用ロックを取得
・・・長い処理
// 意識的にbegin/endで短いスコープを作ってロックしないとロックされっぱなしになる
オチ
とここまで作ったものの、もともとの目的であるwasm.h
の移植にはTRc<T>
をそのまま使うことができませんでした。最初の例を移植するときに以下のようにしたかったんですが
type
PWasmFuncType = ^TWasmFuncType; // PWasmFuncTypeをAPIでやり取りする
TWasmFuncType = record // TWasmFuncTypeにはAPIのラッパー関数を定義する
//own wasm_functype_t* wasm_functype_new(own wasm_valtype_vec_t* params, own wasm_valtype_vec_t* results);
class function New(params : PWasmValtypeVec; results : PWasmValtypeVec) : TRc<PWasmFuncType>;
//const wasm_valtype_vec_t* wasm_functype_params(const wasm_functype_t*);
function Params() : PWasmFuncType;
end;
TWasmFuncType
に定義した関数の戻り値をTRc<PWasmFuncType>
にすると、変換したヘッダー(wasm.pas
)のコンパイルは通りますが、他のユニットから以下のように型推論しようとすると内部エラーが発生してしまいます
var rc := TWasmFuncType.New(params, results); // NG:内部エラー
var rc : TRc<PWasmFuncType> := TWasmFuncType.New(params, results); // OK
仕方ないのでwasm.pas
ユニットの中でTRcFuncType
という名前のrecord
を作り、実装はTRc<PWasmFuncType>
をそのまま書くという残念な方法に落ち着いています。
TWeak<T>
とTRc<T>
の変換やTRc<T>
同士の変換にIRcContainer<T>
を経由しているのはこのためです。
Discussion