我有个用C#编写的程序,需要提供其中的一部分功能给使用Python的同事使用,于是研究了一下如何在Python中调用C#的方法,特别是如何注册C#中的回调,这里记录一下。

使用Python for Net类库


使用Pythonnet类库可以在Python中调用C#编写的dll,要使用pythonnet首先要安装,安装方法很简单,在管理员权限下在命令行中,使用pip即可安装(pip在安装python的时候会自带,如果没有需要额外单独安装):

pip install pythonnet

安装完成之后,使用pip list查看一下:

C:\WINDOWS\system32>pip list
Package    Version
---------- -------
cffi       1.15.1
clr-loader 0.2.5
pip        23.0.1
pycparser  2.21
pythonnet  3.0.1
setuptools 41.2.0

表示pythonnet已经成功安装。

编写C#类库


为了演示,这里编写了一个简单的C#类库,类库名称为PythonCalled,生成的dll为“PythonCalled.dll”,里面有一个名为MyTest的类,内容如下:

using System;

namespace PythonCalled
{
    public class MyTest
    {
        public delegate void LogDelegate(string s);
        private event LogDelegate LogEvent;

        public void SetLogger(LogDelegate n)
        {
            LogEvent = n;
        }
 
        public void Print(string msg)
        {
            LogEvent?.Invoke($"Print(string msg) was called,msg: {msg}");
        }

        public double Add(double x, double y)
        {
            double result = x + y;
            LogEvent?.Invoke($"Add(double x, double y) was called, msg: {x}+{y}= {result}");
            return result;
        }
 
    }
}

这里面我定义了一个public类型的委托LogDelegate,然后定义了一个private的LogEvent,并且提供了一个public的SetLogger方法,用来设置回调,因为在pythonnet里似乎可以识别delegate但不识别event,所以这里没有直接定义一个Event:

public EventHandler<string> LogEvent;

如果这样定义,在python里面,无法给LogEvent赋值。C#代码写完之后编译成dll。这里必须要注意的是选用的.NET Framework版本,是.NET Frameowrk还是.NET Core。我这里选择的是.NET Core 5.0。

编写Python调用C#代码


我这里编写Python用的是自带的Python IDLE,Python是3.8版本,新建一个.py文件,然后编写以下代码:

import os
#如果是.net core必须加上下面两条语句
from pythonnet import load
load("coreclr")
import clr
import sys

#将当前.py文件所在的目录加到系统目录中来
sys.path.append(os.getcwd())

# 读取加载和引用dll
clr.FindAssembly("PythonCalled.dll")
dll = clr.AddReference("PythonCalled")
# 打印dll内容
print(dll)

from PythonCalled import *

def log_callback(arg):
    print(arg)

def print_hi(name):
    # 在下面的代码行中使用断点来调试脚本, 按 Ctrl+F8 切换断点。
    print(f'Hi, {name}')  
    # 实例化类
    instance = MyTest()
    logger = instance.LogDelegate(log_callback)
    instance.SetLogger(logger);
    # 有输入及无返回
    instance.Print("welcome to python call c#")
    # 有输入及输出
    add_data = instance.Add(1, 1)
    print(add_data)

# main函数入口。
if __name__ == '__main__':
    print_hi('yy')

需要的是,如果C#的类库使用的是.NET Core,那么在开头的地方必须要加上以下语句:

from pythonnet import load
load("coreclr")

另外,最好把C#编译后的dll和.py文件放在同一目录下,这样能减少加载dll时的路径查找错误。下面这句代码就是把当前.py文件所在的路径加入到系统路径中:

#将当前.py文件所在的目录加到系统目录中来
sys.path.append(os.getcwd())

代码里面,需要注意的是回调函数的设置。第一步要实例化一个代理,然后将代理设置到C#代码里面去:

logger = instance.LogDelegate(log_callback)
instance.SetLogger(logger);

log_callback,就是python里面的函数,签名需要跟C#里面代理一致,为了简化,这里都用string来作为参数类型,而没有用C#里面的自定义类型,建议简单话处理,就用string。

def log_callback(arg):
    print(arg)

回调方法很简单,就是将参数打印出来。

整个运行的结果如下:

可以看到,很完美的执行了方法及其回调。

如果上述代码运行出错,报"clr找不到”或者“FindAssembly”找不到的错误,那就是clr有冲突,因为python中有clr模块,pythonnet中也有clr,在import中需要使用的是pythonnet中的clr,所以只需要用pip uninstall clr把clr模块卸载掉,再次运行就可以了。

上面的例子已经验证过了在Python中调用C#的方法以及回调,所以接下来就可以应用到实际项目中了。

实际项目应用


现在有一个用C#编写的接收股票实时行情的程序,支持订阅。比如订阅某只股票的行情,该dll就可以通过事件回调返回股票的买卖十档MarketData和逐比成交TAS数据。首先新建了一个类QuoteManagerForPython.cs,里面清楚不必要的代码,专门供Python调用,基本的代码结构如下:

using BaseLib;
using DataFeed;
using Newtonsoft.Json;
using StockSubscription;
using System;

namespace TradeLib
{
    public class QuoteManagerForPython
    {
        private static ClientSubscription subscriber;
        public delegate void MarketDataUpdatedDelegate(string s);
        public delegate void TASUpdatedDelegate(string s);

        private static event MarketDataUpdatedDelegate MarketDataUpdated;
        private static event TASUpdatedDelegate TASUpdated;

        public static void SetMarketDataUpdateCallBack(MarketDataUpdatedDelegate m)
        {
            MarketDataUpdated = m;
        }

        public static void SetTASUpdatedCallBack(TASUpdatedDelegate t)
        {
            TASUpdated = t;
        }
 
        public static void Init(string addr)
        {
            if (!inited)
            {
                subscriber = new ClientSubscription(addr);
                subscriber.SetLogFunc(Logger.LogInfo);
                subscriber.MarketDataUpdated += MarketDataUpdated;
                subscriber.TASUpdated += TASUpdated;
                subscriber.Connect();
                inited = true;
            }
        }
 
        private static void MarketDataUpdated(MarketData md)
        {
            if (null != md)
            {
                FireMarketDataUpdated(md);
            }
        }

        private static void TASUpdated(TAS tas)
        {
            if (null != tas)
            {
                FireTASUpdated(tas);
            }
        }
 
        public static void SubscribeQuote(string code)
        {
            if (null != subscriber)
            {
                subscriber.SubscribeSymbol(code);
            }
        }
 
        private static void FireMarketDataUpdated(MarketData md)
        {
            if (null != MarketDataUpdated)
            {
                try
                {
                    MarketDataUpdated(JsonConvert.SerializeObject(md));
                }
                catch (Exception ex) { BaseLib.Logger.LogError(ex); }
            }
        }
 
        private static void FireTASUpdated(TAS tas)
        {
            if (null != TASUpdated)
            {
                try
                {
                    TASUpdated(JsonConvert.SerializeObject(tas));
                }
                catch (Exception ex) { BaseLib.Logger.LogError(ex); }
            }
        }
    }
}

以上代码略去了很多细节,主要的逻辑为:首先设置两个回调事件,它们分别来接收十档行情和逐比行情。然后调用Init方法,传入行情订阅服务器的地址。接下来就是调用SubscribeQuote方法,订阅需要接收的股票代码,当有新的行情来到时,就会触发回调。所有的逻辑实际都是在ClientSubscription这个类里面来完成,但这里这些细节不重要。

接下来编写python代码来演示如何通过调用和注册C#中的方法和事件来接收行情。需要注意的是.py文件需要放到上面的dll所在的目录下,因为这个dll比较复杂,它引用了许多其它的dll,这些都在同一目录下,将.py放在该目录下能减少clr加载dll出现错误的可能。

在.py中的完整代码如下:

import os
from pythonnet import load
#load("coreclr")
import clr
import sys
import json

sys.path.append(os.getcwd())
#print(os.getcwd())
# 读取dll文件
clr.FindAssembly(‘此处替换为QuoteManagerForPython所在的dll’)
dll = clr.AddReference(‘此处替换为QuoteManagerForPython所在的dll,不要.dll后缀’)
from TradeLib import *

def newMarketData_callback(arg):
#    j = json.loads(arg)
    print(arg)

def newTasData_callback(arg):
    print(arg)

def subscribe(symbol):
    QuoteManagerForPython.SubscribeQuote(symbol)

def init():
    #设置回调函数
    marketFunc=QuoteManagerForPython.MarketDataUpdatedDelegate(newMarketData_callback)
    tasFunc=QuoteManagerForPython.TASUpdatedDelegate(newTasData_callback)
    QuoteManagerForPython.SetMarketDataUpdateCallBack(marketFunc)
    QuoteManagerForPython.SetTASUpdatedCallBack(tasFunc)
    # 连接和初始化 tcp://ipaddress : port
    QuoteManagerForPython.Init('tcp://xxx.xxx.x.x:xxx')
    print('init success')
 
if __name__ == '__main__':
    init()
    subscribe('300059.SZ')
    os.system("pause")

代码非常简单,在python中定义了两个func:newMarketData_callback和newTasData_callback。在init方法中实例化了两个委托marketFunc和tasFunc,最后将这两个委托通过SetMarketDataUpateCallBack和SetTASUpdatedCallback两个方法设置到C#里面去。接下来调用Init方法,传入订阅服务器地址以初始化订阅。

最后通过subscribe方法,传入需要订阅的股票代码。在subscribe方法内部调用C#的SubscribeQuote方法来订阅行情。当有行情到来时,在两个回调func里就能直接处理行情数据了,运行结果如下:

可以看到,能够在python里定义的两个回调函数里面,接收到C#中的股票行情数据。

pythonnet调试


通常,在使用Python代码调用C#方法的时候,可能不会那么顺利,这就需要用到调试。这里也简单介绍一下使用Visual Studio附加进程调试pythonnet的方法。

首先第一步就是设置调试环境,在python代码中,设置断点,然后打开调试环境,在Python IDLE中,点击Run->Python Shell,然后在Python Shell中->Debug->Debugger,弹出Debug Control。

在Debug Control中,选中Source Global,然后点击Python IDLE中的Run->Run Module,运行程序,此时,第一个断点init方法会被命中,如下图:

现在在Debug Control中,点击Step,可以进入到init方法里,逐步运行。现在我们假设要调试init方法里面的QuoteManagerForPython.Init这个方法。现在我们回到Visual Studio中,附加Python的调试进程。在Visual Studio中,点击 调试->附加到进程,就会弹出附加进程界面,在可用进程里面选择pythonw.exe这个进程,注意,一定要选择类型为“托管”这个进行。

然后在Visual Studio中QuoteManagerForPython.cs类的Init方法中设置好断点。

接下来回到Python Debug Control,继续Step逐步运行:

可以看到,已经逐步运行到了Init这句代码,正在运行的代码行会以暗色条纹显示。再次点击Step执行这一句语句,可以看到Visual Studio中在Init方法处设置的断点已经命中:

现在就可以调试python往C#方法传递的参数以及方法执行的过程是否正确了。如果我们想继续调试subscribe方法,同理现在C#代码中设置断点,然后在Python Debug Control中我们继续执行Step或者直接点击Go,就会直接跳到python代码或者C#代码中的下一处断点。

调试外部调用C#程序的方法一般是采用附加线程的方法,比如Matlab调用C# dll,远程桌面调试C# dll等等。

 

参考: