C++/CLI で C# 7 の新機能に追従する


はじめに

C++/CLI はフェードアウトな雰囲気が非常に漂っていますが、過去の資産を継続して使わなくてはいけないなど避けられないケースもまだまだありそうに思います。実際、ネイティブとのバインディングには便利ですからね・・・

一方 C# は現在においても革新が続いており、特に C# 7 以降はその改善速度が非常に上がってきています。 C++/CLI は C# (マネージ側) ~ネイティブとのバインディングに使う事が主な用途だと思いますので、実際に使う C# 側から扱いやすいように便利な記述に対応したいと考えるわけです。特に値型の参照渡し関連はパフォーマンスにも影響があるので是非使いたい。

ということでどこまで対応できるかやってみました。

C# 7 、としていますが C# 7 以上 8 未満のものを扱います。


原理

C# の言語拡張は絶えず続いていますが、実際にビルドして出力されるバイナリ (IL) のフォーマットは変わってきていません。 (C# 8 で導入されるインターフェースのデフォルト実装で IL の拡張が入る予定のようです)

では言語拡張はどのように行われているかというと


  • 命名規則など特定条件が合うように記述された時に機能する

  • 専用の属性の有無で判断する

といった形で行われています。前者は Awaitable パターンなどです。

今回は後者の属性による拡張です。 C# の拡張された言語機能の多くはコンパイルされた結果に属性が付与され、属性が付与されたアセンブリを参照している側からは属性があると、その新しい文法で記述されたものと認識できるので、新しい文法に対応した処理を行うという形になります。

有名な例として C# の拡張メソッドを C++/CLI で記述する、というものがあります。 C# で拡張メソッドを定義するには次のように書きます。


C#

namespace Extensions

{
public static class SampleExtensions
{
public static int Sum(this int self, int a)
{
return self + a;
}
}
}

これを C++/CLI で記述するとこうなります。


C++/CLI

using namespace System::Runtime::CompilerServices;

namespace Extensions
{
[Extension]
public ref class SampleExtensions
{
public:
[Extension]
static int Sum(int self, int a)
{
return self + a;
}
};
}


ExtensionsAttribute という属性が指定のメソッドが拡張メソッドであることを表しているわけです。

C# の拡張メソッドに関する予約語のうち IL で表現できないものが属性による付加情報の形で付け足されるので、それと同じ事を C++/CLI で記述すると C# 側もそれに沿った形で参照できるようになります。


C++/CLI で C# 拡張機能の表現を実装する


パラメーターの in 修飾子

in 修飾子をつけると指定の引数が参照渡しになります。巨大な構造体を扱う場合、コピーが発生しなくなるのでパフォーマンス面で有利になります。

.NET Framework は 4.7.1 以降にしてください (旧バージョンの .NET Framework で使えるようにするのは後述) 。

例えば C# で


C#

static int Sum(in int a, in int b)

{
return a + b;
}

と同じものを C++/CLI で書くには次のようにします。


C++/CLI

using namespace System::Runtime::CompilerServices;

using namespace System::Runtime::InteropServices;

static int Sum([In][IsReadOnly] int% a, [In][IsReadOnly] int% b)
{
return a + b;
}


IsReadOnlyAttribute は .NET 4.7.1 で追加された属性です。

System.Runtime.CompilerServices.InAttribute と System.Runtime.CompilerServices.IsReadOnlyAttribute を付与し、 C++/CLI のマネージド型への参照を表す % を付けた型にすると C# の in 修飾子と同じ効果になります。

戻り値型の ref readonly 修飾子も同じだったのですが、戻り値型に InAttribute の付与が (私が試した限りでは) できませんでした。少々残念。


ref struct, readonly struct


C#

ref struct AA

{
}

readonly struct BB
{
public readonly int X;
}


は次のようになります。


C++/CLI

using namespace System::Runtime::CompilerServices;

[IsByRefLike]
public value class AA
{
};

