Skip to content

Instantly share code, notes, and snippets.

@vadimkantorov
Last active November 22, 2023 03:01
Show Gist options
  • Save vadimkantorov/c9ab97aff995ea01ea7f6248ddc733bc to your computer and use it in GitHub Desktop.
Save vadimkantorov/c9ab97aff995ea01ea7f6248ddc733bc to your computer and use it in GitHub Desktop.
Run a C# function from Python using Python.NET from https://pythonnet.github.io/pythonnet/python.html Call csharpfrompython.py which first calls the compiler and then calls the functions, feature request of better compiler bindings at https://github.com/pythonnet/pythonnet/issues/2196 ; includes two examples of NumPy -> C# array marshalling
namespace CSharpFromPythonNamespace
{
public class CSharpFromPythonClass
{
public string Hi(string x)
{
return "Hi " + x;
}
public static string Hello(string x)
{
return "Hello " + x;
}
public static string World(System.ReadOnlyMemory<float> memory)
{
return memory.ToString() + ':' + string.Join(", ", memory.ToArray());
}
}
// https://stackoverflow.com/a/52191681/445810
public sealed unsafe class UnmanagedMemoryManager<T> : System.Buffers.MemoryManager<T> where T : unmanaged
{
public UnmanagedMemoryManager(System.Int64 pointer, System.Int32 length)
{
this._pointer = (T*)pointer;
this._length = length;
}
public System.ReadOnlyMemory<T> ReadOnlyMemory => this.Memory;
public override System.Span<T> GetSpan() => new System.Span<T>(this._pointer, this._length);
public override System.Buffers.MemoryHandle Pin(System.Int32 elementIndex = 0) => new System.Buffers.MemoryHandle(this._pointer + elementIndex);
public override void Unpin() { }
protected override void Dispose(bool disposing) { }
private readonly T* _pointer;
private readonly System.Int32 _length;
}
}
# https://pythonnet.github.io/pythonnet/python.html
# wget https://dot.net/v1/dotnet-install.sh && bash dotnet-install.sh --channel 7.0
# python -m pip install pythonnet --user
# DOTNET_ROOT="$HOME/.dotnet" PYTHONNET_RUNTIME=coreclr python -m clr
import os
import tempfile
import subprocess
import ctypes
os.environ['DOTNET_ROOT'] = os.path.expanduser('~/.dotnet')
os.environ['PYTHONNET_RUNTIME'] = 'coreclr'
import clr
def asNetArray(npArray):
# https://github.com/pythonnet/pythonnet/issues/514#issuecomment-350375105
#Given a `numpy.ndarray` returns a CLR `System.Array`. See _MAP_NP_NET for
#the mapping of Numpy dtypes to CLR types.
#Note: `complex64` and `complex128` arrays are converted to `float32`
import numpy as np
import System
#and `float64` arrays respectively with shape [m,n,...] -> [m,n,...,2]
_MAP_NP_NET = {
np.dtype('float32'): System.Single,
np.dtype('float64'): System.Double,
np.dtype('int8') : System.SByte,
np.dtype('int16') : System.Int16,
np.dtype('int32') : System.Int32,
np.dtype('int64') : System.Int64,
np.dtype('uint8') : System.Byte,
np.dtype('uint16') : System.UInt16,
np.dtype('uint32') : System.UInt32,
np.dtype('uint64') : System.UInt64,
np.dtype('bool') : System.Boolean,
}
dims = npArray.shape
dtype = npArray.dtype
# For complex arrays, we must make a view of the array as its corresponding
# float type.
if dtype == np.complex64:
dtype = np.dtype('float32')
dims.append(2)
npArray = npArray.view(np.float32).reshape(dims)
elif dtype == np.complex128:
dtype = np.dtype('float64')
dims.append(2)
npArray = npArray.view(np.float64).reshape(dims)
netDims = System.Array.CreateInstance(System.Int32, npArray.ndim)
for I in range(npArray.ndim):
netDims[I] = System.Int32(dims[I])
if not npArray.flags.c_contiguous:
npArray = npArray.copy(order='C')
assert npArray.flags.c_contiguous
try:
netArray = System.Array.CreateInstance(_MAP_NP_NET[dtype], netDims)
except KeyError:
raise NotImplementedError("asNetArray does not yet support dtype {}".format(dtype))
try: # Memmove
destHandle = System.Runtime.InteropServices.GCHandle.Alloc(netArray, System.Runtime.InteropServices.GCHandleType.Pinned)
sourcePtr = npArray.__array_interface__['data'][0]
destPtr = destHandle.AddrOfPinnedObject().ToInt64()
ctypes.memmove(destPtr, sourcePtr, npArray.nbytes)
finally:
if destHandle.IsAllocated: destHandle.Free()
return netArray
cs_paths = [os.path.abspath('csharpfrompython.cs')]
dll_path = tempfile.mkstemp(suffix = '.dll')[-1] # os.path.abspath('csharpfrompython.dll')
compile_first_and_overlink_all_available_reference_assemblies = True
if compile_first_and_overlink_all_available_reference_assemblies:
#DOTNET_ROOT="$HOME/.dotnet"
#DOTNETSDKVER=$("$DOTNET_ROOT/dotnet" --version)
#DOTNETFWKVER=$("$DOTNET_ROOT/dotnet" --list-runtimes | grep Microsoft.NETCore.App | tail -n 1 | cut -d' ' -f2)
#DOTNETLIBDIR="$DOTNET_ROOT/shared/Microsoft.NETCore.App/$DOTNETFWKVER"
#"$DOTNET_ROOT/dotnet" "$DOTNET_ROOT/sdk/$DOTNETSDKVER/Roslyn/bincore/csc.dll" $(find "$DOTNETLIBDIR" -name "*.dll" -printf '-r:"%p" ') -target:library -out:csharpfrompython.dll csharpfrompython.cs
DOTNETSDKVER = subprocess.check_output([os.path.join(os.environ['DOTNET_ROOT'], 'dotnet'), '--version']).decode().strip()
DOTNETFWKVER = [line.split()[1] for line in subprocess.check_output([os.path.join(os.environ['DOTNET_ROOT'], 'dotnet'), '--list-runtimes']).decode().splitlines() if 'Microsoft.NETCore.App' in line][-1]
DOTNETLIBDIR = os.path.join(os.environ['DOTNET_ROOT'], 'shared', 'Microsoft.NETCore.App', DOTNETFWKVER)
csc_args_reference_assembly_paths = ['-r:' + os.path.join(dirname, basename) for dirname, dirs, basenames in os.walk(DOTNETLIBDIR) for basename in basenames if basename.endswith('.dll')]
csc_args_main = ['/unsafe', '-target:library', '-out:' + dll_path] + cs_paths # /unsafe needed for UnmanagedMemoryManager which is needed for zero-copy NumPy->C# array marhsalling
csc_args = csc_args_reference_assembly_paths + csc_args_main
clr.AddReference(os.path.join(os.environ['DOTNET_ROOT'], 'sdk', DOTNETSDKVER, 'Roslyn', 'bincore', 'csc.dll'))
import Microsoft.CodeAnalysis.CSharp.CommandLine
# print(csc_args)
assert 0 == Microsoft.CodeAnalysis.CSharp.CommandLine.Program.Main(csc_args)
clr.AddReference(dll_path)
os.remove(dll_path) # seems we can delete the assembly even before usage
import CSharpFromPythonNamespace, System
print(CSharpFromPythonNamespace.CSharpFromPythonClass().Hi('world')) # does print "Hi world"
print(CSharpFromPythonNamespace.CSharpFromPythonClass.Hello('you')) # does print "Hello you"
import numpy as np
arr = np.array([1,2,3], dtype = np.float32)
print('reference python output:', arr)
print('the copying way: NumPy array => Python array => C# array => ReadOnlyMemory<float>', CSharpFromPythonNamespace.CSharpFromPythonClass.World(System.ReadOnlyMemory[System.Single](System.Array[System.Single](arr.tolist()))))
print('yet another way: NumPy array => C# array => ReadOnlyMemory<float>', CSharpFromPythonNamespace.CSharpFromPythonClass.World(System.ReadOnlyMemory[System.Single](asNetArray(arr))))
ptr = arr.__array_interface__['data'][0]
cnt = arr.__array_interface__['shape'][0]
mgr = CSharpFromPythonNamespace.UnmanagedMemoryManager[System.Single](ptr, cnt)
print('the zero-copy way: NumPy array => raw pointer => C# pointer => custom UnmanagedMemoryManager<float> => Memory<float> => ReadOnlyMemory<float>', CSharpFromPythonNamespace.CSharpFromPythonClass.World(mgr.ReadOnlyMemory))
print('reference python output one more time, to ensure that array was not garbage-colected:', arr)
# prints:
# reference python output: [1. 2. 3.]
# the copying way: NumPy array => Python array => C# array => ReadOnlyMemory<float> System.ReadOnlyMemory<Single>[3]:1, 2, 3
# yet another way: NumPy array => C# array => ReadOnlyMemory<float> System.ReadOnlyMemory<Single>[3]:1, 2, 3
# the zero-copy way: NumPy array => raw pointer => C# pointer => custom UnmanagedMemoryManager<float> => Memory<float> => ReadOnlyMemory<float> System.ReadOnlyMemory<Single>[3]:1, 2, 3
# reference python output one more time, to ensure that array was not garbage-colected: [1. 2. 3.]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment