現代のソフトウェア開発では、1つのプロジェクトの中で複数のプログラミング言語を扱うことは珍しくなく、むしろ不可欠な場合もあります。その背景には次のような理由があります。
本記事では、C、C#、Rust を例に、異なる言語間の連携を実現する仕組みを解説します。
多くのプログラミング言語には Foreign Function Interface (FFI) があり、引数や戻り値の受け渡し、エラーや例外の扱いを行います。C# や Rust では、その複雑さは通常便利な構文によって隠蔽されています。
以下は、C で 2 つの int を足し合わせる関数の実装例です:
// C implementation
int calc_sum(int a, int b)
{
return a + b;
}
※これは、FFI 宣言がどれほど簡単に行えるかを示すための、あくまでシンプルな例です。
実際のソフトウェアプロジェクトでは、このような単純な加算処理を FFI 経由で実装するのは適切ではありません。FFI 呼び出しのオーバーヘッドは通常、C# や Rust のビルトイン機能を使って直接演算する場合よりもはるかに大きくなるからです。
この関数は、C# や Rust に宣言を追加するだけで呼び出すことができます。
// C# P/Invoke import
[DllImport("library", EntryPoint = "calc_sum")]
static extern int CalcSum(int a, int b);
// Rust FFI import
unsafe extern "C" {
fn calc_sum(a: i32, b: i32) -> i32;
}
ただし重要なのは、これらの宣言はそれぞれ独立しており、C の実装と一致しているかどうかは開発者の責任であるという点です(型、引数順、シグネチャなど)。
Rust から C にプリミティブ型を渡す場合は値がそのまま渡されます。構造体や共用体のような複雑な型の場合、Rust ではレイアウトを明示的に指定できます。
以下の例は、2バイト、1バイト、4バイトの整数からなる構造体を C 互換 #[repr(C)] で定義しています。
#[repr(C)]
struct ThreeInts {
first: i16,
second: i8,
third: i32
}
もう一つの例として、Rust の参照型があります。参照 &T は本質的には non-null のポインタですが、C の関数に渡す場合や C から返す場合には、nullable なポインタとして扱う必要があります。null を直接扱うのは unsafe であり、未定義動作につながる可能性があるため、Rust では nullable な C ポインタが安全な Rust の Option<&T> と互換であることが保証されています。そのため、言語境界で null が渡された場合には、Rust 側で自動的に Option::None に変換されます。
「Marshalling(マーシャリング)」とは、C# のオブジェクトが C 側で正しく解釈されるように メモリ表現を変換する処理 のことです。
例として、C# の bool は内部的に 1 バイトで true =1、false = 0ですが、Windows API の BOOL は 4 バイトで、false = 0、true = 0以外の値で表されます。
こうした違いに対応するため、C# には StructLayoutAttribute があり、構造体のレイアウトを明示的に指定できます。
FFI (Foreign Function Interface) は標準的なルールを定義していますが、C# の MarshalAsAttributeなどの存在により、ルールを言語ごとに変更できる場合があります。また、C 側・C# 側・Rust 側で宣言を繰り返し書く必要があるため、定義変更時に更新漏れが起こると不整合による問題が発生します。
たとえば C の関数と一致していた C# 宣言:
void *create_foo(int foo_creation_flags);
// C# P/Invoke import
[DllImport("library", EntryPoint = "create_foo")]
static extern nint CreateFoo(FooCreationFlags flags);
これがライブラリ更新で以下に変更されたとします:
void create_foo(int foo_creation_flags, void **result_obj);
この場合、コンパイルエラーは出ませんが、実行時に必ず失敗します。
シグネチャの不一致の種類によっては、次のようなエラーが発生する可能性があります:
さらに、このような不整合は開発時・テストの段階では気づかれないことがあります。
上記の例では、FooCreationFlags は手動で宣言し管理する必要がある列挙型 (enum) である点に注意してください。C 側の定義に変更があった場合は、C# 側の定義にも手作業で反映しなければならず、これを怠ると実行時の障害やバグにつながります。
これは Rust の FFI (Foreign Function Interface) でも同様です。特に構造体を扱う場合、一方の構造体定義でフィールドの欠落や順序変更があると、その問題は実行時まで気付かれなかったり、プログラムが密かに誤った結果を返したりすることになります。
残念ながらできません。Rust では FFI 宣言は常に「unsafe」であり、正しさの保証は開発者の責任です。C# では、ポインタのような “unsafe” な型を使わずに P/Invoke を宣言することも可能ですが、それでもコンパイル時に整合性をチェックする仕組みはありません。これは、C# コンパイラが参照できるのはソースコード内の宣言だけであり、C 側の本来の定義を理解したり検証したりすることができないためです。
アーキテクチャ検証や静的コード解析ツールについて、より詳しい情報をご覧になりたい方は、ぜひ Axivion Suite の紹介ページをご覧ください。
ご不明点やデモのご希望がありましたら、お気軽にお問い合わせください。
最新の製品情報やイベント情報を入手するには、ニュースレターへのご登録をご検討ください: Sign Up For Our Newsletter