Can one write a very straightforward C# reloader akin to this Java reloader, that enables Mathematica-like usage of functions from existing C# code in string format, in an easy and elegant fashion directly in Mathematica?
Answer
Yes it is possible to write such a code and I will present the code and sample usage below. The motivation for this question/answer is this Java reloader by Leonid Shifrin and I borrowed some code from his answer.
Simplistic C# Reloader
The code below is a C# reloader. It takes a string of C# code and (optionally) a C# compiler path, attempts a compile and if the compilation succeeds it calls on NETLink
to load the DLL
. It also takes the same Option
s as LoadNETType
, making it easy to choose how you want the loaded functions to appear.
Code
BeginPackage["DotNETReloader`", {"NETLink`"}];
dotNETLoader::usage =
"dotNETLoader[code_, compilerPath_] takes a C# code in string format
and (optionally) a C# compiler path, it then attempts to compile the
string code into a .NET dll suitable for use with NETLink"
dotNETLoader::dirmakeerr = "Can not create directory `1`";
dotNETLoader::invst = "The loader is not in a valid state. Perhaps some temporary
directories do not exist or could not be created";
dotNETLoader::cmperr = "The following compilation errors were encountered: \n`1`";
Options[dotNETLoader] = Options[LoadNETType]
Begin["`Private`"]
$stateValidQ = True;
$csc = If[$SystemID === "Windows-x86-64", "csc.exe", "mcs"];
$err = If[$SystemID === "Windows-x86-64", 2, 3];
$tempDirectory = FileNameJoin[{$UserBaseDirectory, "Temp", "dotNET"}];
(* If a namespace exists, get the name *)
getNamespace[code_String] :=
With[{ns =
StringCases[code, "namespace" ~~ Whitespace ~~ name : (WordCharacter ..) :> name]},
First @ ns /; ns =!= {}]
(* Get the class name *)
getClass[classCode_String] :=
With[{cl =
StringCases[classCode,
"public" ~~ Whitespace | (Whitespace ~~ "static" ~~ Whitespace) ~~
"class" | "struct" | "interface" ~~ Whitespace ~~
name : (WordCharacter ..) :> name]}, First @ cl /; cl =!= {}]
getClass[__] := Throw[$Failed, error[getClass]];
If[!FileExistsQ[#] && CreateDirectory[#] === $Failed,
Message[dotNETLoader::dirmakeerr, #]; $stateValidQ = False;] &@$tempDirectory
dotNETLoader[code_String, compiler_String: $csc,
opts : OptionsPattern[]] /; $stateValidQ :=
Module[{sourcePath, run,
options = FilterRules[{opts}, Options[dotNETLoader]],
namespace = getNamespace[code], className = getClass[code]},
sourcePath = FileNameJoin[{$tempDirectory, className <> ".cs"}];
Export[sourcePath, code, "String"];
run = RunProcess[{compiler, "-t:library", "-optimize+", sourcePath},
ProcessDirectory -> $tempDirectory];
If[run[[1]] =!= 0, Message[dotNETLoader::cmperr,
Style[#, Red]& @ run[[$err]]]; $Failed,
ReinstallNET[];
LoadNETAssembly @ FileNameJoin[{$tempDirectory, className <> ".dll"}];
If[Head @ namespace =!= getNamespace,
LoadNETType[namespace <> "." <> className, options],
LoadNETType[className, options]]
]
]
dotNETLoader[_String] := CompoundExpression[Message[dotNETLoader::invst]; $Failed]
End[]
EndPackage[]
Notes
I have tested this code successfully on both Windows and Linux and would appreciate it if someone that has a Mac can also test it on MacOS (I won't be able to do this but I expect it to work unaltered since the .NET platform and C# compiler are available via Mono). Also, please I will like others to test this code and provide feedback on all platforms. You will notice that I have named the function dotNETLoader
instead of cSharpLoader
, this is because I'm planning on extending this function to include F#
(which I have successfully tested on Windows) and possibly, other .NET languages.
Due to my usage of RunProcess
to avoid bringing up the console in Windows, this will only work on Mathematica 10 and above versions.
The package works by saving the C# string code and saving it as a .cs
file in a temporary directory and using the New in 10 function RunProcess
to invoke your installed C# compiler to compile the class into a .NET library .dll
file. It then calls NETLink
to load the library and make the functions available for use in Mathematica.
Usage
I will show some examples of how to use this package. Let's start by borrowing some C# codes from the web and saving them as strings.
code1 =
"public class LCS{
public static string longestCommonSubstring(string s1, string s2)
{
int Start = 0;
int Max = 0;
for (int i = 0; i < s1.Length; i++){
for (int j = 0; j < s2.Length; j++){
int x = 0;
while (s1[i + x] == s2[j + x]){
x++;
if (((i + x) >= s1.Length || ((j + x) >= s2.Length)))
break;
}
if (x > Max) {
Max = x;
Start = i;
}
}
}
return s1.Substring(Start, Max);
}
}";
This code is the C# equivalent of the 2nd code from here (There are only a few differences but it took less than a minute to make the changes to C# code)
First we load the package:
Needs["DotNETReloader`"]
Now we load the function using dotNETLoader
:
dotNETLoader[code1, StaticsVisible -> True]
NETType["LCS", 1]
We can see the possible options:
Options[dotNETLoader]
{StaticsVisible -> False, AllowShortContext -> True}
Setting StaticsVisible
to True
enables us to call the function directly without the class name and makes it feel like a built-in function.
longestCommonSubstring["AAABBBBCCCCC","CCCBBBAAABABA"]
"AAAB"
I'll add the same benchmark Leonid used in his Java reloader just to show that in some cases it may be better to use C#:
s1 = StringJoin@RandomChoice[{"A", "C", "T", "G"}, 10000];
s2 = StringJoin@RandomChoice[{"A", "C", "T", "G"}, 10000];
Here we use the Mathematica's built-in function LongestCommonSubsequence
:
LongestCommonSubsequence[s1, s2] // RepeatedTiming
{0.25, "GTCCCTCACACTG"}
And here is our loaded C# function:
longestCommonSubstring[s1, s2] // RepeatedTiming
{0.35, "GTCCCTCACACTG"}
This collatz optimization question is interesting, so I decided to see how our new C# reloader can help us here. I found this code online a while ago (can't remember where) and made just one simple change to parallelize it.
code2 =
"using System.Threading.Tasks;
namespace Tester
{
public struct Calc
{
public static int[] ParallelCollatz(int[] list)
{
Parallel.For(0, list.Length, i =>
{
int count;
long k = list[i];
for (count = 1; k != 1; count++)
{
k = ((k & 1) == 0) ? k / 2 : 3 * k + 1;
}
list[i] = count;
});
return list;
}
}
}";
As before (load the package if not already loaded), we load the function using dotNETLoader
dotNETLoader[code2, StaticsVisible -> True]
NETType["Tester.Calc", 2]
I wanted to point out a few things about the package with this example. First, notice that the type is Tester.Calc
instead of just Calc
, this is becaude this class was placed in a namespace, but the reloader notices this and takes care of it for us. Secondly, the way we use the feature in conjuction with built-in Mathematica functions to solve the problem shows how natural the approach becomes. Finally, this will also work for structs
as well as static classes. Back to the example:
int = Range[1*^6];
(* Here ParallelCollatz is the loaded C# function *)
RepeatedTiming @ Ordering[ParallelCollatz @ int, -1]
{0.0610, {837799}}
I will compare this to the compiled-to-C function provided by DumpsterDoofus.
Ordering[collatzLength @ int, -1] // RepeatedTiming
{0.877, {837799}}
Summary
We see that this C# Reloader can be quite useful and fits naturally into the Mathematica-style usage scenario. In the first example the highly optimized built in function was only slightly faster than our borrowed-from-web code. What's interesting from my testing so far is that there's not much performance difference when using these functions on both Windows and Linux. For the second example where I compared brute force approaches, we see that the loaded function was over a magnitude faster.
Comments
Post a Comment