このページについて

伺かアドベントカレンダー2015の12月14日の記事を掲載しているページです。

C++/CLIで書いた混合アセンブリSAORIでC#を実行しよう。


この記事を理解するための推奨スキル
・C++が書けること(C++/CLI は説明していくので書けなくても大丈夫かも)
・C#が書けること
・さおりの仕様がすこしわかること
・Visual Studioが使えること

この記事で得られるであろう知見
・さとりすとを使ったゴーストのさわり判定の作り方
・さとりすとのサーフェスビューワ・サーフェスパレットに対応したシェルの作り方

記事の内容

基本的にアンマネージDLLしか呼び出せないSAORIを、C++/CLIを使って書いたSAORIを経由して.NETのDLLを呼び出す方法のご紹介です。この記事ではマネージコードにC#を使用しています。
比較的記述が簡単なC#で、ゴーストのややこしい部分をいじってみるとか、そういうものに繋がればと思います。

この方法では、動かすPCに.NET FrameworkとVisual C++ランタイムが必要になります。
.NET Frameworkは3.5がwindows7でプリインストールです。
Visual C++ランタイムは別途ユーザにインストールしてもらうというよりは、必要なファイルをゴーストに同梱することになるでしょう。

1.とりあえず混合アセンブリでDLLを作ろう

まず、VisualStudioで混合アセンブリでビルドするSAORI DLLを作成してみます。
DLLを作成するプロジェクトを作成してください。

次に、DLLを混合アセンブリにしますのでプロジェクトの全般設定で共通言語ランタイム(.NET)サポートを有効にします。
プロジェクトの設定

準備ができたら基礎知識のご紹介です。
C++/CLIでは、C++で使うアンマネージ型のstd::stringのような型とは別にC#と同じマネージ型のクラスを使うことが出来ます。
マネージクラスの使用には、一般的なC++と異なる文法を使いますのでここで確認しておきましょう。

■参照型
	C#の場合
	String str = "あああ";
	str.ToString();

	C++/CLIの場合
	String^ str = "あああ";
	str->ToString();
	//参照型(.NET マネージハンドル)を示す「^」が付きます。ポインタみたいな。

■名前空間
	C#の場合
	using System.Collections.Generic;
	System.Collections.Generic.List

	C++/CLIの場合
	using namespace System::Collections::Generic;
	System::Collections::Generic::List
	//単純に記法がC++の名前空間になるだけです。

■クラス
	C#の場合
	public class ClassName
	{
		public static int a;
		public int b{get;set;}
	}

	C++/CLIの場合
	public ref class ClassName
	{
	public:
		static int a;
		property int b;
	};

	//C++/CLIの場合マネージクラスにはrefキーワードをつけます。
	//マネージクラスにはアンマネージクラス(普通のC++形式のclass)をメンバにできず、
	//同様にアンマネージクラスのメンバにマネージクラスを含めることもできません。

■値型
	C#の場合もC++/CLIの場合も同じです。
	bool a = true;

■null
	C#の場合
	null

	C++/CLIの場合
	nullptr
	//C++/CLIの場合はnullptrと表記し、C++のNULLとは別の扱いになります。

■new
	C#の場合
	String str = new String("あいうえ");

	C++/CLIの場合
	String^ str = gcnew String("あいうえ");
	//C++ で動的メモリを確保するnewと区別して、.NETマネージメモリを使用するgcnewを使用します。
	//.NETのメモリなのでC#同様、delete相当のものはありません。
	// ref class キーワードで定義するマネージクラスはgcnewで作成します。

■配列new
	C#の場合
	String[] a = new String[10];
	
	C++/CLIの場合
	array<String^>^ a = gcnew array<String^>(10);

まず、混合アセンブリでは1つのソースコード上にマネージ(.NET)とアンマネージ(普通のC++)を混在させることになりますので、線引をする必要があります。
これはそれぞれ、#pragmaを使用して区切ることが出来ます。
区切りを超えて相互に関数を呼び出すことが出来ます。(マネージ型を引数にもつ場合はネイティブからは呼べないけど)

#pragma managed
//ここにマネージコードを書く
#pragma unmanaged
//ここにネイティブコードを書く
それぞれ制約があるのですが、今回はあまり意識しなくて良いようにアンマネージ関数はSAORIインターフェース部分のみ、それ以外はマネージ関数とはっきりわけて書いてます。
まずは、SAORIのインターフェースから作成したマネージ関数を呼び出すところまで書きます。

#pragma managed
//ここにマネージコードを書く
#inclide<Windows.h>
using namespace System;
using namespace System::Reflection;

//さおりのフォルダ名を保存しておくクラス。
public ref class Data
{
public:
	static String^ saori_dir;
};

void Load( HGLOBAL h, long len )
{
	Data::saori_dir = gcnew String( (char*)h, 0, len, System::Text::Encoding::GetEncoding("Shift_JIS"));
	//HGLOBALの解放
	GlobalFree(h);

}

HGLOBAL Request( HGLOBAL h, long* len )
{
	String^ request_str = gcnew String( (char*)h, 0, *len, System::Text::Encoding::GetEncoding("Shift_JIS"));
	GlobalFree(h);

	//ここでさおりの出力を作成します。
	//実際にさおりとしてきちんと使う場合は、リクエストの解析が必要ですが、とりあえず最小構成。
	String^ return_str = "SAORI/1.0 200 OK\r\nResult: てすとっととと\r\nCharset: Shift_JIS\r\n\r\n";

	//StringをC++の文字列に変換するために、Shift_JISでエンコードしたbyte配列を作成し、
	//マネージメモリからアンマネージメモリにコピーするMarshal::CopyでHGLOBALに書き込んで戻り値にします。
	array<byte>^ ar = System::Text::Encoding::GetEncoding("Shift_JIS")->GetBytes(return_str);
	HGLOBAL hg = GlobalAlloc(GMEM_FIXED, ar->Length);
	System::Runtime::InteropServices::Marshal::Copy(ar, 0, System::IntPtr(hg), ar->Length);
	*len = ar->Length;
	return hg;
}

#pragma unmanaged
//ここにネイティブコードを書く

extern "C" __declspec(dllexport) BOOL __cdecl load(HGLOBAL h, long len)
{
	Load(h, len);
	return TRUE;
}

extern "C" __declspec(dllexport) HGLOBAL __cdecl request(HGLOBAL h, long* len)
{
	return Request(h, len);
}

extern "C" __declspec(dllexport) BOOL __cdecl unload()
{
	return TRUE;
}
こういう感じですね。色々と最小限ですが。
一応、さおりとして呼び出せば「てすとっととと」と返してくれるはずです。

2.C#のDLLを呼び出してみよう

次に、マネージDLLを読み込む機能を用意します。
まずは先にC#で呼び出される側のマネージコードをつくってみましょう。
簡単に、さおりの応答を返すだけのメソッドを作成してみます。

using System;
namespace TestNamespace
{
	public class TestClass
	{
		public static String Test()
		{
	 		return "SAORI/1.0 200 OK\r\nResult: てすとです!!\r\nCharset: Shift_JIS\r\n\r\n";
		}
	}
}
これでビルドしてManaged.dllという名前にしておくことにして、

SAORI側はC#を呼び出すようにRequest()を改造します。


HGLOBAL Request( HGLOBAL h, long* len )
{
	String^ request_str = gcnew String( (char*)h, 0, *len, System::Text::Encoding::GetEncoding("Shift_JIS"));
	GlobalFree(h);

	//マネージDLLを読み込みます。
	//さおり側のDLLが解放されないと読み込んだDLLが解放されないらしいので、本来はここで読み込んじゃダメ。
	Assembly^ asmb = Assembly::LoadFrom( Data::saori_dir + "Managed.dll");

	//マネージDLLから呼び出すメソッドのある型の情報を取得。
	Type^ type = asmb->GetType("TestNamespace.TestClass");

	//型からメソッド情報を取得。
	MethodInfo^ method = type->GetMethod("Test");

	//メソッド情報を通じてメソッドを呼び出します。
	//最初の引数はthisにあたるオブジェクトですがstaticメソッドなのでnullptr
	//次の引数はメソッドに渡す引数の配列ですが呼び出し先が引数をとらないのでnullptr
	//呼び出し先の戻り値はStringなのでStringでキャストします。
	String^ return_str = (String^)method->Invoke(nullptr, nullptr);

	array<byte>^ ar = System::Text::Encoding::GetEncoding("Shift_JIS")->GetBytes(return_str);
	HGLOBAL hg = GlobalAlloc(GMEM_FIXED, ar->Length);
	System::Runtime::InteropServices::Marshal::Copy(ar, 0, System::IntPtr(hg), ar->Length);
	*len = ar->Length;
	return hg;
}

これでマネージDLLを呼び出せます。実行すれば出力がかわってるはず。
わっほい。

3.C#のコードを実行時にコンパイルして使おう

最後にC#のコードを実行時にコンパイルして使う方法をご紹介します。
C#は、C++と異なり、コンパイラが実行環境に付属しています。
そのためC#のコードをスクリプトのように使用することも十分可能です。

//コンパイルオプションを設定するDictionaryを作成します。
//.NET Framework3.5 でコンパイルせよと設定しています。
System::Collections::Generic::Dictionary < String^, String^ >^ options = 
	gcnew System::Collections::Generic::Dictionary <String^, String^ >();
options->Add("CompilerVersion", "v3.5");

//コンパイラ本体のようなCSharpCodeProviderと、コンパイルパラメータを準備します。
Microsoft::CSharp::CSharpCodeProvider code = gcnew Microsoft::CSharp::CSharpCodeProvider(options);
System::CodeDom::Compiler::CompilerParameters param = gcnew System::CodeDom::Compiler::CompilerParameters();
param->GenerateInMemory = true;				//メモリ上にアセンブリを作成(ファイルとして作成しない)
param->IncludeDebugInformation = false;		//デバッグ情報を出力しない

//コンパイルするファイル名を配列にして、コンパイルを実行。
array<String^>^ files = {  Data::saori_dir + "Source.cs" };
System::CodeDom::Compiler::CompilerResults^ result = SaoriAsm::CodeProvider->CompileAssemblyFromFile(param, files);

if( result->Errors->Count == 0 )
{
	//コンパイル成功

	//コンパイルしたアセンブリ。DLLを読み込んだものと同様に使うことが出来ます。
	Assembly^ asmb = result->CompiledAssembly;
}

これでAssemblyオブジェクトを作成できるので、あとはマネージDLLを読み込んだ時と同様に呼び出すことができます。
もちろん.NETのコードなのでC++/CLIで書かなくてもC++/CLIから呼び出すC#でも同様のことをすることができます。


とまぁ、やや駆け足でしたがこういった感じでSAORIとしてC#を使うことが出来るようにできます。
最後にまとめますと、
できること
・C#がゴーストで使える!!
・SSPのプロセスとして動かせる。

問題点
・Visual C++ のランタイムの同梱が必要。
・C++/CLIを経由する必要がありやや面倒。
こういった感じです。

ここまでよんでくださって、ありがとうございました。
どのくらいに人によんでもらえて、役に立てて貰えるかはさっぱりわかりませんが、こういうこともやろうと思えば出来ますよ、ということでひとつ書かせていただきました。

おまけ:ゴーストアップローダを作った話。

さとりすとを使ってゴーストをアップロードして公開や更新ができるアップローダのご紹介です。
以前よりPHPでアップローダを作ってはいたのですが、今回はバージョン2になってそれなりにきちんと動き出したので改めてちょっとした記事にしてみました。

こういうやつです。
なー をあげるから、なーなろーだー。略してななろだ。

Sosiremiと何が違うの?

ゴーストアップローダには既にSosiremiという更新が可能なゴーストアップローダもあります。
比較するとこんな感じです。
Sosiremiとの主な違い
・ダウンロードカウンタがない
・アップロードがさとりすと専用
・作者ごとにIDを作成
・検索機能がない
・ゴーストごとのページが作成されない
・転送量上限は大きめ(有料サーバの契約プラン上)
わりと比べると出来ないことは多いんですが、Sosiremiで公開した場合は検索によって探せてしまうので、ダウンロードの際には自作の配布ページを必ず経由して欲しいなどの要求には対応できたりしています。
また、さとりすとでゴーストをアップロードすることができるので、さとりすとを使っているなら最初に設定を行うだけであとはとても簡単に更新ができます。
そのあたりは一長一短ということで相性のいいものを選んでもらうのが良いでしょう。

この他にも色々できるようになったらいいなぁと思いつつも。
こういうひとにおすすめ!
・さとりすとを使ってる
・ブログやサイトに配布ページを作りたい、そこからDLしてもらいたい

つかってみたい

きになったら、お気軽にどうぞ。
練習用もあります。さとりすとwikiに説明かいてます。
ななろだ

web拍手
もしよかったら、ぽちっと。

お借りした素材
下記の素材をお借りしました。ありがとうございました。
http://www.pixiv.net/member_illust.php?mode=medium&illust_id=52766818