前段时间做steamVR驱动开发,发现可以按它的标准写一个动态链接库(DLL),然后放在它程序dirvers文件夹下,它就能自动加载并使用了。(文档如下:https://github.com/ValveSoftware/openvr/wiki/Driver-Factory-Function)
下图分别是steamVR的DLL放置目录及其开发接口。
最近正好写程序想支持扩展,就想到了它这个方法。
预期效果:
我程序大概想实现的效果就是,程序写好以后发布出去不再改动,别人可以按我提供的标准写一个DLL,放在指定目录下。然后程序就可以加载并调用这个DLL中某个指定函数,而不用改动程序的代码。
技术思路:
一、通过扫描目录获取某个目录下的DLL列表
二、由于dll未知,不能使用传统的.h、.lib的方式来配置调用,所以需要使用动态的方式加载DLL及函数接口。
技术实现:
一、扫描目录下DLL文件
扫描功能比较简单,网上资料也很丰富,这里就不赘述了
二、动态加载dll及接口:
使用LoadLibrary函数和GetProcAddress可以实现动态加载dll。
具体过程如下:
1. 首先创建一个导出来测试的DLL。创建DllExport工程,并使用extern "C" __declspec(dllexport)标识导出函数,然后生成DLL(文末提供下载)
//calc.h
#pragma once
extern "C" __declspec(dllexport) int Add(int a, int b);
//calc.cpp
#include"calc.h"
extern "C" __declspec(dllexport) int Add(int a, int b) {
return a + b;
}
2. 再创建一个DllImport工程,用于动态导入DLL,然后将上一步生成的dll拷贝到DllImport工程环境
3. 在DllImport工程中定义接口函数类型
typedef int(*fun)(int, int);
4. 通过LoadLibrary和GetProcAddress引入DLL及其接口
HINSTANCE hActive = LoadLibrary("DllExport.dll");
CallFunc AddFunc = (CallFunc)GetProcAddress(hActive, "Add");
5. 然后就可以调用了,DllImport工程完整代码如下
#include<stdio.h>
#include<Windows.h>
typedef int(*CallFunc)(int, int);
int main() {
HINSTANCE hActive = LoadLibrary("DllExport.dll");
CallFunc AddFunc = (CallFunc)GetProcAddress(hActive, "Add");
if (AddFunc) {
printf("%d\n", AddFunc(11, 10));
}
FreeLibrary(hActive);
return 0;
}
运行可以发现,程序输出了21。这段代码只通过了"DllExport.dll"和"Add"两个字符串,便调用了DllExport中的函数,实现了DLL动态调用。配合思路一的扫描过程,便可以动态调用目录下的DLL了。
然后只需要向DLL开发者提供接口规范,然后开发者按规范开发DLL并放在目录下,就能实现DLL扩展了。
至此便实现了跟steamVR一样,支持DLL扩展的功能了。
额外补充:
上面这种方式针对固定函数接口的情况,已经完全满足了。但如果接口不固定,要怎么做呢?
比如,在上面的解决方法中,接口类型已经被定义死了:int(*)(int,int),在开发者在开发DLL时候,接口类型参数为两个int,返回值为int类型的函数。
如果我们想允许开发者自定义接口参数类型,开发者可以自由设定其接口的参数,接口返回值,这种方式就行不通了。
下面来说说解决方法:
要明确的一点是,即使接口类型由开发者自己定义,也必须要告诉我们的程序,这样程序才能知道可以调用哪些东西。
所以除了DLL文件外,开发者还需要向我们的程序提供一个配置文件,告诉程序接口名及其参数、返回值的构成。
第二个问题,程序知道了开发者自定义接口的构成,但程序如何将获取到的函数接口转为特定类型,并向DLL传递各种参数的组合呢。因为typedef声明个数终归是有限的,但开发者接口中各种参数的排列组合却几乎是无限的,没法用typedef把所有开发者使用到的参数组合类型都定义一遍。
这里我尝试了很多方法,就不多说了,直接给出最优解吧。
解决方案是使用void*实现。在声明调用函数指针的时候将参数及返回值类型全部声明成void*类型,比如上面的例子,typedef int(*CallFunc)(int, int); 改为 typedef (void*)(*CallFunc)(void*, void*);
开发者在开发DLL的时候,依然可以使用自定义接口类型比如int(*CallFunc)(int, int);开发接口。而我们程序在调用的时候,只需要按配置文件提供的参数类型,构造好两个int并强转成void*传递进去就可以实现调用。
测试代码如下:
#include<stdio.h>
#include<Windows.h>
typedef void*(*CallFunc)(void*, void*);
int main() {
HINSTANCE hActive = LoadLibrary("DllExport.dll");
CallFunc AddFunc = (CallFunc)GetProcAddress(hActive, "Add");
if (AddFunc) {
printf("%d\n", AddFunc((void*)11, (void*)10));
}
FreeLibrary(hActive);
return 0;
}
这里使用的DLL跟前面的使用的一样,同样也能输出21。
通过这种方式,就可以把任意类型的参数强制传给DLL的接口了。
这样开发者可以自定义接口类型,写在配置文件中,我们的程序就能通过读配置文件,传接口匹配的类型数据给DLL了。
最后一个问题,参数的数量也是动态的。参数可能只有一个void*,也可能是十个,也可能没有。
虽然可以通过配置文件告诉我们的程序有多少个参数。但是同样我们没法在程序中预定义所有参数数量的接口类型。
这个问题我目前依然没有找到很好的解决方法,所以我使用了一种妥协方案:枚举常用的数量,并限制了开发者接口使用参数数量。
画风就像这样:
typedef void*(*CallFunc_0)();
typedef void*(*CallFunc_1)(void*);
typedef void*(*CallFunc_2)(void*, void*);
typedef void*(*CallFunc_3)(void*, void*, void*);
//...
至此就没了。
最近找这方面资料还是花了很久,主要是开发者自定义接口类型资料太少了,尝试过typeid、模板类、decltype、def文件等等方法,最后都没成。
中途有段时间几乎快放弃自定义接口类型了,差点考虑让开发者使用(int argv,char** argc)的组合了😂。最后自己突然想到void*,然后尝试了一下,发现居然成了。
记录一下,不然以后也忘了。能给后来做这个的人提供一种思路,那就再好不过了,至少说明我这篇文章写的还是有价值的(嘿嘿)。如果里面有哪里写的不对的地方,也欢迎指正。
附上第一段代码打包的DllExport.Dll资源:https://download.csdn.net/download/h84121599/13767802