[.NET 7] P/Invokeの落とし穴メモ
15年以上P/Invokeに向き合っていますが分からないことは尽きません。雑なメモなので不正確ならすみません。おいおい書き足していくかもしれません。
環境
.NET 7
LibraryImport の話を加味しています。ただし.NET 7特有の話は一部です。
1. 解放処理だけはSafeHandleを使わない
C++側定義の例
ここではVC++向け仕様としています(__declspecのところ
)が、この点は本題とは無関係です。
// ラップしたいクラス
class Foo
{
...
};
extern "C"
{
__declspec(dllexport) Foo* __cdecl createFoo()
{
return new Foo;
}
__declspec(dllexport) void __cdecl releaseFoo(Foo* obj)
{
delete obj;
}
}
C#側定義 (動かない例)
上のネイティブ実装を呼び出すC#側の例です。Foo
のポインタを表すSafeHandle
であるFooHandle
と、最新LibraryImport
による呼び出しの定義です。
using System.Runtime.InteropServices;
internal class FooHandle : SafeHandle
{
internal FooHandle()
: base(invalidHandleValue: IntPtr.Zero, ownsHandle: true)
{
}
public static FooHandle Create()
{
return NativeMethods.createFoo();
}
protected override bool ReleaseHandle()
{
NativeMethods.releaseFoo(this);
return true;
}
public override bool IsInvalid => handle == IntPtr.Zero;
}
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
internal static partial class NativeMethods
{
private const string LibraryName = "Foo";
[LibraryImport(LibraryName)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial FooHandle createFoo();
[LibraryImport(LibraryName)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial void releaseFoo(FooHandle obj);
}
しかしこれは実行時エラーになります。
// 作ってすぐ解放
{
using var foo = FooHandle.Create();
}
System.ObjectDisposedException
Safe handle has been closed.
Object name: 'SafeHandle'.
at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success)
LibraryImportAttribute
が生成してくれたコードを見に行くと、理由がわかります。
internal static unsafe partial class NativeMethods
{
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.10605")]
[System.Runtime.CompilerServices.SkipLocalsInitAttribute]
internal static partial void std_string_delete2(global::FooHandle obj)
{
System.IntPtr __obj_native = default;
// Setup - Perform required setup.
bool obj__addRefd = false;
try
{
// Marshal - Convert managed data to native data.
obj.DangerousAddRef(ref obj__addRefd);
__obj_native = obj.DangerousGetHandle();
{
__PInvoke(__obj_native);
}
}
finally
{
// Cleanup - Perform required cleanup.
if (obj__addRefd)
obj.DangerousRelease(); // ここ?
}
// Local P/Invoke
[System.Runtime.InteropServices.DllImportAttribute("Foo", EntryPoint = "deleteFoo", ExactSpelling = true)]
[System.Runtime.InteropServices.UnmanagedCallConvAttribute(CallConvs = new System.Type[]{typeof(global::System.Runtime.CompilerServices.CallConvCdecl)})]
static extern unsafe void __PInvoke(System.IntPtr obj);
}
}
__PInvoke
のところで解放されたあと、obj.DangerousRelease();
で再び解放処理に向かってしまうのが問題のようです。解放以外でSafeHandleを渡すメソッド定義には有効なコード生成ですが、解放では困るということですね。
C#側定義 (正解例)
簡単な解決は、解放だけは素のポインタで定義するというものです。
internal static partial class NativeMethods
{
[LibraryImport(LibraryName)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial void releaseFoo(IntPtr obj);
}
internal class FooHandle : SafeHandle
{
// 中略
protected override bool ReleaseHandle()
{
NativeMethods.releaseFoo(handle);
return true;
}
}
ちなみにDllImportでも同様の挙動になるようで、裏では似たようなおまじないがなされているのかもしれません。
2. stringが返るP/Invokeメソッド定義はしない
メモリ領域確保の実装上、あまり例は無いかもしれませんが。
C/C++側定義の例
何らかのネイティブ側で確保されているchar*
が返るとします。
static const char* str = "Hello world!";
extern "C" __declspec(dllexport) const char* __cdecl getString()
{
return str;
}
C#側定義の例 (動かない例)
internal static partial class NativeMethods
{
private const string LibraryName = "Foo";
[LibraryImport(LibraryName, StringMarshallingCustomType = typeof(Utf8StringMarshaller))]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial string getString();
}
呼び出すと、私の環境では何秒か待たされたのちに死にます。これもコード生成結果を見ると理由がわかります。
internal static unsafe partial class NativeMethods
{
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.10605")]
[System.Runtime.CompilerServices.SkipLocalsInitAttribute]
internal static partial string getString()
{
string __retVal;
byte* __retVal_native = default;
try
{
{
__retVal_native = __PInvoke();
}
// Unmarshal - Convert native data to managed data.
__retVal = global::System.Runtime.InteropServices.Marshalling.Utf8StringMarshaller.ConvertToManaged(__retVal_native);
}
finally
{
// Cleanup - Perform required cleanup.
global::System.Runtime.InteropServices.Marshalling.Utf8StringMarshaller.Free(__retVal_native);
}
return __retVal;
// Local P/Invoke
[System.Runtime.InteropServices.DllImportAttribute("OpenCvSharpExtern", EntryPoint = "foo", ExactSpelling = true)]
[System.Runtime.InteropServices.UnmanagedCallConvAttribute(CallConvs = new System.Type[]{typeof(global::System.Runtime.CompilerServices.CallConvCdecl)})]
static extern unsafe byte* __PInvoke();
}
}
// Cleanup - Perform required cleanup.
のところでFree
されています。今回はマネージド側での解放は意図していません。引数でstringを渡すときはこのコード生成が有効に働きそうですが、戻り値では余計なお世話になっています。
この例もまた、DllImport
でも同様にダメのようです。
C#側定義 (正解例1)
ポインタのまま得て、自分でマネージド文字列にするのが無難に思われました。
internal static partial class NativeMethods
{
[LibraryImport(LibraryName, StringMarshallingCustomType = typeof(Utf8StringMarshaller))]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial IntPtr getString();
}
var p = NativeMethods.getString();
var s = Marshal.PtrToStringUTF8(p);
C#側定義 (正解例2)
コード生成結果から拝借です。
internal static partial class NativeMethods
{
[LibraryImport(LibraryName, StringMarshallingCustomType = typeof(Utf8StringMarshaller))]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static unsafe partial byte* getString();
}
unsafe
{
var p = NativeMethods.getString();
var s = System.Runtime.InteropServices.Marshalling.Utf8StringMarshaller.ConvertToManaged(p);
}
C#側定義 (正解例3)
カスタムマーシャラーを定義する方法で、.NET 7以降を要します。今回の例に対しては大げさかもしれませんが。
Discussion