起初只是希望能在运行时调试通达信 DLL 时输出一些调试信息,直接想到的是采用一个窗体,窗体内包含输出信息的文本框,和一个清除文本框内容的按钮。但我并没有通过 C++ 开发桌面端应用的经验。否则用 Qt 应该是个不错的选择。
我对桌面端应用的开发经验大部分是关于 C# 的,首先想到的就是用 C# 来做窗体控件了,这就涉及到了 C++ 调用 C#,或者 C++ 和 C# 的混合编程。经过短时间调研,最终选择了采用 C++/CLI 来建立混合编程的方式,来构建 DLL。
这样做的好处是,我不用再额外点太多 C++ 的技能点,而且还可以享受丰富的 .NET 生态。譬如,除了设计一个调试输出窗口,还可以借助 .NET 的生态,嵌入 javascript 引擎,采用 js 写指标,让我能够在运行时动态调整指标代码。
现在开始吧。
先决条件
- 你需要了解 C++ 语言的基础知识。
- 在 Visual Studio 2017 及更高版本中,C++/CLI 支持是一个可选组件。 若要安装它,请从 Windows 开始菜单打开“Visual Studio 安装程序”。 确保选中“使用 C++ 的桌面开发”磁贴,并在“可选”组件部分选中“C++/CLI 支持”。
环境搭建
Win10 系统,采用 Visual Studio 2022(17.5.3),在 Visual Studio Installer 里,单个组件要勾选上:
- .NET 桌面开发
- 使用 C++ 的桌面开发
- 默认选项
- 对 v143 生成工具(最新)的 C++/CLI 支持(具体版本由你选定的 C++ 版本确定,我这里是 v143)
更多内容请参考 演练:在 Visual Studio 中编译面向 CLR 的 C++/CLI 程序。
新建项目
打开 VS,创建新项目,选择“CLR 空项目(.NET Framework)”:
新建一个名称为CLRDllTest
的项目,载入后右侧是标准的 VS 开发 C++ 项目的结构,按照通达信DLL函数编程规范将指定的头文件和源文件放入对应的结构内。
新建一个 .NET Framework 类库项目DebugForm
,并设计新建一个窗体类LogForm
:
using System;
using System.Windows.Forms;
namespace DebugForm
{
public partial class LogForm : Form
{
public LogForm()
{
InitializeComponent();
}
public void Log(string message)
{
textBox1.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.FFF}] {message}\r\n");
textBox1.ScrollToCaret();
}
private void button1_Click(object sender, EventArgs e)
{
textBox1.Text = "";
}
}
}
窗体设计内容如下:
为了方便CLRDllTest
引用,我们可以调整项目DebugForm
的生成后事件,将其 copy 一份到CLRDllTest
的项目下。此处不赘述。
CLRDllTest 引用 C# 窗体
假设CLRDllTest
项目已经能引用DebugForm.dll
,为了能在全局引用托管的DebugForm
,在TCalcFuncSets
内定义一个全局引用类:
ref class GlobalObjects {
public:
static DebugForm::LogForm^ logForm;
};
修改导出给通达信 TCalc 的函数RegisterTdxFunc
:
BOOL RegisterTdxFunc(PluginTCalcFuncInfo** pFun)
{
GlobalObjects::logForm = gcnew DebugForm::LogForm();
GlobalObjects::logForm->Show();
GlobalObjects::logForm->Log("DLL loaded.");
if (*pFun == NULL)
{
(*pFun) = g_CalcFuncSets;
return TRUE;
}
return FALSE;
}
试着在任意自定义函数内调用logForm
:
void TestPlugin1(int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc)
{
GlobalObjects::logForm->Log("1 号函数被调用,DataLen:" + DataLen);
for (int i = 0; i < DataLen; i++)
pfOUT[i] = i;
}
测试使用
编译DebugForm.dll
和CLRDllTest.dll
,将前者放在通达信执行目录(即包含 tdxw.exe 文件的目录)下,后者放在通达信目录下的 T0002/dlls/ 下,打开通达信配置调用 DLL 指标:
{这里 H, L, C 是随便填来占位的,实际在函数内用不到}
X1:TDXDLL1(1, H, L, C);
结果:
由此便完成了 C++/CLI 模式下的 .NET Framework 混合编程。
注意事项
dll 最终放置位置/引用
DebugForm.dll 是通过 C++/CLI 托管的动态链接库,载入时,应该由主程序 EXE 载入,即放在 tdxw.exe 同目录下,由 tdxw.exe 载入,而非放在指标 DLL 目录下,否则 tdxw.exe 会找不到 DebugForm.dll,导致程序崩溃。
除非 DebugForm.dll 是通过 COM 注册的 .NET DLL,但那就是运行时动态加载了。此时,Native C++ 项目内其实是有相关 .NET DLL 的信息的(tlb, tlh)文件的,并且此时也不能使用 CLR 模式了。
窗体控件缓冲
此处调出DebugForm::LogForm
,并用 TextBox 来存放日志只是一种示范,实测中,当不断调用 Log 方法将日志写入 TextBox 一段时间后,程序便卡死了。为此我调试了两天时间,什么智能指针之类的东西都用了,最后发现居然是……调试窗体的 TextBox 被输入了太多文本,内存溢出了。
这是我万万没想到的。
解决方法是定期清理缓冲区。我编写的程序中,TextBox 的MaxLength
被设置为了默认值 32767,输入的日志长度刚超过这个范围时,无事发生,因为此处的MaxLength
仅阻止用户输入,而不会阻止通过设置Text
属性变化的文本1。但超过大约 138308 (确切值未知)的字符串长度后,程序便卡死。后来我对 TextBox 做了超过指定长度便清除的操作,就没再出现过卡死。猜测应该是被托管的窗体的 TextBox 控件的缓冲区内存溢出了,具体达到多少值便溢出,以及这个最大值由什么决定,没有深究。
所以实际上,当使用某种托管控件时,应该要考虑到该控件的性能限制,按照功能需求合理选择控件,且要考虑到控件能接受的运行时的最大缓冲,避免因内存溢出而导致程序卡死。
为了解决这种情况,如果托管的窗口有太多资源内容的,可以考虑将窗口作为一个独立的进程,通达信和额外窗口之间采用管道进行通信,而非直接调用。(见在通达信 C++/CLI DLL 与外部应用程序之间使用管道通信)。
全局引用
在示例代码中,托管窗体logForm
被我放在了一个 ref class 中作为静态变量。实际上经过测试,也可以直接在最顶层声明静态/非静态的托管类型。
资源回收
采用 C++ 编程的最重要的一点,即是要注意资源的回收,否则可能会出现内存溢出的情况。