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[]in1in2in3,并分别计算三个参数的平均数,返回一个新的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函数测试.png

其他

实时更改 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

avalonedit.png

错误提示

对于 JS 错误(如语法错误)导致的执行失败,应捕获并给予提示,同时返回空结果,避免主程序卡死。