Welcome! Please see the About page for a little more info on how this works.

0 votes
in ClojureCLR by

I am porting a C# application (AutoCAD plugin to be exact) from .NET 4.8 to .NET 8.

The code uses some Clojure CLR.
In .NET 4.8 the clojure namespaces were compiled to assemblies (.dll files) using Clojure.Compile.
At runtime, these namespaces were then loaded using clojure.lang.RT.load

However, precompiling clojure clr code is not possible in .NET 8. (https://ask.clojure.org/index.php/13866/is-still-possible-compile-cljr-files-dll-exe-dotent-and-above)

If I understood correctly, the recommended approach is to include the clojure sources with the application and then load the clojure namespaces using clojure.clr.api.Clojure class?
I could not find any examples or documentation on how to set up my application / environment so that clojure.core/load will find my .clj files.

On page
https://github.com/clojure/clojure-clr/wiki/Using-ClojureCLR-in-a-C#-project
there is instructions on how to invoke clojure.core/load

IFn load= clojure.clr.api.Clojure.var("clojure.core", "load");
load.invoke("some.thing");

However, clojure.core/load is documented to use CLASSPATH to find namespaces. Does this also apply in Cojure CLR, i.e. should I define an appropriate CLASSPATH environment variable before calling clojure.core/load ?
https://clojuredocs.org/clojure.core/load

by
Having started w/ this I did not yet get to the point where the application's own Clojure sources (or compiled binaries if that were possible) would be needed. Initializing Clojure itself seems to have problems:

clojure.clr.api.Clojure.var method (being the first method invoked from Clojure library) throws an exception. There are a couple of inner exceptions, the root cause is

    {"Could not locate clojure.core.clj.dll, clojure.core.cljc.dll, clojure/core.clj or clojure/core.cljc on load path.":null}    System.Exception {System.IO.FileNotFoundException}
Whose stack trace shows it was thrown from RT.load :
        StackTrace    "   at clojure.lang.RT.load(String relativePath, Boolean failIfNotFound)\r\n   at clojure.lang.RT.load(String relativePath)\r\n   at clojure.lang.RT..cctor()"    string

I am using the Clojure nuget 1.11.0 on .NET 8. Do I need to include something else as well? Or where might I be going wrong?
by
Hi Antti, did you ever resolve this issue?
I'm having similar issues trying to get something working for BricsCAD.

thanks,
Mick
ago by
Hi Michael,

Yes, I got it working. I had to build ClojureCLR from master, the current release has a showstopper bug. I don't anymore recall the details, but there was an uncaught exception at startup.

If you have a specific problem, I can try to help if it is anything similar to what I already tackled.


    .::Antti::.
ago by
Thanks for the reply, with a bit of help from AI I located the same problem and fixed it. It's to do with ClojureCLR not running in the same process/location as the BricsCAD host so the path's turn up blank.
I'll reply to David's message with the details, cheers.

1 Answer

+1 vote
ago by

clojure.core/load calls RT.load as seen in the stack trace.
The set of probe paths for RT.load is

  • the directory of the domain of the thread that is running (domains really being more relevant to .NET Framework at this point)
  • the current directory
  • the directory where Clojure.dll is located
  • the directory of the entry assembly
  • all directories listed as the value of the environment variable CLOJURE_LOAD_PATH
    • this is equivalent to the classpath in Java-land.
    • separator character is System.IO.Path.PathSeparator (; on Windows)

Clearly this is not sufficiently documented. Will work on that.

ago by
Hi David, with the help of AI and some testbed app's I located the problem in the static RT() at line 469, a detailed report below:

Issue:

RT..cctor fails with ArgumentException when hosted in a native process (e.g. BricsCAD, AutoCAD) due to AppDomain.CurrentDomain.BaseDirectory returning empty string
Problem Description
When ClojureCLR is loaded as a plugin inside a native C++ host process (such as BricsCAD or AutoCAD) via their .NET plugin APIs, RT..cctor throws:

System.ArgumentException: Path "Clojure.Source.dll" is not an absolute path. (Parameter 'path')
  at System.Reflection.Assembly.LoadFile(String path)
  at clojure.lang.RT..cctor()

Root Cause
In Clojure/Clojure/Lib/RT.cs, the static constructor locates Clojure.Source.dll using:

csharpstring baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
try {
    Assembly.LoadFile(Path.Combine(baseDir, "Clojure.Source.dll"));
}
catch (FileLoadException) { }
catch (FileNotFoundException) { }

When .NET is hosted inside a native C++ process, AppDomain.CurrentDomain.BaseDirectory returns an empty string rather than the application directory. This causes Path.Combine("", "Clojure.Source.dll") to return the bare relative filename "Clojure.Source.dll". On .NET 8, Assembly.LoadFile with a relative path throws ArgumentException immediately — and crucially, ArgumentException is not in the catch list, so it propagates and permanently faults the RT type initializer.

On .NET Framework this worked accidentally because LoadFile with a relative path resolved against Environment.CurrentDirectory. On .NET 8 this was tightened to require absolute paths, exposing the latent bug.

Why console apps are unaffected
In a normal console app, AppDomain.CurrentDomain.BaseDirectory correctly returns the executable's directory, so Path.Combine produces a valid absolute path and LoadFile succeeds.

Fix
Replace AppDomain.CurrentDomain.BaseDirectory with typeof(RT).Assembly.Location, which always returns the correct absolute path to Clojure.dll regardless of how the CLR is hosted:

// Before:
string baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);

// After:
string baseDir = Path.GetDirectoryName(typeof(RT).Assembly.Location);

Optionally also add ArgumentException to the catch block as a secondary safety net:

try {
    Assembly.LoadFile(Path.Combine(baseDir, "Clojure.Source.dll"));
}
catch (ArgumentException) { }    // add this - relative path in hosted contexts
catch (FileLoadException) { }
catch (FileNotFoundException) { }

File to change
Clojure/Clojure/Lib/RT.cs — the static constructor RT()

Why typeof(RT).Assembly is the correct fix

typeof(RT).Assembly always refers to Clojure.dll itself — the assembly that contains Clojure.Source.dll alongside it
It is unaffected by how the CLR is hosted or what the current process is
AppDomain.BaseDirectory is a process-level concept that is meaningless when .NET is a guest inside a native host
This is consistent with how other assemblies in the codebase locate their resources

Environment

ClojureCLR NuGet package version: 1.12.2
.NET version: net8.0
Host process: BricsCAD V26 (native C++ exe hosting .NET 8 via plugin API)
Platform: Windows x64

hope that helps, thanks for all of your hard work, cheers

Mick
ago by
Yours is the first report I've heard of someone using ClojureCLR with .NET hosted inside a native app.  Certainly not on my radar -- or in my testing regime.   I'll scan for other possible references and get a fix in shortly.  I expect to put out another alpha release in a day or so.
ago by
I knew it would work as I had it working back in 2016 as a Bricscad plugin but I had to put the project down. AutoCAD and Bricscad wrapped their C++ API's in .net back in ~2006 and with the extra tooling available now, ClojureCLR is a very viable way to build plugins that don't require a rebuild and load every time you edit a file, and you have a repl to boot.
cheers.
ago by
The only references I could find indicate a change between Framework and .NET 8, though there are so few mentions it could have happened earlier.  It would not have been an issue if more errors were trapped on loading.  Fix coming.
ago by
I think I have a fix.  For testing, I added a little C++ console app that hosts the CLR runtime and calls out to a .NET assembly that in turn references ClojureCLR.  If you are interested, it's located in https://github.com/clojure/clojure-clr/tree/master/other/CPlusPlusHost .  I hope to have a new alpha release out later today that you can test against.
ago by
thanks, the new alpha version is working from a few quick tests. I'll do some further testing and get back if there are any issues but everything's loading and working fine now.
ago by
hmm, since the latest update I’m seeing what looks like a System.Text.Json dependency/version resolution issue with my BricsCAD plugin. My socket server for a vscode repl extension previously used System.Text.Json, but now the host fails resolving the required assembly at runtime and hangs the server.
I switched from json to edn for the socket messages and it's working again, I don't know exactly what's changed - just a heads up.
ago by
System.Text.Json comes in only as a transitive dependency.  Not sure what other libs require it -- could be several.  Installed version is 10.0.7.  Could be a version issue?  Not sure how to even figure these things out in your scenario.  (I had to do some serious tweaking to get all the right libraries in place for my little C++ console app.)
...