动态链接库(八)--二次开发dll

写在前面

此前已接触了开发和使用动态链接库时的诸如声明时的导入导出符号位置、调用约定位置; 不同编译器调用约定的名字改编规则以及如何解决不同编译器调用约定导致的名字改编问题等。

在经历上述问题并了解如何解决过后,相信后续对动态链接库的开发使用都会更从容一些。

例在实际的工作中,难免会用到其他开发人员开发的dll。

在使用其他开发人员开发的dll时,从该dll导入的函数都有发生名字改编,且提供的.h头文件中也没有指明编译环境和调用约定,导致在自己项目中调用.h头文件中的函数时,会有一堆 无法解析的外部符号 错误提示。

这里我们就会知道,该错误是因为这些dll的导出函数的 编译环境和调用约定 可能和我们目前使用该dll的项目不一样,导致的名字改编问题,因此使用原始函数名调用时导致编译报错。

知道原因后,我们就可以同之前一样,使用dumpbin工具查看该dll的导出函数发生改编后的名字,依次推断下这些导出函数的编译环境和调用约定

然后在dll提供的.h头文件中添加相应处理:不同编译器添加extern “C”, 调用约定则在声明中显示添加即可。

但也会有其他开发同事使用该dll时会碰到上述问题,假设这里就有同事不会处理过来问你如何解决的,这时你就可以神秘地给他这几段代码:
调用约定
解决不同编译器导致的名字改编问题
解决不同调用约定导致的名字改编问题

告知他:全是你想要的😏。

当然上面链接都是咱们之前解决不同编译器和调用约定导致的名字改编问题的实例,其他需求的话,不会还有人不知道全球最大的hub吧。


虽然知道如何解决了,但为了方便后续使用,你的leader让你封装下这个dll,封装成后续使用时不用在人为的修改包含的.h头文件就能直接使用原始函数名调用。

因为这个dll是其他开发人员的,当然最理想的方式就是找到他,让他修改该dll项目,解决名字改编问题。

不过人家大概率是不会搭理你的,如果他帮你改了,那你就可以考虑入手了,毕竟过日子还是傻点的好。




如果他不改,你也可以问他要代码,自己改,不过人家大概率也是不会给你的。

那么这是你可能就会考虑,二次开发这个dll,再封装成dll。即自己新建一个dll,在自己这个dll中使用发生名字改编的dll,然后在自己的这个dll中解决名字改编问题,后续就提供自己封装的这个dll,依旧能够使用之前dll的函数,且不再有名字改编导致的调用问题。

下面,我们就此需求出发,详细学习下,如何二次开发dll。这里会具体分成:二次开发C dll和二次开发C++ dll 两部分。

二次开发C dll

既然要二次开发dll,总得需要有个dll让我们二次开发吧。因此这里提供一个C编译生成的dll,代替上述没有源码,且发生名字改编的dll。

//CDll.h
#pragma once
#ifdef DLL_API
#else
#define  DLL_API __declspec(dllimport)
#endif

int DLL_API __stdcall add_c(int a, int b);
int DLL_API __stdcall sub_c(int a, int b);

void DLL_API __cdecl fun();

//CDll.c
#define DLL_API __declspec(dllexport)
#include "CDll.h"

#include <windows.h>
#include <tchar.h>

int __stdcall add_c(int a, int b)
{
	return a + b;
}

int __stdcall sub_c(int a, int b)
{
	return a - b;
}


void DLL_API __cdecl fun()
{
	OutputDebugString(_T("\n-----fun-----\n"));
}

因为要有名字改编,因此这里不提供模块定义文件。

编译生成,使用dumpbin查看:
1

这里通过改编名,我们可以推断_add_c@8是C编译__stdcall 调用约定,同理_sub_c@也是,而fun则是C编译__cdecl调用约定。

为了模拟上述使用原始名调用失败问题,这里将生产的dll更新到我们C++项目,使用原始名调用:
2

简单解决就是手动在cplusTest项目包含CDll.h中添加extern “C” 限定符即可:
3

但这里像新创建一个dll项目,在这个dll项目中封装CDll,并解决名字改编问题。

这里新建了一个Dll2的dll项目,并有将CDll更新到该项目中:
4

然后如何解决名字改编呢?
先修改CDll.h,添加条件编译的extern “C” 限定符, 再在模块定义文件中添加CDll中导出函数的符号,然后在Dll2.h 头文件中包含CDll.h即可。

5

编译生成,对外提供Dll2.h、Dll2.lib、Dll2.dll以及在Dll2中引用的CDll的相关文件: CDll.h、CDll.lib、CDll.dll。

使用dumpbin查看CDll.dll 和 二次封装的Dll2.dll 中的导出函数:
6
7
可以看到在CDll.dll 中三个导出函数都是有发生名字改编的,改编名分别是**_add_c@8、_sub_c@8和_fun**, 在Dll2中解决名字改编(即在模块定义文件中指定导出符号)后,从Dll2.dll导出的三个函数就没有发生名字改编了:add_c、sub_c和fun。

这里注意函数相对地址的联系,以add_c函数为例,在Dll2.dll中add_c的函数地址为Dll2.dll的相对地址(当被加载的使用进程后会被替换成Dll2.dll在该进程中的实际地址) + 一个偏移量(70), 这里对应的是小括号后的_add_c@8。

因为Dll2.dll中没有该函数的实现,因此会去Dll2.dll中引用的其他模块—CDll.dll中查找,在CDll.dll中,_add_c@8的函数地址为CDll.dll的相对地址(当被加载至Dll2时会被替换成在Dll2中的实际地址) + 偏移量(125).

因此,当使用Dll2.dll的项目在链接时,会将所有相对地址赋实际值。当调用add时,会去Dll2.dll中找_add_c@8, 然后再对应到CDll.dll中的 实际地址 + 125 偏移量的函数地址。

更新到cplusTest项目中,虽然多了Dll2.h、Dll2.lib、Dll2.dll,实际使用时只需包含Dll2.h即可。
当然也需要在项目属性中导入库文件Dll2.lib 和 CDll.lib。
8

只需包含Dll2.h, 即可直接使用原始函数名调用。

注意事项

注意二次封装的Dll2中,不能有和CDll中的导出函数有同名的存在,这样编译就不会通过。

9

二次开发C++ dll

同上,这里也提供一个C++的Dll1,然后在Dll2中二次开发。

c++dll项目Dll1如下:

//Dll1.h
#pragma once
#ifdef DLL1_API
#else
#define DLL1_API __declspec(dllimport) 
#endif

//C编译
#ifdef __cplusplus
extern "C"
{
#endif

	int DLL1_API __stdcall add(int a, int b);
	int DLL1_API __stdcall subtract(int a, int b);

#ifdef __cplusplus
};
#endif


//C++编译
void DLL1_API __cdecl test();

//重载函数
void DLL1_API __stdcall output(int a, int b);
void DLL1_API __stdcall output(int a, char b);
void DLL1_API __stdcall output(int a, int b, char c);


class Dll1
{
public:
	//构造和析构必须指定导出符号, 否则无法创建销毁Dll1实例对象
	DLL1_API Dll1();
	virtual DLL1_API ~Dll1();

	void DLL1_API publicFun();

public:
	//无法为类成员变量声明导出符号
	int /*DLL1_API*/ m_nVal;

private:
	void DLL1_API privateFun();
	//无法为类成员变量声明导出符号
	int /*DLL1_API*/ m_nVal2;
	
};

Dll1.cpp如下:

//Dll.cpp
#define DLL1_API __declspec(dllexport)
#include "Dll1.h"
#include <windows.h>
#include <tchar.h>

int __stdcall add(int a, int b)
{
	return a + b;
}

int  __stdcall subtract(int a, int b)
{
	return a - b;
}


//C++编译
void DLL1_API __cdecl test()
{
	OutputDebugString(_T("\n-----test-----\n\n"));
}

void DLL1_API __stdcall output(int a, int b)
{
	TCHAR buf[100] = { 0 };
	_stprintf_s(buf, _T("\noutput重载一:int a = %d,int b = %d\n\n"), a, b);
	OutputDebugString(buf);
}

void DLL1_API __stdcall output(int a, char b)
{
	TCHAR buf[100] = { 0 };
	_stprintf_s(buf, _T("\noutput重载二:int a = %d,char b = %c\n\n"), a, b);
	OutputDebugString(buf);
}

void DLL1_API __stdcall output(int a, int b, char c)
{
	TCHAR buf[100] = { 0 };
	_stprintf_s(buf, _T("\noutput重载三:int a = %d,int b = %d, char c = %c\n\n"), a, b, c);
	OutputDebugString(buf);
}

Dll1::Dll1() : m_nVal(0), m_nVal2(0)
{
	TCHAR buf[100] = { 0 };
	_stprintf_s(buf, _T("\nDll1默认构造--m_nVal = %d\n"), m_nVal);
	OutputDebugString(buf);
}

Dll1::~Dll1()
{
	OutputDebugString(_T("\nDll1析构\n"));
}

void Dll1::publicFun()
{
	OutputDebugString(_T("\nDll1--publicFun\n"));
}

void Dll1::privateFun()
{
	OutputDebugString(_T("\nDll1--privateFun\n"));
}

因为希望Dll1各导出函数发生名字改编,因此这里不提供模块定义文件解决名字改编问题。

编译生成后,使用dumpbin命令查看:
10

然后在Dll2项目中解决Dll1中能够解决的名字改编问题,不能解决的有:
①重载函数无法通过模块定义文件解决名字改编问题
②类的析构函数

