C语言编写酷Q插件 · 不使用任何SDK
大家都知道如果要用酷Q开发一个QQ机器人,第一步要去下载一个SDK,用易语言的人下载易语言SDK,用C++的人下载C++SDK,用Go的人下载Go语言SDK……
但是其实,完全可以不用任何SDK就能写出一个酷Q插件。
写个空插件
说到底,所谓一个QQ机器人插件,其实就是响应一些事件(Event),处理这些事件,调用一些接口(API),对应的其实就是在最终生成的动态链接库(DLL)中导出一些函数作为事件,再从酷Q的CQP.dll中导入一些函数作为API拿来调用。
以下的宏就完全可以胜任以上工作,注意这些是C代码,如果用C++的话需要加上extern "C"。
#include <stdint.h>
#if defined(_MSC_VER)
#define _CQ_EVENT(ReturnType, FuncName, ParamsSize) \
__pragma(comment(linker, "/EXPORT:" #FuncName "=_" #FuncName "@" #ParamsSize)) __declspec(dllexport) \
ReturnType __stdcall FuncName
#else
#define _CQ_EVENT(ReturnType, FuncName, ParamsSize) __declspec(dllexport) ReturnType __stdcall FuncName
#endif // defined(_MSC_VER)
#define _CQ_API(ReturnType, FuncName, ...) ReturnType(__stdcall* FuncName)(int32_t ac,##__VA_ARGS__)
有了这么好用的宏,接下来自然是要用它导出几个函数试试了。紧接着像下面这样写。
int32_t ac;
_CQ_EVENT(const char*, AppInfo, 0) // 插件ID
() {
return "9,io.github.tnze.luaq";
}
_CQ_EVENT(int32_t, Initialize, 4) // 插件初始化
(int32_t auth_code) {
ac = auth_code;
return 0;
}
我们导出了两个函数,AppInfo和Initialize,这两个函数的声明是酷Q规定好的。给_CQ_EVENT宏传入的三个参数分别是返回类型、函数名和参数总大小,例如AppInfo没有参数就是0,Initialize有一个int32所以是4。另外还定义了一个全局变量ac用来储存酷Q发给我们的授权码,后面调用API的时候会需要用到。
写到这里,这个插件编译后就已经能被酷Q加载并运行了。那么怎么编译呢?在Windows上编译C语言一直是个非常令人头疼的事情,因为有很多坑,这两个我踩了个遍。
我们这么硬核的项目自然是要用CMake这种现代的工具来管理了。先看一眼最终用来编译这个DLL的脚本。
cmake_minimum_required(VERSION 3.6)
project(MyProject)
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_options(app INTERFACE -static -Wl,--kill-at,--enable-stdcall-fixup)
set_target_properties(app PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32" PREFIX "")
endif ()
add_library(app SHARED "app.c")
这里要解决的第一个问题是,酷Q要求我们编译出的DLL是32位的,处理方法又与编译器有关:如果你想用VS自带的msvc编译器,那么需要找到"项目-CMake设置",添加一个x86配置。选定之后点击菜单生成-全部生成,dll就编译出来了;如果你决定用MinGW-w64,则上面脚本中if内的参数-m32会自动生效。
第二个问题更难解决,酷Q要求函数调用要用stdcall约定。而C语言默认使用cdecl 约定,所以在上面的宏中使用__stdcall关键字指定使用stdcall,而这又导致了另一个问题:改变了函数签名。
使用stdcall之后,会在函数名前面加个下划线,在函数名后加个@,然后后面在跟上参数长度。结果就是原本好好的Initialize,突然就变成了_Initialize@4,而这与酷Q的约定不符。解决方式呢也是分两种:在msvc中,通过在_CQ_EVENT宏中添加那一串linker参数。在MinGW中则要把链接参数放在cmake脚本中,也就是-static -Wl,--kill-at那一部分。
编译完了"app.dll",接下来写"app.json"。
{
"ret": 1,
"apiver": 9,
"name": "插件名",
"version": "1.0.0",
"version_id": 1,
"author": "作者",
"description": "填写你的应用描述",
"event": [],
"menu": [],
"status": [],
"auth": []
}
这两个文件就能被酷Q作为插件加载、启用了。目前为止它什么都不干,但是接下来我们就要给它添加功能了。
给插件加点料
让我们先写一个Enable事件吧,这个事件将在插件被启用时触发。
_CQ_EVENT(int32_t, OnEnable, 0)
() {
return 0;
}
这个函数有0个参数,返回一个int32,函数名任意的。目前先什么都不干,让我们先在json中注册这个函数,这样酷Q才能知道如何调用这个函数。
{
"event": [
{
"name": "插件启用",
"function": "OnEnable",
"type": 1003,
"priority": 20000,
"id": 1
}
]
}
写了很多东西,事件名、函数名、事件类型、优先级、ID等等,其中1003固定代表插件启用事件。
此时运行插件,在启用时OnEnable就会被调用了,但是调用了还看不出来呀?别急,接下来告诉你怎么调用API。
// int CQ_addLog(int ac, int level, char *type, char *msg);
_CQ_API(int32_t, CQ_addLog, int32_t level, const char* tp, const char* msg);
_CQ_EVENT(int32_t, Initialize, 4) // 插件初始化
(int32_t auth_code) {
ac = auth_code;
HMODULE hm = LoadLibrary("CQP.dll");
CQ_addLog = (void*)GetProcAddress(hm, "CQ_addLog");
return 0;
}
第一行定义了一个函数指针变量CQ_addLog,然后在初始化时动态载入CQP.dll,并且从dll中找到酷Q的"CQ_addLog"这个函数的地址,然后赋给这个指针。接下来就可以在Enable里面调用了。
_CQ_EVENT(int32_t, OnEnable, 0)
() {
CQ_addLog(ac, 10, "Test", "hello, world");
return 0;
}
调用时第一个参数要传入之前获取到的授权码,第二个参数是日志等级,分别是:
- Debug = 0
- Info = 10
- Warning = 20
- Error = 30
- ……
这时候去运行一下吧!你能看到自己用C从头开始写的插件发出来的第一条日志的!
这篇文章写到这里就要结束啦,基本的东西已经介绍完了,通过举一反三相信调用其他API或者导出其他Event对你来说也没有什么难度了。
另一个bug
对了,我研究这些东西的时候还遇到一个问题,因为我是在Windows沙盒中运行酷Q进行测试的,结果一直报错:LoadLibrary失败(126 找不到指定的模块。)。
排查了半天也不知道为什么无法加载,最后在某网友的帮助下确认了问题。。是crt。
解决方法有三:
- 用MinGW编译,放弃MSVC
- 用MSVC,编译参数MD改为MT
- 啥都不改,在目标机器上安装VC++ runtime