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;
}

我们导出了两个函数,AppInfoInitialize,这两个函数的声明是酷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