DLL 指标的存在,使得原本某些通过通达信原生指标语法实现的功能,得以借助编程语言实现,如循环结构和更复杂的分支结构。虽然使用 DLL 指标仍然存在一些限制(见:通达信DLL- 注意事项),但可以通过一些特殊手段(见:通达信 DLL 指标多参数解决方法),绕过限制。总的来说,DLL 指标优点大于缺点。
但此时就不可避免地引出了一些问题:载入 DLL 的时候,内部的指标算法是已经被编译好的,如果想要修改算法,就必须关闭程序、修改代码、重新编译再重新打开程序。这样的操作未免太麻烦。有没有一种方式,可以不用重新编译、反复打开程序,就能实现代码的修改呢?
答案是肯定的:那就是引入脚本语言——DLL 本身并不直接参与指标的计算,而是提供函数入口和脚本语言解释器。当 DLL 内的函数被调用时,首先通过脚本解释器解释动态加载的脚本,而后将函数传入的参数传递给脚本执行,最终得到的结果再返回。
在通过 C++/CLI 建立 .NET Framework 混合的通达信指标 DLL中,阐述了如何通过 C++/CLI 编程使用 .NET Framework 的生态。而通过借助 .NET 的丰富生态,自然可以便捷地实现脚本解释器的功能。可供选择的脚本语言非常多,如 python、lua、ruby 等。在这里我选用对环境要求最低的一门跨平台脚本语言:Javascript。
调研工作
根据资料 Running JavaScript inside a .NET app with JavaScriptEngineSwitcher. Andrew Lock. 所述,.NET 运行 JavaScript 有两种思路,一种是通过 Node.js 构建相应的 JS 应用,由 .NET 与 Node.js 交互得到运行结果;另一种是直接在 .NET 应用程序或类库中嵌入 JS 引擎,由 .NET 应用直接解释/运行 JS 脚本。
显然,后者更适合我们的需求。根据文章对后者实现的分类,我最终选择了 Jint 作为最终实现的方式。
JS 解释器和 JS 方法实现
使用和 DebugForm 同样的方式建立JsTestClass
类,并建立如下的TestJs
方法:
public float[] TestJs(float[] in1, float[] in2, float[] in3)
{
float[] result = new float[in1.Length];
try
{
var engine = new Engine();
engine.SetValue("in1", in1);
engine.SetValue("in2", in2);
engine.SetValue("in3", in3);
engine.Execute(@"
var result = [];
for (var i = 0; i < in1.length; i++) {
result.push((in1[i] + in2[i] + in3[i])/3);
}
");
result = engine.GetValue("result").AsArray().Select(x => (float)x.AsNumber()).ToArray();
}
catch (Exception ex) {}
return result;
}
该方法接受三个float[]
:in1
、in2
和in3
,并分别计算三个参数的平均数,返回一个新的float[]
。
DLL 调用 JS 方法
由于调用 DLL 方法时,通达信提供的是float
型的数组指针,我们需要将其转换为float
托管类型。在 TCalcFuncSet.cpp 内编写转换方法:
static array<float>^ ToManagedArray(float* pointer, int size) {
array<float>^ result = gcnew array<float>(size);
Marshal::Copy(IntPtr(pointer), result, 0, size);
return result;
}
在任意自定义方法内调用托管的JsTestClass::TestJs
方法,并处理返回:
void volatile TestPlugin2(int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc)
{
array<float>^ in1 = ToManagedArray(pfINa, DataLen);
array<float>^ in2 = ToManagedArray(pfINb, DataLen);
array<float>^ in3 = ToManagedArray(pfINc, DataLen);
DebugForm::JsTestClass^ jsTest = gcnew DebugForm::JsTestClass();
array<float>^ result = jsTest->TestJs(in1, in2, in3);
int size = result->Length;
Marshal::Copy(result, 0, IntPtr(pfOUT), size);
delete in1, in2, in3, jsTest, result;
}
通达信内调用
将上面编译好的 DLL 放在指定位置下(参考dll 最终放置位置/引用),启动通达信。新建以下的指标公式:
X1:TDXDLL1(2, H, L, C);
命名为“测试公式”,即可看到调用结果:
其他
实时更改 JS 脚本
实际使用过程中,JS 脚本可以通过 TextBox 控件实时更改,而非写死在代码中,如:
public float[] TestJs(float[] in1, float[] in2, float[] in3)
{
float[] result = new float[in1.Length];
try
{
var engine = new Engine();
engine.SetValue("in1", in1);
engine.SetValue("in2", in2);
engine.SetValue("in3", in3);
engine.Execute(textBox1.Text); // textBox1 是窗体编辑器控件,可在控件内输入 js 脚本
result = engine.GetValue("result").AsArray().Select(x => (float)x.AsNumber()).ToArray();
}
catch (Exception ex) {}
return result;
}
需要注意:上文的示例中,TestJs
方法所属类JsTestClass
是放在一个 DLL 中的,为了方便理解,engine.Exceute
直接调用了 textBox1 控件。
实际上为了避免因 窗体控件缓冲 引发的内存溢出,需要:
- 使用 管道通信 建立一个独立的外部应用程序,控件 textBox1 属于外部应用程序,而
JsTestClass
所属 DLL 也需要通过管道向外部应用程序请求到 textBox1 内的脚本,经由管道返回后,再编译和执行; - 或者:将
TestJsClass
放在外部应用程序中,tdxdll 通过管道传输参数至外部应用程序,外部应用程序调用自身 textBox1 获取并执行脚本后,将执行结果通过管道返回 tdxdll,再交由通达信主程序渲染。这可能涉及到管道的同步/异步处理,本文不表。
语法高亮控件
可以使用一些控件对代码进行高亮,如 WPF 的 AvalonEdit:
错误提示
对于 JS 错误(如语法错误)导致的执行失败,应捕获并给予提示,同时返回空结果,避免主程序卡死。