更新Dll2项目如下:

#pragma once

#ifdef DLL2_API
#else
#define DLL2_API _declspec(dllimport)
#endif

#include "CDll.h"

#include "Dll1.h"

//Dll2中也可以在这声明自己的导出函数
void DLL2_API __stdcall dll2_test();
#define DLL2_API _declspec(dllexport)
#include "Dll2.h"

#include <windows.h>
#include <tchar.h>


void __stdcall dll2_test()
{
	OutputDebugString(_T("\n-----dll2_test-----\n\n"));
}

编译生成,使用dumpbin命令查看:
11
可以看到Dll1.dll 和 CDll.dll 中的导出函数都没有发生名字改编了。

然后更新到C++项目cplusTest中使用,所需文件:
CDll: CDll.h、CDll.lib、CDll.dll
Dll1:Dll1.h、Dll1.lib、Dll1.dll
Dll2: Dll2.h、Dll2.lib、Dll2.dll

dll 文件直接放在输出exe文件的Debug目录下:
12

可以在项目根目录下创建一个Lib文件夹用来存放dll对应lib 和 .h文件:
13
Lib文件夹内再创建一个include文件夹用来存放.h文件:
14
15
这样子的话方便管理,不过为了能在项目中直接include “Dll2.h”, 需要配置下项目属性:
16

选择刚刚创建的include文件夹目录即可:
17

导入lib的时候也许注意,添加相对main.cpp的lib存放路径:
18

然后就可以正常使用了, 使用时只需包含Dll2.h即可

//cplusTest中的main.cpp
#include <iostream>
using namespace std;
#include "Dll2.h"

int main()
{
	//CDll.dll 导出函数测试
	cout << "add_c: " << add_c(3, 3) << endl;
	cout << "sub_c: " << sub_c(3, 3) << endl;
	fun();

	//Dll1.dll 导出函数测试
	cout << "add: " << add(4, 4) << endl;
	cout << "subtract: " << subtract(3, 3) << endl;

	test();
	output(1, 2);
	output(1, 'C');
	output(1, 2, 'D');

	Dll1 dll1;
	dll1.publicFun();
	//dll1.privateFun();	即使是导出函数,也无法通过实例调用类的私有成员函数

	//Dll2.dll 导出函数测试
	dll2_test();


	system("pause");
	return 0;
}

重新编译运行,均能正常调用:
19

总结

无论是C编译生成的dll, 还是C++编译生成的dll,若其没有解决 不同编译器 或 不同调用约定 导致的名字改编问题,都可以通过在封装一层的方式去解决原始dll中没有解决的名字改编问题。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 通达信 dll二次开发主要是指在通达信软件的基础上,通过使用动态链接库dll)进行二次开发来实现自定义的扩展功能。通达信软件本身已经提供了许多股票交易相关的功能,但是很多用户可能还需要其他定制化的功能或者数据接口,这时候就可以通过dll二次开发来实现。 通达信 dll二次开发的主要过程包括编写C++代码、动态链接库编译、调用dll等。在编写C++代码时,开发人员需要根据需求来编写相关的功能实现代码,并将其封装dll文件。编写完成后,需要进行编译并生成所需的动态链接库文件。最后,在使用通达信软件时,可以通过调用dll来实现自定义的功能或者数据接口。 通达信 dll二次开发主要的应用范围包括行情数据接口、交易接口、指标计算等方面。通过使用dll,可以让通达信软件获得更加灵活的扩展性和定制性,以满足用户的个性化需求。同时,这种方式也提高了开发效率和代码重用性。 总之,通达信 dll二次开发是一种非常有用的扩展方案,可以为用户提供更加完善和满足个性化需求的股票交易软件。 ### 回答2: 通达信是国内最受欢迎的股票分析软件之一,拥有众多的用户和开发者。其中,通达信 dll二次开发是一种非常重要的开发方向,它可以为用户带来更多的功能和灵活性。 通达信 dll二次开发需要掌握一些专业的技术,如C++、MFC、Win32、COM等。在使用这些技术进行开发之前,需要先了解通达信软件的架构和原理,包括主程序、数据文件和策略文件等。这样才能更好地对其进行二次开发,实现用户想要的功能。 在通达信 dll二次开发中,最常见的需求是添加自定义指标、自定义公式和自定义函数等,以及实现外部交互功能。开发者可以利用通达信提供的DLL接口来实现这些功能,从而满足用户的需求。 除此之外,通达信 dll二次开发还可以扩展通达信软件的功能和性能,例如加速大量数据的处理、绘制更复杂的图表和图形、增强交易功能、优化策略执行等。 总之,通达信 dll二次开发是一个非常重要的开发方向,它可以增强通达信软件的功能和灵活性,为用户和开发者带来更多的价值。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值