【C/C++】让自己的程序可以支持DLL进行扩展——动态加载DLL及其参数

前段时间做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

 

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值