Unity3D (Mono/IL2CPP) 中 P/Invoke 平台调用代码应该如何传委托

IL2CPP does not support marshaling delegates that point to instance methods to native code. 你可能平时在 .NET Core / Framework 的代码中写得很正常的托管代码的委托调用,在 Unity3D 中变得不可行。

本文举个例子,并且将其改正。


举例:查找所有可见窗口

本文的例子会使用到 NuGet 包 Lsj.Util.Win32,这是个非常棒的 Win32 调用的 API 包装,可以免去大量自己可能写不对的 [DllImport]

引入命名空间:

using Lsj.Util.Win32;
using Lsj.Util.Win32.BaseTypes;

然后查找所有的可见窗口。

public static IReadOnlyList<HWND> FindVisibleWindows()
{
    var found = new List<HWND>();
    User32.EnumWindows(OnWindowEnum, IntPtr.Zero);
    return found;

    BOOL OnWindowEnum(HWND hWnd, LPARAM lparam)
    {
        if (User32.GetParent(hWnd) == IntPtr.Zero && User32.IsWindowVisible(hWnd))
        {
            found.Add(HWND);
        }
        return true;
    }
}

Mono / IL2CPP

Unity 编译的时候可以选择脚本后端是 Mono 还是 IL2CPP。不幸的是,没有 .NET Core 或者未来的 .NET 5/6,因此很多 .NET Core 的特性不能用。

关于脚本后端的选择,可以参见我的另一篇博客:

在编译时不会有什么问题,但是在运行时会发生异常(如果你去捕捉,或者用 VS 调试就可以看到):

NotSupportedException:
        IL2CPP does not support marshaling delegates that point to instance methods to native code.
        The method we're attempting to marshal is: Win32WindowExtensions+<>c__DisplayClass0_0::<FindVisibleWindows>g__OnWindowEnum|0
    at Lsj.Util.Win32.User32.EnumWindows (Lsj.Util.Win32.User32+WNDENUMPROC lpEnumFunc, Lsj.Util.Win32.BaseTypes.LPARAM lParam)
    at Win32WindowExtensions.FindVisibleWindows ()

“IL2CPP 不支持封送实例方法到本机代码”。

修正代码

Mono/IL2CPP 要求封送到本机的代码必须是静态方法,且必须标 MonoPInvokeCallback 特性。

因此,我们不得不把上面的代码改成这样:

using System;
using System.Collections.Generic;
using System.Text;
using AOT;
using Lsj.Util.Win32;
using Lsj.Util.Win32.BaseTypes;

public class WindowsEnumerator
{
    private static List<HWND> _currentList;

    public IReadOnlyList<HWND> FindVisibleWindows()
    {
        _currentList = new List<HWND>();
        User32.EnumWindows(OnWindowEnum, IntPtr.Zero);
        return _currentList;
    }

    [MonoPInvokeCallback(typeof(User32.WNDENUMPROC))]
    static BOOL OnWindowEnum(HWND hWnd, LPARAM lparam)
    {
        if (User32.GetParent(hWnd) == IntPtr.Zero && User32.IsWindowVisible(hWnd))
        {
            _currentList?.Add(HWND);
        }
        return true;
    }
}

当然上述代码不是线程安全的。所以如果你希望在多线程环境下使用,请自行修改为线程安全的版本。


参考资料

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/unity3d-marshal-callback-must-be-static.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,或者前往 CSDN 关注我的主页

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected])