Skip to content

GinoMan/CSharpPythonBridgePOC

Repository files navigation

CSharpPythonBridgePOC

This is a proof of concept of communicating between C#.NET and Python 3.8 code by creating a DLL/DLLs that can then be called by Python code.

What does it do?

The current version of the python script shows a dialog box with a custom title and text. The C# library merely wraps Forms.MessageBox.Show(). It then is able to interpret the result of the call as an enum and compare the Enum to the result in python to see what button was clicked by the user. It also retrieves boolean values from C#, and passes and returns strings from C#. It displays them on the screen using the message box.

The enum itself is fully implemented but the dialog code will always return "DialogResult.OK" unless you close it at which point you'll get a different value. It is intended for the MessageBoxButtons and MessageBoxIcon to be implemented in the next release (See Roadmap) so that they can be passed along to the msgbox function and different values of DialogResult will be possible.

Known Issues/Notes

Only Strings, Ints, and Enums (as ints) are supported. Eventually the project will demonstrate marshalling of other types. See the "Roadmap" section for details.

The wrapper reverses the order of the Text and Caption parameters of the underlying function since they're both as of now required and it seemed more intuitive.

This doesn't work with multi-platform binaries. You need to compile a x64 and x86 version of the C# DLL and load the correct one at runtime.

Concept

Conceptually, the way the project works is it exposes C# static methods using the Conari and DllExport libraries which can be found on nuget. The exposure makes the functions and data types available to any language that can load C Dlls and call functions from those Dlls. Python, through it's CTypes library is able to do this.

So conceptually, it works like this:

Python <-> (C/C++) <-> C#.NET.

C/C++ is in parentheses because no direct C/C++ code need be written and at the time of this writing, there isn't any C/C++ code in the project. This same concept can eventually be used to wrap whole C#.NET libraries for use by CPython; potentially even GUI libraries like Windows Forms or WPF (Windows Presentation Foundation) without the need for IronPython. It may even facilitate the creation of a full blown Python GUI library for Windows.

Recommendation for Application

In applying the principles that this demonstrates, I recommend writing a Python package that creates a "pythonic" interface for the functionality provided by the C# library. Then package the DLL generated by the C# project and call into the C# library from that pythonic interface. Then import the library into your actual running script or library and use it like you would any other python library.

I can potentially see some possible issues arising however. For whatever reason, I needed to wrap "result" in DialogResult()'s c-tor. I'm sure this can be mitigated using some type introspection in the pythonic interface library that would be written and the python library would just handle the typing correctly, but it is still not perfect as of yet. I will be continuing to experiment with this.

Roadmap

  1. Implement and pass the Enums for MessageBoxButtons and MessageBoxIcon to the Dialog so that any kind of dialog can be shown and processed.
  2. Work on marshaling DateTime between the two languages.
  3. Try to return a string to python.
  4. ???
  5. $Profit?
  6. Create project template for Python Accessible C# libraries

Motivation for this

I wanted to create a python script that allowed me to easily and in bulk fix missing and incorrect song information in the id3 tags of a massive music library on my phone which I sometimes use as an MP3 player. The problem is that when you connect your phone to a windows computer, it doesn't mount the device as a drive in Windows such that you can directly access it via a path. Instead, you have to use the Media Transfer Protocol (MTP) to communicate with the device. Annoying but whatever.

The problem is that I wasn't finding any libraries for MTP on Windows specifically for Python. I could find one in nuget for C# that is fairly good and well documented, but not one for Python. I wanted to use the C# library but how do you do that from Python? I thought about using IronPython but it seems not to have been updated for a long time.

This led me to ask if C# libraries can be exposed for use by C since Python can use C libraries using the CTypes standard lib. It was then that I found Conari and DllExport. From there I worked on creating this proof of concept just to see if it would work to allow Python to call C# code and pass data back and forth and it does. By expanding this proof of concept, I will be able to demonstrate and prototype the techniques I intend to use to write a wrapper for this library in Python and C#.

Types Guide

WIP

This guide is a work in progress much like the rest of the concept, but the guide contains advice/documentation on how to wrap the types on the C# and Python sides.

String

Strings from Python are UTF-16 encoded, which means that every other byte of normal ASCII text will be the null byte (0x00). This means that wrapped methods/functions that expect a null-terminated string will stop after the first character. To mitigate this, function parameters that expect a string can be tagged with the [MarshalAs(UnmanagedType.LPWStr)] attribute.

To return a string from C# into Python, the C# method must return type IntPtr, but then return a Conari UnmanagedString of UnmanagedString.SType.Unicode. Also on the Python side, you will need to mark the imported function as returning a c_wchar_p by setting the appropriate method.restype (e.g. lib.getText.restype = c_wchar_p. This fully supports unicode.

Enum

Enums cannot be passed directly per-se. The reason for this is that they are a first class type as far as the CLI is concerned, and as far as Python is concerned, but not as far as C is concerned. C will automatically treat enums as ints in the generated assembly code and subsequent binary. The C Compiler checks that the value is only one of the defined values, but otherwise the underlying int could be any value that fits. The binary doesn't know the difference.

Because of this, Enums should be returned as ints where the enum is casted to int in the return value and the return type of the exposed function is int. This may have Type Safety ramifications but Python being flexibly typed doesn't really care except in comparisons for some reason as mentioned in "Recommendation for Application".

Pretty much any enum defined by .NET has documented numeric values that can directly be used in defining the enum in Python. If the enum returned or I suspect is passed with the same numeric value, then it means exactly the same in python as in C#. E.g. The DialogResult returned in the sample, will return the same numeric values that correspond to the .NET type. So by defining the Enum in Python with those same names and values, it can be compared to the result identically to how it would be compared in C#.

It should be noted that the names need not be identical, but it is best practice to do so unless doing so is impossible (for example, if one of the identifiers is a keyword like "None").

Int

Thankfully Integers are marshalled exactly as they are. No extra processing is required and they can be used as is. C# and DllExport are smart enough to pass them transparently.

Methods

The method that is being exported MUST be a public static method. Tagging the method with [DllExport] suffices in most cases to export the method for use by Python. However, certain types will have to be marshaled in some way.

Further testing has revealed that C# functions named in such a way that is the same as an object builtin function (like bool or repr) will not replace those functions. However, it can be done manually by giving them a different name and then wrapping it in a class in theory.

Bool

Booleans are marshalled completely transparently as well. Returning true from C# will evaluate to true in Python. Returning false from C# will evaluate to false in Python.

Special Thanks

Special thanks to @reflectronic on the C# discord for helping me with the marshaling of strings in C#.

About

This is a proof of concept of communicating between C# and Python code by creating a DLL/DLLs that can then be called by Python code.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors