Recently I encountered the issue of having to call some functions from an third-party library, which is available as .NET DLL only (is it called “assembly” in .NET jargon?). It doesn’t worth the effort to learn and write code in C# just to be able to use the library. So ways to access the .NET DLL in Python has been investigated.
It turns out, there are two ways to hook up .NET and Python:
- use IronPython, a Python environment implemented in .NET
- use pythonnet, the Python for .NET module in CPython
Because of compatibility to other Python libraries, it should be better to use the latter. However, it seems to me that their syntax are the same and therefore the code are interchangeable between the two.
Bootstrap
Below is an example code:
# load Python.NET
import clr
# the CLR namespaces are now recognized as Python packages
from System import String
from System.Collections import *
from System import Environment
drives = Environment.GetLogicalDrives()
# Load an assembly by "AddReference"
clr.AddReference('System.Speech')
from System.Speech.Synthesis import SpeechSynthesizer
spk = SpeechSynthesizer()
spk.Speak('Hello world!')
In the above, the AddReference()
call allows using C# modules (assemblies).
This is the way to do it because the .DLL file for .NET and that for
traditional Windows are different. In order to generate the “old style” DLL
that we can use ctypes
to access, we have to compile the C# code using csc
.
The AddReference()
call will find the DLL in some locations. To hint where to
find the DLL, we can do either of the following:
# method 1: use AddReferenceToFileAndPath()
import clr
clr.AddReferenceToFileAndPath(r"C:\Folder\Subfolder\file.dll")
# method 2: append the path to sys.path
import clr
import sys
sys.path.append(r"C:\Folder\Subfolder") # path of dll
clr.AddReference("file.dll") # the dll
Afterwards the .NET namespaces are available as Python modules.
However, I found that Python for .NET works under native Windows but not under cygwin (which somewhat reasonable because under cygwin, it try to pretend as much as a Linux). If we write a code that may be (accidentally) run under cygwin, we can check against the case with:
import platform
print(platform.python_implementation())
Also we can put path for DLL files in sys.path
but mounted drive letter are
not supported (quite sure this is the issue with Python for .NET module, not
the python interpreter). In case a mounted drive letter is in the path, e.g.
M:\Lib
, calling clr.AddReference()
will cause the error of “Unable to find
assembly” even if some other path after the mounted drive letter provides the
correct DLL.
Allowing ctypes
to use the .NET code
The traditional ctypes
module cannot load those DLL is because they are not
“COM visible”
and ctype
cannot access a .NET assembly. The DLL file is COM visible only if
we set the following attribute in the C# code:
[ComVisible(true)]
or better, we explicitly expose the function. Below is an example of how to do it (in C#):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using RGiesecke.DllExport;
class Test
{
[DllExport("add", CallingConvention = CallingConvention.Cdecl)]
public static int TestExport(int left, int right)
{
return left + right;
}
}
The above code must set, in Visual Studio, the Settings → Build → Platform
Target to “x86” or “x64”, but not “Any CPU”. Then the generated DLL can be
called using ctypes
:
import ctypes
a = ctypes.cdll.LoadLibrary(source)
a.add(3, 5)
Of course, we cannot go with this approach if we do not have the source code. But the following pages describe how to write a wrapper over C# DLL so that we can use in Python:
- https://stackoverflow.com/a/42930903/9214517
- http://pragmateek.com/if-your-plumbing-doesnt-work-youre-just-not-using-enough-pipes/
- https://stackoverflow.com/questions/39456464/calling-c-sharp-from-python
- https://stackoverflow.com/questions/33285732/how-to-import-a-c-sharp-dll-to-python
In summary, C# code should have function marked [DllExport]
or more verbose,
[DllExport("add", CallingConvention = CallingConvention.Cdecl)]
to specify the exact name exposed as opposed to the function name. Then Python
can use ctypes.WinDLL("path/to/myfile.dll")
to load the DLL
Executing Python in C#
In the reverse direction, Python for .NET also allows C# code to access Python. For example the following code is from Python for .NET readme:
static void Main(string[] args)
{
using (Py.GIL())
{
dynamic np = Py.Import("numpy");
Console.WriteLine(np.cos(np.pi * 2));
dynamic sin = np.sin;
Console.WriteLine(sin(5));
double c = np.cos(5) + sin(5);
Console.WriteLine(c);
dynamic a = np.array(new List<float> { 1, 2, 3 });
Console.WriteLine(a.dtype);
dynamic b = np.array(new List<float> { 6, 5, 4 }, dtype: np.int32);
Console.WriteLine(b.dtype);
Console.WriteLine(a * b);
Console.ReadKey();
}
}
Specific syntax
Python and C# are different languages. And .NET system do not have an exact language mapping to Python syntax. Some nice thing in C#, such as iterable data structures, are trivially mapped as iterables in Python, but others are not directly mapped. Below I list out some aspects that we need a special syntax in Python in order to access to the C# library:
data type
There are different data type systems in .NET than in Python. Some data type
will be converted automatically when we use it, but some do not. One example
is the Decimal
type, which we have to do conversion manually:
# Decimal to int:
n = Deciaml.ToInt32(decimal_obj)
assert isinstance(n, int)
# Convert to string:
s = decimal_obj.ToString()
assert isinstance(s, str)
Because Decimal
is a class, reverse the above conversion is as simple as
decimal_obj = Decimal(n)
.
generics
C# has the “generics” syntax (mimicking C++ templates) and such code in C#:
EventHandler<EventArgs>(); // C# code
becomes this in Python:
EventHandler[EventArgs]() # Python code
output variables
As an example, the DateTime.TryParse()
function in C# has the following
signature:
public static bool TryParse(
string s,
out DateTime result
)
which means the function will write something in result
(must be a lvalue)
and at the same time return another value from the function. Python does not
have the concept of output variable in the function. Therefore the above
function will be transformed to return a tuple of (bool, DateTime)
:
ok, parsed = DateTime.TryParse("2018-01-01 12:01:00", DateTime())
To match the function signature, we must provide a dummy DateTime()
as the
second argument to the function TryParse()
. But the result
that supposed to
be written into now becomes the second element of the returning tuple. The
one provided as function argument, however, will never be used. The first
element of the tuple is always the original return value of the function
according to the C# function signature. Simply speaking, If the function has
\(n\) output variables, the return will be a \((n+1)\)-tuple. Python for .NET
requires the function prototype to match exactly so that in case of overloaded
function, it can ensure which one to invoke.
delegates
According to this SO question and the doc, the following is the way to pass a python function to a C# function as “delegates”:
def my_handler(source, args):
print 'my_handler called!'
# instantiate a delegate
d = AssemblyLoadEventHandler(my_handler)
# use it as an event handler
AppDomain.CurrentDomain.AssemblyLoad += d
Delegate is the C# way to call a function object. C# Events, however, can do implicit conversion on the delegates that are supposed to be event handlers:
def handler(source, args):
print 'my_handler called!'
# register event handler
object.SomeEvent += handler
# unregister event handler
object.SomeEvent -= handler
# fire the event
result = object.SomeEvent(...)
References
- Documentation in sourceforge or a tutorial on Github
- https://stackoverflow.com/questions/4350191/c-sharp-scripting-python
- https://stackoverflow.com/questions/1200182/how-to-use-a-c-sharp-dll-in-ironpython
- https://stackoverflow.com/questions/14633695/how-to-install-python-for-net-on-windows
- Repository for Windows python binary: https://www.lfd.uci.edu/~gohlke/pythonlibs/#pythonnet
- https://stackoverflow.com/questions/34662448/net-c-interop-to-python
- IronPython cookbook
- http://ironpython.net/
- https://notebooks.azure.com/api/user/denfromufa/library/pythonnet/html/Python.NET.ipynb