11 3月 2024

C#からC++へのコード変換ルール:基本編

私たちのトランスレーターがC#言語の構文構造をC++に変換する方法について話し合いましょう。翻訳の詳細と、このプロセス中に発生する制限について説明します。

プロジェクトとコンパイル単位

翻訳はプロジェクトごとに行われます。1つのC#プロジェクトは、1つまたは2つのC++プロジェクトに変換されます。最初のプロジェクトはC#プロジェクトのミラーであり、2番目のプロジェクトは、元のプロジェクトにテストが存在する場合にテストを実行するgoogletestアプリケーションとして機能します。各入力プロジェクトにはCMakeLists.txtファイルが生成され、ほとんどのビルドシステムのプロジェクトを作成できます。

通常、1つの.csファイルは1つの.hファイルと1つの.cppファイルに対応します。通常、型定義はヘッダーファイルに入り、メソッド定義はソースコードファイルに置かれます。ただし、テンプレート型の場合はすべてのコードがヘッダーファイルに残ります。少なくとも1つの公開定義を含むヘッダーファイルはincludeディレクトリに入り、依存するプロジェクトとエンドユーザーがアクセスできます。内部定義のみを含むヘッダーファイルは、sourceディレクトリに入ります。

元のC#コードの翻訳から得られたコードファイルに加えて、翻訳者はサービスコードを含む追加のファイルを生成します。このプロジェクトの型をヘッダーファイルでどこで見つけるかを指定するエントリを含む設定ファイルも出力ディレクトリに配置されます。この情報は、依存するアセンブリを処理するために必要です。さらに、包括的な翻訳ログが出力ディレクトリに保存されます。

ソースコードの一般的な構造

  1. C#の名前空間C+の名前空間にマッピングされます。名前空間使用演算子はC+の同等物に変換されます。
  2. コメントはそのまま転送されますが、型とメソッドのドキュメントは別途処理されます。
  3. フォーマットは部分的に保持されます。
  4. プリプロセッサディレクティブは転送されません。すべての定数は構文木の構築中に定義される必要があります。
  5. 各ファイルは、含まれるファイルのリストで始まり、次に型の前方宣言のリストが続きます。これらのリストは、現在のファイルに記載されている型に基づいて生成されるため、含まれるリストができるだけ最小限になります。
  6. 型メタデータは、実行時にアクセス可能な特別なデータ構造として生成されます。無条件のメタデータ生成はコンパイルされたライブラリのサイズを大幅に増加させるため、必要に応じて特定の型に対して手動で有効にされます。

タイプ定義

  1. 型のエイリアスusing <typename> = ... という構文を使って翻訳されます。
  2. C#の列挙C++14の列挙にマップされます(enum class構文を使用)。
  3. デリゲートSystem::MulticastDelegate クラスの特殊化のエイリアスに変換される:
public delegate int IntIntDlg(int n);
using IntIntDlg = System::MulticastDelegate<int32_t(int32_t)>;
  1. C#のクラスと構造体C++のクラスとして表現されます。インターフェースは抽象クラスになる。継承構造は C# と同じで、System.Object からの暗黙の継承は明示的になる。
  2. プロパティとインデクサーは、ゲッターとセッターの別々のメソッドに分割されます。
  3. C#の仮想関数は、C++の仮想関数に対応する。インターフェースの実装も、仮想関数のメカニズムを使って実現されます。
  4. 汎用型とメソッドは、C++のテンプレートに変換されます。
  5. ファイナライザーデストラクターに変換されます。

制限

これらすべての要因が相まって、いくつかの制限が課される:

  1. 仮想ジェネリックメソッドの翻訳はサポートされていません
  2. インターフェイスメソッドの実装は、元のC#コードになかったとしても、仮想的です。
  3. 既存の仮想メソッドやインターフェイスメソッドと同じ名前とシグネチャを持つ新しいメソッドを導入することはできません。しかし、トランスレータはそのようなメソッドの名前を変更することができます。
  4. 基底クラスのメソッドが派生クラスでインターフェースの実装に使用される場合、 C#にはなかった定義が派生クラスに追加されます。
  5. 構築時と確定時に仮想メソッドを呼び出すと、翻訳後の動作が異なるため、 避ける必要があります。

C#の動作を厳密に模倣するには、多少異なるアプローチが必要であることは理解しています。とはいえ、変換後のライブラリーのAPIをC++のパラダイムに近づけるため、このロジックを選択しました。以下の例は、これらの特徴を示している:

C#コード:

using System;

public class Base
{
    public virtual void Foo1()
    { }
    public void Bar()
    { }
}
public interface IFoo
{
    void Foo1();
    void Foo2();
    void Foo3();
}
public interface IBar
{
    void Bar();
}
public class Child : Base, IFoo, IBar
{
    public void Foo2()
    { }
    public virtual void Foo3()
    { }
    public T Bazz<T>(object o) where T : class
    {
        if (o is T)
            return (T)o;
        else
            return default(T);
    }
}

C++ヘッダーファイル:

#pragma once

#include <system/object_ext.h>
#include <system/exceptions.h>
#include <system/default.h>
#include <system/constraints.h>

class Base : public virtual System::Object
{
    typedef Base ThisType;
    typedef System::Object BaseType;
    
    typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;
    RTTI_INFO_DECL();
    
public:

    virtual void Foo1();
    void Bar();
};

class IFoo : public virtual System::Object
{
    typedef IFoo ThisType;
    typedef System::Object BaseType;
    
    typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;
    RTTI_INFO_DECL();
    
public:

    virtual void Foo1() = 0;
    virtual void Foo2() = 0;
    virtual void Foo3() = 0;
};

class IBar : public virtual System::Object
{
    typedef IBar ThisType;
    typedef System::Object BaseType;
    
    typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;
    RTTI_INFO_DECL();
    
public:

    virtual void Bar() = 0;
};

class Child : public Base, public IFoo, public IBar
{
    typedef Child ThisType;
    typedef Base BaseType;
    typedef IFoo BaseType1;
    typedef IBar BaseType2;
    
    typedef ::System::BaseTypesInfo<BaseType, BaseType1, BaseType2> ThisTypeBaseTypesInfo;
    RTTI_INFO_DECL();
    
public:

    void Foo1() override;
    void Bar() override;
    void Foo2() override;
    void Foo3() override;
    template <typename T>
    T Bazz(System::SharedPtr<System::Object> o)
    {
        assert_is_cs_class(T);
        
        if (System::ObjectExt::Is<T>(o))
        {
            return System::StaticCast<typename T::Pointee_>(o);
        }
        else
        {
            return System::Default<T>();
        }
    }
};

C++ソースコード:

#include "Class1.h"
RTTI_INFO_IMPL_HASH(788057553u, ::Base, ThisTypeBaseTypesInfo);
void Base::Foo1()
{
}
void Base::Bar()
{
}
RTTI_INFO_IMPL_HASH(1733877629u, ::IFoo, ThisTypeBaseTypesInfo);
RTTI_INFO_IMPL_HASH(1699913226u, ::IBar, ThisTypeBaseTypesInfo);
RTTI_INFO_IMPL_HASH(3787596220u, ::Child, ThisTypeBaseTypesInfo);
void Child::Foo1()
{
    Base::Foo1();
}
void Child::Bar()
{
    Base::Bar();
}
void Child::Foo2()
{
}
void Child::Foo3()
{
}

各変換されたクラスの始めにある一連のエイリアスとマクロは、主にGetTypetypeofisなどのC#のメカニズムをエミュレートするために使用されます。効率的な型比較のために、.cppファイルからのハッシュコードが使用されます。インターフェースを実装するすべての関数は仮想ですが、これはC#の動作とは異なります。

関連ニュース

関連動画

関連記事