27 3月 2025

SmartPtrの実装: C#コードをC++に移植する方法

当初から、このタスクには数百万行に及ぶコードを含む複数のプロジェクトの移植が含まれていました。基本的に、トランスレーターの技術仕様は「これらすべてがCで正しく移植され、実行されることを保証する」という一文に集約されました。C製品のリリースを担当する人々の仕事には、コードの変換、テストの実行、リリースパッケージの準備などが含まれます。通常発生する問題は、いくつかのカテゴリのいずれかに分類されます。

  1. コードがC++に変換されない - トランスレーターがエラーで終了する。
  2. コードはC++に変換されるが、コンパイルできない。
  3. コードはコンパイルされるが、リンクできない。
  4. コードはリンクして実行されるが、テストが失敗するか、ランタイムクラッシュが発生する。
  5. テストは成功するが、製品の機能に直接関係しない問題が実行中に発生する。例:メモリリーク、パフォーマンスの低下など。

このリストの進行は上から下へです。たとえば、変換されたコードのコンパイル問題を解決しない限り、その機能とパフォーマンスを検証することは不可能です。その結果、多くの長年の問題は、CodePorting.Translator Cs2Cppプロジェクトの作業の後期段階で初めて発見されました。

当初、オブジェクト間の循環依存によって引き起こされる単純なメモリリークを修正する際、フィールドに CppWeakPtr 属性を適用し、その結果フィールドは WeakPtr 型になりました。 WeakPtrlock() メソッドを呼び出すか、暗黙的に(構文的にはより便利) SharedPtr に変換できる限り、これは問題を引き起こしませんでした。しかし、後には CppWeakPtr 属性に特別な構文を使用して、コンテナ内に含まれる参照も弱参照にする必要があり、これがいくつかの厄介な驚きをもたらしました。

採用したアプローチにおける最初の問題の兆候は、C++の観点から見ると MyContainer<SharedPtr<MyClass>>MyContainer<WeakPtr<MyClass>> が2つの異なる型であることでした。その結果、これらは同じ変数に格納したり、同じメソッドに渡したり、そこから返したりすることができません。元々はオブジェクトフィールドでの参照の格納方法を管理するためだけ意図されていた属性が、ますます奇妙なコンテキストで現れ始め、戻り値、引数、ローカル変数などに影響を与えるようになりました。それを処理するトランスレーターのコードは日に日に複雑になっていきました。

2番目の問題も、私たちが予期していなかったものでした。C#プログラマにとっては、オブジェクトごとに単一の連想コレクションを持ち、そこには現在のオブジェクトが所有し、他からはアクセスできないオブジェクトへの一意の参照と、親オブジェクトへの参照の両方を含むことが自然であることが判明しました。これは特定のファイル形式からの読み取り操作を最適化するために行われましたが、私たちにとっては、同じコレクションが強参照と弱参照の両方を含む可能性があることを意味しました。ポインタの型がその動作モードの最終的な決定要因ではなくなったのです。

ポインタ状態の一部としての参照型

明らかに、これら2つの問題は既存のパラダイム内では解決できず、ポインタ型が再考されました。この改訂されたアプローチの結果が SmartPtr クラスであり、SmartPtrMode::SharedSmartPtrMode::Weak の2つの値のいずれかを受け入れる set_Mode() メソッドを備えています。すべての SmartPtr コンストラクターもこれらの同じ値を受け入れます。結果として、各ポインタインスタンスは次の2つの状態のいずれかで存在できます。

  1. 強参照: 参照カウンターはオブジェクト内にカプセル化されます。
  2. 弱参照: 参照カウンターはオブジェクトの外部にあります。

モード間の切り替えは、ランタイムでいつでも発生する可能性があります。弱参照カウンターは、オブジェクトへの弱参照が少なくとも1つ存在するようになるまで作成されません。

私たちのポインタがサポートする機能の完全なリストは次のとおりです。

  1. 強参照ストレージ: 参照カウントによるオブジェクトのライフタイム管理。
  2. オブジェクトの弱参照ストレージ
  3. intrusive_ptr セマンティクス: 同じオブジェクトに対して作成された任意の数のポインタが単一の参照カウンターを共有します。
  4. デリファレンスとアロー演算子 (->): 指し示されたオブジェクトへのアクセス用。
  5. コンストラクターと代入演算子の完全なセット
  6. 指し示されるオブジェクトと参照カウントされるオブジェクトの分離(エイリアスコンストラクター): クライアントのライブラリはドキュメントを扱うため、ドキュメント要素へのポインタがドキュメント全体を生かし続ける必要があることがよくあります。
  7. キャストの完全なセット
  8. 比較演算の完全なセット
  9. ポインタの代入と削除: 不完全型に対して操作します。
  10. ポインタの状態を確認および変更するための一連のメソッド: エイリアスモード、参照ストレージモード、オブジェクト参照カウントなど。

SmartPtr クラスはテンプレート化されており、仮想メソッドを含みません。これは参照カウンターのストレージを処理する System::Object クラスと密接に結合しており、その派生クラスとのみ動作します。

典型的なポインタの動作からの逸脱があります:

  1. ムーブ(ムーブコンストラクター、ムーブ代入演算子)は状態全体を変更せず、参照型(弱/強)を保持します。
  2. 弱参照を介したオブジェクトへのアクセスにはロック(一時的な強参照の作成)は必要ありません。なぜなら、アロー演算子が一時オブジェクトを返すアプローチは、強参照のパフォーマンスを著しく低下させるためです。

古いコードとの互換性を維持するために、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つの方法があります。

  1. C#コードで何もしないサービスメソッドを宣言し、変換中にそれを必要な操作を実行する手動で記述された同等のものに置き換えることができます。
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);
    }
};
  1. C#コードに特別にフォーマットされたコメントを配置し、トランスレーターがそれをC++コードに変換するようにします。
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::Dictionarydata() メソッドは、このコンテナの基になる std::unordered_map への参照を返します。

関連ニュース

関連動画

関連記事