[IsReadOnly]
public value class BB
{
public:
[IsReadOnly]
initonly int X;
};


IsByRefLikeAttribute も .NET 4.7.1 以降のようです。

readonly ref struct は "IsByRefLikeAttribute" と "IsReadOnlyAttribute" の両方を指定すれば OK です。


メンバー名付 ValueTuple

.NET Framework は 4.7 以降にしてください (私が試した限りでは C++/CLI に NuGet の System.ValueTuple の追加ができなかったため) 。

ValueTuple のメンバーは Item1, Item2, ... といった通し番号的で意味のわからないものですが、 C# では名前を付けることができます。下記の場合、 x と y がそれぞれのメンバー名です。


C#

static (int x, int y) Pack(int x, int y)

{
return (x, y);
}

C++/CLI で同じことは


C++/CLI

using namespace System::Runtime::CompilerServices;

[returnvalue:TupleElementNames(gcnew array<System::String^>{"x", "y"})]
static System::ValueTuple<int, int> Pack(int x, int y)
{
return System::ValueTuple::Create(x, y);
}


と記述します。

TupleElementNamesAttribute の引数に文字列配列でメンバー名を順番に指定します。

お手軽感が完全に損なわれているのとパフォーマンス面で優位性もないので、ここまでするくらいなら C++/CLI でこのようなケースでは専用の構造体を定義した方がベターな可能性が高いと思います。


実装時の注意

これはあくまで C++/CLI での実装で C# コンパイラが付与するのと同じもの相当の属性を付与することで C# 側から拡張された言語機能として認識させる、というものです。

C# であればコンパイラがチェックをした上で属性を付与するわけですが、 C++/CLI はそのような事を理解できないので実装者が正しく記述する必要があります。


  • パラメーター in 修飾子で IsReadOnlyAttribute を付与しても C++/CLI 側は readonly と認識できないので普通に参照先が書き換え可能になってしまっています。書き換えをするコードを実装してはいけません。

  • readonly struct ではメンバーフィールドにも IsReadOnlyAttribute を付与しなければなりません。

一方で C++/CLI は文法的にパラメーター定義で明示的に参照かどうかの区別がつくので構造体の防衛的コピーが自動で行われることはないのはいいことかもしれません。


旧バージョンの .NET Framework で使う

これまで挙げた IsReadOnlyAttribute などの新しい属性は .NET 4.7 ないし 4.7.1 で追加されたものです。一方、 C# 7 は .NET 4.6 でも使用可能で、その場合でも in 修飾子は使用可能です。 4.7 じゃないと存在しない属性を使って 4.6 向けのコンパイルができているということになります。

では C# コンパイラはどうやっているのかというと、 internal な属性クラスをアセンブリ内に組み込んで、それを参照するようにしているようです。同じことを C++/CLI でもやれば .NET 4.6 世代でも C++/CLI で対応可能になります。


IsReadOnlyAttribute.h

namespace System

{
namespace Runtime
{
namespace CompilerServices
{
ref class IsReadOnlyAttribute : System::Attribute
{
};
}
}
}


おわりに

C++/CLI の現状、一番つらいところは


  • 基本的な言語仕様が C++03 ベースのままで、 C++11 以降の新機能が使えない

  • マネージドは致し方ないにしても、ネイティブで C++11 以降で追加されたライブラリが使えない

あたりですね。特に後者はつらい。

今後 .NET Core 3 に移行するにあたって、 C++/CLI の mixed mode アセンブリの対応 もする方向のようなので、ビルドしたバイナリの動作は継続できそうですが、コンパイラ等の開発環境はどうなるのかが不透明です。このまま .NET Framework ベースのまま現状維持が有力かな・・・

C++ とのネイティブバインディングは C++/CLI に頼らないやり方を確立しておくのがよいと思います。以前書いた記事 (C#からC++のインスタンスメソッドを呼び出す) も是非ご一読ください。