27 3月 2025
当初から、このタスクには数百万行に及ぶコードを含む複数のプロジェクトの移植が含まれていました。基本的に、トランスレーターの技術仕様は「これらすべてがCで正しく移植され、実行されることを保証する」という一文に集約されました。C製品のリリースを担当する人々の仕事には、コードの変換、テストの実行、リリースパッケージの準備などが含まれます。通常発生する問題は、いくつかのカテゴリのいずれかに分類されます。
このリストの進行は上から下へです。たとえば、変換されたコードのコンパイル問題を解決しない限り、その機能とパフォーマンスを検証することは不可能です。その結果、多くの長年の問題は、CodePorting.Translator Cs2Cppプロジェクトの作業の後期段階で初めて発見されました。
当初、オブジェクト間の循環依存によって引き起こされる単純なメモリリークを修正する際、フィールドに CppWeakPtr
属性を適用し、その結果フィールドは WeakPtr
型になりました。 WeakPtr
が lock()
メソッドを呼び出すか、暗黙的に(構文的にはより便利) SharedPtr
に変換できる限り、これは問題を引き起こしませんでした。しかし、後には CppWeakPtr
属性に特別な構文を使用して、コンテナ内に含まれる参照も弱参照にする必要があり、これがいくつかの厄介な驚きをもたらしました。
採用したアプローチにおける最初の問題の兆候は、C++の観点から見ると MyContainer<SharedPtr<MyClass>>
と MyContainer<WeakPtr<MyClass>>
が2つの異なる型であることでした。その結果、これらは同じ変数に格納したり、同じメソッドに渡したり、そこから返したりすることができません。元々はオブジェクトフィールドでの参照の格納方法を管理するためだけ意図されていた属性が、ますます奇妙なコンテキストで現れ始め、戻り値、引数、ローカル変数などに影響を与えるようになりました。それを処理するトランスレーターのコードは日に日に複雑になっていきました。
2番目の問題も、私たちが予期していなかったものでした。C#プログラマにとっては、オブジェクトごとに単一の連想コレクションを持ち、そこには現在のオブジェクトが所有し、他からはアクセスできないオブジェクトへの一意の参照と、親オブジェクトへの参照の両方を含むことが自然であることが判明しました。これは特定のファイル形式からの読み取り操作を最適化するために行われましたが、私たちにとっては、同じコレクションが強参照と弱参照の両方を含む可能性があることを意味しました。ポインタの型がその動作モードの最終的な決定要因ではなくなったのです。
明らかに、これら2つの問題は既存のパラダイム内では解決できず、ポインタ型が再考されました。この改訂されたアプローチの結果が SmartPtr
クラスであり、SmartPtrMode::Shared
と SmartPtrMode::Weak
の2つの値のいずれかを受け入れる set_Mode()
メソッドを備えています。すべての SmartPtr
コンストラクターもこれらの同じ値を受け入れます。結果として、各ポインタインスタンスは次の2つの状態のいずれかで存在できます。
モード間の切り替えは、ランタイムでいつでも発生する可能性があります。弱参照カウンターは、オブジェクトへの弱参照が少なくとも1つ存在するようになるまで作成されません。
私たちのポインタがサポートする機能の完全なリストは次のとおりです。
intrusive_ptr
セマンティクス: 同じオブジェクトに対して作成された任意の数のポインタが単一の参照カウンターを共有します。->
): 指し示されたオブジェクトへのアクセス用。SmartPtr
クラスはテンプレート化されており、仮想メソッドを含みません。これは参照カウンターのストレージを処理する System::Object
クラスと密接に結合しており、その派生クラスとのみ動作します。
典型的なポインタの動作からの逸脱があります:
古いコードとの互換性を維持するために、SharedPtr
型は SmartPtr
のエイリアスになりました。WeakPtr
クラスは現在 SmartPtr
から継承し、フィールドを追加せず、常に弱参照を作成するようにコンストラクターをオーバーライドするだけです。
コンテナは常に MyContainer<SmartPtr<MyClass>>
セマンティクスで移植され、格納される参照の型はランタイムで選択されます。STLデータ構造に基づいて手動で記述されたコンテナ(主に System
名前空間のコンテナ)の場合、デフォルトの参照型はカスタムアロケーターを使用して設定されますが、個々のコンテナ要素に対してモードを変更することも可能です。変換されたコンテナの場合、参照ストレージモードを切り替えるために必要なコードがトランスレーターによって生成されます。
このソリューションの欠点は、主にポインタの作成、コピー、削除操作中のパフォーマンス低下です。通常の参照カウントに加えて、参照型の必須チェックが追加されるためです。具体的な数値はテスト構造に大きく依存します。現在、ポインタ型が変更されないことが保証されている場所で、より最適なコードを生成することについて議論が進行中です。
私たちの移植方法では、参照を弱参照にすべき箇所をマークするために、ソースC#コードに手動で属性を配置する必要があります。これらの属性が正しく配置されていないコードは、変換後にメモリリークや、場合によっては他のエラーを引き起こします。属性を持つコードは次のようになります。
struct S {
MyClass s; // オブジェクトへの強参照
[CppWeakPtr]
MyClass w; // オブジェクトへの弱参照
MyContainer<MyClass> s_s; // 強参照のコンテナへの強参照
[CppWeakPtr]
MyContainer<MyClass> w_s; // 強参照のコンテナへの弱参照
[CppWeakPtr(0)]
MyContainer<MyClass> s_w; // 弱参照のコンテナへの強参照
[CppWeakPtr(1)]
Dictionary<MyClass, MyClass> s_s_w; // キーが強参照で、値が弱参照で格納されるコンテナへの強参照
[CppWeakPtr, CppWeakPtr(0)]
Dictionary<MyClass, MyClass> w_w_s; // キーが弱参照で、値が強参照で格納されるコンテナへの弱参照
}
場合によっては、SmartPtr
クラスのエイリアスコンストラクターまたは格納される参照型を設定するメソッドを手動で呼び出す必要があります。移植されたコードを編集することは避けるようにしています。なぜなら、そのような変更は各トランスレーターの実行後に再適用する必要があるからです。代わりに、そのようなコードをC#ソース内に保持することを目指しています。これには2つの方法があります。
class Service {
public static void SetWeak<T>(T arg) {}
}
class Service {
public:
template <typename T> static void SetWeak(SmartPtr<T> &arg)
{
arg.set_Mode(SmartPtrMode::Weak);
}
};
class MyClass {
private Dictionary<string, object> data;
public void Add(string key, object value)
{
data.Add(key, value);
//CPPCODE: if (key == u"Parent") data->data()[key].set_Mode(SmartPtrMode::Weak);
}
}
ここで、System::Collections::Generic::Dictionary
の data()
メソッドは、このコンテナの基になる std::unordered_map
への参照を返します。