通过 C++/CLI 建立 .NET Framework 混合的通达信指标 DLL 一文中,介绍了一种使用托管类的方式,来实现调用 .NET Framework 调试窗口的一种方法,同时也说明了该种方法可能会存在内存泄漏导致主窗体和托管窗体卡顿(见窗体控件缓冲)的问题。

在前文中,虽然指出了可以通过清理缓冲区的方式来临时解决问题,但具体的内存分配大小可能会因不同通达信版本而异,分析通达信分配的内存大小也较为不便。为了解决该问题,一种较为合理的方法便是将外部窗体或外部调用类独立成一个进程,主程序和外部进程之间使用管道通信,外部进程的内存交由操作系统来管理,而非交由通达信主程序管理,避免内存泄漏。这便引出了本文。

建立外部窗体应用程序

新建 .NET Core 窗体应用程序,设计如下的和前文类似的窗体:

外部调试应用程序窗体设计

在窗体代码中设计一个独立线程,用来:1. 建立管道流;2. 接收数据和打印日志。

using System.IO.Pipes;
using System.Text;

namespace PipelineApp
{
    public partial class Form1 : Form
    {
        readonly Thread pipelineThread;
        
        public Form1()
        {
            InitializeComponent();
            pipelineThread = new Thread(() =>
            {
                byte[] bytes = new byte[10];
                char[] chars = new char[10];
                Decoder decoder = Encoding.UTF8.GetDecoder();
                while (true)
                {
                    using (NamedPipeServerStream pipeStream =
                            new("messagepipe", PipeDirection.InOut, 1,
                            PipeTransmissionMode.Message, PipeOptions.None))
                    {
                        pipeStream.WaitForConnection();
                        pipeStream.ReadMode = PipeTransmissionMode.Message;
                        int numBytes;
                        do
                        {
                            string message = "";
                            do
                            {
                                numBytes = pipeStream.Read(bytes, 0, bytes.Length);
                                int numChars = decoder.GetChars(bytes, 0, numBytes, chars, 0);
                                message += new string(chars, 0, numChars);
                            } while (!pipeStream.IsMessageComplete);

                            decoder.Reset();
                            if (!string.IsNullOrEmpty(message))
                                Invoke(new MethodInvoker(() => PrivateLog(message)));
                        } while (numBytes != 0);
                    }
                }
            })
            { IsBackground = true };
            pipelineThread.Start();

        }

        private void PrivateLog(string message)
        {
            if (textBox1.TextLength + message.Length > textBox1.MaxLength * 4)
            {
                textBox1.Text = "";
            }
            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 = "";
        }
    }
}

生成项目,将 PipelineApp.exe、PipelineApp.dll 和 Pipeline.runtimeconfig.json 三个文件放在通达信目录下。这样一个外部日志窗口程序就准备好了。

编写 C++/CLI 相应方法

在''TCalcFuncSets.h''里声明如下方法:

void LogDequeue();

在''TCalcFuncSets.cpp''里声明如下的全局类和方法:

ref class GlobalObjects {
public:
    static ConcurrentQueue<String^>^ logQueue = gcnew ConcurrentQueue<String^>();
    static Thread^ thread = gcnew Thread(gcnew ThreadStart(&LogDequeue));
};

void LogDequeue() {
    NamedPipeClientStream^ clientStream;
    while (true) {
        try {
            clientStream = gcnew NamedPipeClientStream("messagepipe");
            clientStream->Connect();
            while (true) {
                String^ log;
                if (GlobalObjects::logQueue->TryDequeue(log)) {
                    array<Byte>^ buffer = Encoding::UTF8->GetBytes(log);
                    clientStream->Write(buffer, 0, buffer->Length);
                    clientStream->Flush();
                }
                else {
                    Thread::Sleep(1);
                }
            }
        }
        catch (Exception^ e) {
            // 应对外部应用程序关闭了的情况
            if (clientStream != nullptr) {
                clientStream->Close();
            }
        }
    }

}

static void Log(System::String^ message) {
    GlobalObjects::logQueue->Enqueue(message);
}

BOOL RegisterTdxFunc(PluginTCalcFuncInfo** pFun)
{
    // 启动外部程序和线程
    Process^ debugProcess = gcnew Process;
    debugProcess->StartInfo->FileName = "PipelineApp.exe";
    debugProcess->Start();
    GlobalObjects::thread->IsBackground = true;
    GlobalObjects::thread->Start();
    Log("DLL loaded.");
    if (*pFun == NULL)
    {
        (*pFun) = g_CalcFuncSets;
        return TRUE;
    }
    return FALSE;
}

最终效果

启用通达信,外部程序便被挂载,输出日志。

管道测试结果

即使关闭外部应用程序,主程序也未见卡顿,证明管道连接断开的异常已被捕获,并开始准备下一次的连接。重新手动打开外部应用程序,又能继续接收到日志。完成任务。