# 说在前面
Another-Mirai-Native2 在 2.6.0 版本支持加载小栗子插件,期间被这种插件的加载方式折磨的比较难受,写一篇文来记录一下。
# 相关资源
- 小栗子易语言 SDK、应用空白模板
- 小栗子 C# SDK
# 插件种类
根据 SDK 来看,小栗子插件分为 V3 和 V4 版本,区分两种插件的方法是:插件有没有同名的 .dll.json 文件。若存在此文件,则为 V4 插件;若不存在,则是 V3 插件。
# V3 插件
# 插件信息
小栗子插件通过 初始化 这个 Dll 接口来向上层传递插件本身的信息,信息以 Json 格式传递,其中包括:
- SDK 版本 (sdkv)
- 应用名称 (appname)
- 作者 (author)
- 应用描述 (describe)
- 应用版本 (appv)
之后在以 addres 结尾字段均为插件暴露的事件函数地址
随后在 data 字段中是插件所需要的所有 API 列表,严格来说也不是列表,是一个对象

# 插件初始化
我们在通过上一步获取了插件基础信息之后,就可以将事件的所有地址转换为本地函数以供进行事件分发。
此后,我们需要将插件提供的所有 API 列表,通过调用 初始化 方法的方式传递给插件
初始化函数在易语言中定义的参数如下:
根据 SDK 源码猜测,第一个参数以 Json 数组格式传递 API 地址列表,第二个参数传递类似于 AuthCode 的参数用于标识插件
其中,地址列表的格式如下:
1 | [ |
地址列表,顾名思义就是函数的在内存中的地址列表,而 C# 属于托管语言,获取自身函数在内存的地址较为困难,我们会在后续章节通过曲线救国的方式来解决这个问题。
# 事件分发
# 事件分类
小栗子的事件一共有以下几种,下面是事件的名称与在第一步返回的事件地址键的名称
- 被启用处理函数 (useproaddres)
- 被禁用处理函数 (banproaddres)
- 将被卸载处理函数 (unitproaddres)
- 插件菜单处理函数 (setproaddres)
- 私聊消息处理函数 (friendmsaddres)
- 群聊消息处理函数 (groupmsaddres)
- 频道推送统一处理函数 [注:仅在 V4 插件有]
- 事件消息处理函数 (eventmsaddres)
- 插件消息输出替换处理函数 [注:仅在 V4 插件有]
- 滑块识别处理函数 (SliderR)
- 短信接码处理函数 (SMSVerification)
- GetTicket (getticketaddres) [注:名字我起的,含义未知,仅在 V3 插件有]
- GetVefCode (getvefcodeaddres) [注:名字我起的,含义未知,仅在 V3 插件有]
# 事件函数原型
函数的参数表在提供的空白模板中都有,此处不再占用篇幅
想说是插件在进行参数传递时,使用了很多的结构体,甚至结构数组。当然对于 P/Invoke 来说,使用结构体传递复杂参数很正常,但是不正常点在于:易语言的结构体数组与常规的数组结构有很大不同,在这里我也苦恼了好几天,才摸索出门道,将在后面进行详细介绍。
# 窗口
小栗子插件最多只有一个窗口,触发逻辑与调用事件相同
# 调用上层 API
此处是做兼容比较头疼的部分,一方面是小栗子提供的 API 有 400 个之多,甚至他们连发红包的接口都提供了,方法甚至要求输入银行卡号以及支付密码,这我是完全理解不了的,开发者难道不怕框架偷偷把这些信息转发走吗......
另一方面其实还是和数量多有关,数量多需要做的代码转换工作就多,建议使用 CodeGen 一类的方法来减轻工作量,我搓了一个能够应付易语言的脚本,能够将这些文本转换为 C# 的 DllExport 输出,感兴趣的可以试试。
想吐槽的一点是,从上面的图片也能看出来,他们每次调用 API 的时候都需要从上层提供的 API 列表里查找函数地址,之后再调用,我觉得挺低效的。
# V4 插件
其实没什么差别,唯一不同的点就是调用初始化函数获取插件信息的部分
获取插件信息的步骤和 V3 插件一样,都是通过 初始化 这个方法,交换 API 地址列表与插件基础信息,不同的点是:V4 插件拥有自己的 .json 描述文件,结构如下:
感觉和 V3 相比清晰了一些,但是不多。
V4 插件将事件更改为了导出的函数名称,这样上层就可以使用名称来找函数导出而不是使用地址。所以当调用初始化方法时,函数返回的内容就只有前五个基础信息了。
# 兼容难点
- 可读性不高的 SDK:易语言我只是凑合能看所以不评价这个。而 C# SDK 即使是抄的 Jie2GG 的酷 Q SDK,新增的内容命名的方式非常的百度翻译、部分字段命名非常迷惑,可读性一塌糊涂。
- API 传递使用地址而不是 P/Invoke:可能对于非托管语言来说这个问题不大,但是 C# 是托管语言,一旦双方均使用 C#,就会发生各种命名空间冲突,发生异常。
- 易语言的结构体数组非常规
# 使用地址调用 API
最一开始的实现里,所有的 API 均在框架的一个静态类中,通过 Marshal.GetFunctionPointerForDelegate 来获取地址,确实能够获取得到,但是当插件调用时就发生了问题:当插件也是 C# 插件时,会发生类型不匹配,这是因为双方的委托虽然参数列表都一致,但是由于对方也是 C#,在 CLR 看来无异于将一个不同类型的委托复制给一个变量,所以发生了类型不一致的异常。
1 | public string OutLog(string message, int text_color = 16711680, int background_color = 16777215) |
解决方案其实原理我也没闹明白,只是能用而已。
我将所有的 API 实现均移植到了 CQP.dll (这个是酷 Q 插件 API 实现的位置),之后通过 DllExport 导出。在调用初始化函数提供 API 地址列表的时候,改为通过 GetProcAddress 获取。
我也不知道通过这个导出之后在 CLR 看来有什么变化,反正是不异常了。
1 | private string BuildSelfEntryInfo() |
# 易语言结构体非常规
在 C++ 中,数组是一串连续的内存块,每一块中放置数据的地址。但是在易语言中,结构体数组结构如下:
| 0x0 | 0x4 | 0x4 + 4 | 0x4 + 8 | ... | 0x4 + 4 * n |
|---|---|---|---|---|---|
| 起始位置 (一般写 1) | 数组成员数量 (? 猜的) | 第一个成员的地址 | 第二个成员的地址 | ... | 第 n 个成员的地址 |
这样的结构如果使用预先设定的结构很难实现
1 | [] |
这样的结构是不可行的,我通过观察内存结构发现,这个数组不会像上图一样铺开,而是单纯放置一个指针,其实还挺合理的。
查询了一些资料无果后,决定使用仿造结构的方式,直接操作内存来构造这个结构:
1 | int dataListSize = Marshal.SizeOf(typeof(int)) * 2 + Marshal.SizeOf(typeof(int)) * list.Count; // 通过欲构建的数量计算内存大小 |