22 11月 2024
C# や C++ のような言語間で効果的なコードトランスレータを作成することは複雑な作業です。CodePorting.Translator Cs2Cpp ツールの開発は、これらの2つの言語の構文、セマンティクス、およびプログラミングパラダイムの違いに起因する多くの問題に直面しました。この記事では、私たちが直面した主な困難とそれらを克服するための可能な方法について説明します。
これは、例えば、using
と yield
オペレーターに関するものです:
using (var resource = new Resource())
{
// リソースの使用
}
public IEnumerable<int> GetAllNumbers()
{
for (int i = 0; i < int.MaxValue; i++)
{
yield return i;
}
}
このような場合、ソースコードの動作をエミュレートするために、トランスレータとライブラリの両方でかなり複雑なコードを書かなければならない場合があります(最初のケース)、またはそのような構文のサポートを放棄する(2 番目のケース)という選択をしなければなりません。
例えば、ソースコードには、仮想ジェネリックメソッドや仮想関数を使用するコンストラクタが含まれています:
public class A
{
public virtual T GenericMethod<T>(T param)
{
return param;
}
}
public class A
{
public A()
{
VirtualMethod();
}
public virtual void VirtualMethod()
{
}
}
public class B : A
{
public override void VirtualMethod()
{
}
}
このような場合、問題のあるコードを C# に変換できる形に書き直す以外に選択肢はありません。幸い、このようなケースはまれで、小さなコードフラグメントに関するものがほとんどです。
これには、リソース、リフレクション、動的アセンブリのリンク、および関数のインポートが含まれます:
static void Main()
{
var rm = new ResourceManager("MyApp.Resources", typeof(Program).Assembly);
var value = rm.GetString("MyResource");
}
static void Main()
{
var type = typeof(MyClass);
var method = type.GetMethod("MyMethod");
var result = method.Invoke(null, null);
Console.WriteLine(result);
}
public class MyClass
{
public static string MyMethod()
{
return "Hello, World!";
}
}
static void Main()
{
var assembly = Assembly.Load("MyDynamicAssembly");
var type = assembly.GetType("MyDynamicAssembly.MyClass");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("MyMethod");
method.Invoke(instance, null);
}
このような場合、対応するメカニズムをエミュレートしなければなりません。これには、リソースのサポート(アセンブリに静的配列として組み込まれ、特殊なストリーム実装を通じて読み取られる)およびリフレクションが含まれます。当然ながら、.NET アセンブリを C++ コードに直接リンクしたり、異なるプラットフォーム上で動作する Windows の動的ライブラリから関数をインポートしたりすることはできないため、そのようなコードは削除または書き換えなければなりません。
この場合、通常、商用製品での使用を禁止しないサードパーティライブラリの実装を使用して、対応する動作を実装します。
いくつかの場合、これは通常簡単に修正できる単純な実装エラーです。もっと悪いのは、ライブラリコードで使用されるサブシステムのレベルで動作の違いがある場合です。
例えば、私たちの多くのライブラリは、GDI+ をベースに構築された System.Drawing
ライブラリのクラスを広範に使用しています。C++ 用に開発したこれらのクラスのバージョンでは、グラフィックスエンジンとして Skia を使用しています。Skia の動作は GDI+ と異なることが多く、特に Linux 上では、一貫したレンダリングを達成するためにかなりのリソースを費やさなければなりません。同様に、System::Xml
の実装に基づく libxml2 も他のケースで異なる動作を示し、それをパッチするか、ラッパーを複雑にする必要があります。
C# プログラマーは、コードが実行される条件に合わせて最適化します。しかし、多くの構造が不慣れな環境では遅くなります。
例えば、C# で多数の小さなオブジェクトを作成することは、異なるヒープ管理スキーム(ガベージコレクションを考慮しても)のため、一般的に C++ よりも速く動作します。C++ での動的型キャスティングもやや遅いです。ポインタのコピー時に参照カウントを行うことは、C# にはない追加のオーバーヘッドです。最後に、C# から翻訳された概念(列挙子)を使用することは、組み込みの最適化された C++ のもの(イテレータ)に比べてコードのパフォーマンスを低下させます。
ボトルネックを排除する方法は状況によって大きく異なります。ライブラリコードが比較的容易に最適化できる場合、不慣れな環境で翻訳された概念の動作を維持しながらそのパフォーマンスを最適化することは非常に困難です。
例えば、公開 API に SharedPtr<Object>
を受け入れるメソッドがあったり、コンテナにイテレータがなかったり、ストリーム処理メソッドが istream
、ostream
、または iostream
の代わりに System::IO::Stream
を受け入れたりします。
私たちは、C++ プログラマーがコードを便利に使えるように、トランスレータとライブラリを継続的に拡張しています。例えば、トランスレータはすでに標準ストリームで動作する begin
-end
メソッドとオーバーロードを生成できます。
C++ ヘッダーファイルには、プライベートフィールドの型と名前、およびテンプレートメソッドの完全なコードが含まれています。この情報は通常、.NET アセンブリのリリース時に難読化されます。
サードパーティ製ツールとトランスレータ自体の特別なモードを使用して不要な情報を除外するよう努めていますが、常に可能というわけではありません。例えば、プライベートな静的フィールドと非仮想メソッドを削除しても、クライアントコードの動作には影響しませんが、仮想メソッドを削除または改名することは機能を失うことなく行うことは不可能です。フィールドは改名でき、その型は同じサイズのスタブに置き換えることができますが、完全なヘッダーファイルでコンパイルされたコードからコンストラクタとデストラクタがエクスポートされることが条件です。同時に、公開テンプレートメソッドのコードを隠すことは不可能です。
私たちのフレームワークを使用して作成された C++ 言語用の 製品 のリリースは、長年にわたり成功裏に展開されています。最初は縮小版をリリースしていましたが、現在でははるかに完全な機能を維持できるようになりました。
同時に、改善と修正の余地はまだたくさんあります。これには、これまで省略されていた構文構造とライブラリ部分のサポート、およびトランスレータの使いやすさの向上が含まれます。
現在の問題の解決と計画された改善に加えて、トランスレータを現代の Roslyn 構文解析器に移行する作業を進めています。最近まで、NRefactory 解析器を使用していましたが、これは C# バージョン 5.0 までのサポートに制限されていました。Roslyn への移行により、以下のような現代の C# 言語構造をサポートできるようになります:
最後に、サポートされる言語の数を拡大する計画です。ターゲット言語とソース言語の両方を含みます。Roslyn ベースのソリューションを使用して VB コードを読むための適応は比較的簡単です。特に C++ と Java のライブラリが既に準備されていることを考慮すると、比較的容易です。一方、Python をサポートするために使用したアプローチははるかに簡単であり、同様に他のスクリプト言語(PHP など)もサポートできます。