やりたいこと
仕事でC++の資産を使うためにP/InvokeでC#からC++のdllにある関数を呼ぶことがあるが、その際、元々あるC#のP/Invokeのコードを参考にほかの関数を呼ぶためのC#コードを書いてるが、0から書きなさいと言われると正直書き方がわからない。
あと、WindowsのWin32APIを呼ぼうと思うと、pinvoke.netとかにC#側コードのひな型やサンプルを置いてくれているが、なんでそういうC#コードになるのかわからない、またpinvoke.netであるWin32APIのC#側コードをみると、なんか複数の書き方が書かれていて(戻り値とか引数の型が違う等)、どれが正しいのかよくわからない。
どう書けばよいか、この際調べたい。
やり方
いろいろ試す中で(今もまだ試行錯誤中)、現状下記の流れで、C++の関数をC#側で呼ぶように調査を進めるのがよい気がする。
- 呼びたいC++関数の仕様を確認する。
(既存の自作関数の場合は仕様書を見るor呼んでる個所のコードや実動作を見る、Win32APIならMicrosoftの公式ドキュメントを見る) - 呼びたいC++の関数を、C++で呼ぶ練習をする。
(自作関数の場合はそれを呼ぶ個所を見る、Win32APIならそれをC++のConsoleアプリなどでまず呼んで動作の確認をする) - 呼びたいC++の関数の戻り値と引数の型を、C#ではどんな型にすればよいか確認する。
(pinvoke.net や Wikipedia、MSの公式doc を参考にする) - C#側のコードで、NativeMethodsクラスを作成し、その中に呼びたいC++関数のP/Invokeの定義を書く。
- P/InvokeメソッドをC#のテスト用Consoleプロジェクトなどで呼んでみる。
- 本番のコードに、作ったP/Invokeのコードを入れる。
サンプル1(メモリリークする失敗Ver)
21/02/27追記
何となく動いているが、確保したメモリの解放ができてないせいでメモリリークしているっぽい。
調べます...
(CoTaskMemAllocでやってるからまずい?Marshallの中のメモリを確保するやつでやればいい?)
■C++(呼ばれる側)
今回は、上の手順をぐるぐる回しつつ自分でC++側の関数も作って、C#側から呼ぶ練習をした。
(今思うと、数値の型で試せばもう少し簡単だったと思うのだが、文字列を扱う関数を作ってしまった。とりあえず、それをサンプルとして挙げる。)
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <combaseapi.h>
#include <wchar.h>
#define VC_DLL_EXPORTS
#include "DllTest.h"
void __cdecl StringPinvokeTest(WCHAR** msg)
{
// コピーしたい文字列
const wchar_t* tmp = L"あいうえおかきくけこ";
// コピーしたい文字列の分だけメモリを確保
*msg = (WCHAR*) ::CoTaskMemAlloc((wcslen(tmp) + 1) * sizeof(WCHAR));
// 確保したメモリに文字列をコピー
if (*msg != NULL) wcscpy_s(*msg, wcslen(tmp) + 1, tmp);
}
#pragma once
#include <Windows.h>
#include <string.h>
// エクスポートとインポートの切り替え
#ifdef VC_DLL_EXPORTS
#undef VC_DLL_EXPORTS
#define VC_DLL_EXPORTS extern "C" __declspec(dllexport)
#else
#define VC_DLL_EXPORTS extern "C" __declspec(dllimport)
#endif
// エクスポート関数のプロトタイプ宣言
VC_DLL_EXPORTS void __cdecl StringPinvokeTest(WCHAR** msg);
■C#(呼ぶ側)
using System;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string buf = null;
NativeMethod.StringPinvokeTest(ref buf);
Console.WriteLine(buf);
Console.ReadLine();
}
}
public static class NativeMethod
{
[DllImport("DllTest.dll", CharSet = CharSet.Unicode)]
public extern static void StringPinvokeTest(ref string lpText);
}
}
サンプル2(正しく動くVer)
サンプル1ではメモリリークが発生してうまくいかなかったので改良を加えた。
サンプル1では、バッファをC++側で確保していたが、そうではなく
C#側でもって、それをC++側に渡せばよいっぽい。
(そうすれば、C#側でちゃんと解放できる)
その場合、今のところ、下記の3種類のやり方があった。(ほかにもあるかも)
- ① stringで受け取る(GCが開放)
- ② IntPtrで受け取る(自前で解放)
- ③ byte[]で受け取る(GCが開放)
①は、stringにあらかじめセットした文字列の長さが、C++にわたるバッファのwchar_tの配列の要素数になるっぽい。で、そんな動きはよくわからない(必要な文字数をあらかじめセットするってなんだ??)ので、②か③がよいなと思った。
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
{
// ①stringで受け取る版
// stringで入れた文字列の長さ(文字数)が、C++側に渡されるwchar_tのバッファの長さになるっぽい
// string str = "";にすると、なにも帰ってこない
string str = "AAAAAAAAAAAAAAAAAAAAAAA";
NativeMethod.CopyStringToBuffer(str);
Console.WriteLine(str);
}
{
// ②IntPtrで受ける版
IntPtr buf = Marshal.AllocHGlobal((int)16);
NativeMethod.CopyStringToBuffer_2(buf);
var name = Marshal.PtrToStringUni(buf);
Marshal.FreeHGlobal(buf);
Console.WriteLine(name);
}
{
// ③byteで受け取る版
var buf = new byte[16];
NativeMethod.CopyStringToBuffer_3(buf);
Console.WriteLine(Encoding.Unicode.GetString(buf));
}
Console.ReadLine();
}
}
public static class NativeMethod
{
// ①stringで受け取る版
[DllImport("DllTest.dll", CharSet = CharSet.Unicode)]
public extern static void CopyStringToBuffer(string s);
// ②IntPtrで受ける版
[DllImport("DllTest.dll", EntryPoint = "CopyStringToBuffer")]
public extern static void CopyStringToBuffer_2(IntPtr s);
// ③byteで受け取る版
[DllImport("DllTest.dll", EntryPoint = "CopyStringToBuffer")]
public extern static void CopyStringToBuffer_3(byte[] s);
}
}
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <combaseapi.h>
#include <wchar.h>
#define VC_DLL_EXPORTS
#include "DllTest.h"
// 「あいうえお」を、引数で渡されたバッファにコピーするだけの関数
// ※本来はバッファサイズを受け取って保護をかけたりすべきだと思うが、
// 今回は簡単にするために省略
void __cdecl CopyStringToBuffer(wchar_t* str)
{
wchar_t tmp[] = L"あいうえお";
if (str != NULL)
{
// lengthはバッファの大きさ+1にしておかないと落ちる
wcsncpy_s(str, wcslen(tmp) + 1, tmp, wcslen(tmp) + 1);
}
}
#pragma once
#include <Windows.h>
#include <string.h>
// エクスポートとインポートの切り替え
#ifdef VC_DLL_EXPORTS
#undef VC_DLL_EXPORTS
#define VC_DLL_EXPORTS extern "C" __declspec(dllexport)
#else
#define VC_DLL_EXPORTS extern "C" __declspec(dllimport)
#endif
// エクスポート関数のプロトタイプ宣言
VC_DLL_EXPORTS void __cdecl CopyStringToBuffer(wchar_t* str);
備考
文字列のP/Invokeが難しかった
文字列の扱い、なにかややこしい。下記サイトをもとに、ちょっと勉強したい。
++C++ 小ネタ string型のマーシャリング
https://ufcpp.net/blog/2016/12/tipsstringmarshal/
C#で呼ぶ以前に、自分のC++への理解不足に気づいてしまった
今回のサンプルのように、単純に文字列をメモリにコピーするだけの処理でも、C++で書くとなるとどうやったらよいのか迷いに迷った。C++をまずは勉強しなおさないといけないっぽい。