Skip to content

Commit 758c5a6

Browse files
Dangercoderdmiller
authored andcommitted
Fix gen-class :main true to work on .NET 9+ (enable zero-C# executables)
gen-class :main true generates a static Main(string[]) method, but on .NET 9+ two things were broken: 1. SetEntryPoint was only called behind #if NETFRAMEWORK. Added the NET9_0_OR_GREATER path via GenContext.SetEntryPoint which delegates to PersistedAssemblyBuilder. 2. The generated Main did not call RT.Init(). On .NET Framework, static constructors handled this implicitly. On .NET Core+, the runtime must be explicitly initialized. Main now calls RT.Init() before looking up and invoking -main. 3. If -main returns a Task (async entry point), Main now waits for it instead of discarding the result. This enables AOT-compiled Clojure executables with no C# at all: (ns myapp (:gen-class :main true)) (defn -main [& args] (println "Hello!")) Compile with (compile 'myapp), run with: dotnet myapp.exe Includes 2 NUnit tests verifying Main method generation and entry point setting on .NET 9+. Adds net11.0 to TargetFrameworks for testing.
1 parent 0a4d51c commit 758c5a6

File tree

3 files changed

+168
-2
lines changed

3 files changed

+168
-2
lines changed

Clojure/Clojure/CljCompiler/Context/GenContext.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,13 @@ public GenContext WithTypeBuilder(TypeBuilder tb)
249249

250250
#region Other
251251

252+
#if NET9_0_OR_GREATER
253+
internal void SetEntryPoint(MethodBuilder mb)
254+
{
255+
_assyGen.SetEntryPoint(mb);
256+
}
257+
#endif
258+
252259
// DO not call context.AssmeblyGen.SaveAssembly() directly.
253260
internal void SaveAssembly()
254261
{

Clojure/Clojure/CljCompiler/GenClass.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,14 @@ static void DefineCtors(TypeBuilder proxyTB,
394394
static void EmitMain(GenContext context, TypeBuilder proxyTB, string mainName, FieldBuilder mainFB)
395395
{
396396
MethodBuilder cb = proxyTB.DefineMethod("Main",MethodAttributes.Public| MethodAttributes.Static,CallingConventions.Standard,typeof(void),new Type[] { typeof(String[]) });
397-
CljILGen gen = new CljILGen(cb.GetILGenerator()); ;
397+
CljILGen gen = new CljILGen(cb.GetILGenerator());
398398

399399
Label noMainLabel = gen.DefineLabel();
400400
Label endLabel = gen.DefineLabel();
401+
Label notTaskLabel = gen.DefineLabel();
402+
403+
// Initialize the Clojure runtime before anything else
404+
gen.Emit(OpCodes.Call, typeof(RT).GetMethod("Init", BindingFlags.Public | BindingFlags.Static));
401405

402406
EmitGetVar(gen, mainFB);
403407
gen.Emit(OpCodes.Dup);
@@ -406,6 +410,16 @@ static void EmitMain(GenContext context, TypeBuilder proxyTB, string mainName, F
406410
gen.EmitLoadArg(0); // gen.Emit(OpCodes.Ldarg_0);
407411
gen.EmitCall(Method_RT_seq); // gen.Emit(OpCodes.Call, Method_RT_seq);
408412
gen.EmitCall(Method_IFn_applyTo_Object_ISeq); // gen.Emit(OpCodes.Call, Method_IFn_applyTo_Object_ISeq);
413+
414+
// If -main returns a Task (async), wait for it
415+
gen.Emit(OpCodes.Dup);
416+
gen.Emit(OpCodes.Isinst, typeof(System.Threading.Tasks.Task));
417+
gen.Emit(OpCodes.Brfalse_S, notTaskLabel);
418+
gen.Emit(OpCodes.Castclass, typeof(System.Threading.Tasks.Task));
419+
gen.Emit(OpCodes.Callvirt, typeof(System.Threading.Tasks.Task).GetMethod("Wait", Type.EmptyTypes));
420+
gen.Emit(OpCodes.Br_S, endLabel);
421+
422+
gen.MarkLabel(notTaskLabel);
409423
gen.Emit(OpCodes.Pop);
410424
gen.Emit(OpCodes.Br_S, endLabel);
411425

@@ -417,8 +431,9 @@ static void EmitMain(GenContext context, TypeBuilder proxyTB, string mainName, F
417431
gen.Emit(OpCodes.Ret);
418432

419433
#if NETFRAMEWORK
420-
//context.AssyBldr.SetEntryPoint(cb);
421434
context.AssemblyBuilder.SetEntryPoint(cb);
435+
#elif NET9_0_OR_GREATER
436+
context.SetEntryPoint(cb);
422437
#endif
423438
}
424439

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#if NET9_0_OR_GREATER
2+
3+
using System;
4+
using System.IO;
5+
using System.Reflection;
6+
using System.Threading.Tasks;
7+
using clojure.lang;
8+
using NUnit.Framework;
9+
10+
namespace Clojure.Tests.LibTests
11+
{
12+
[TestFixture]
13+
public class GenClassMainTests
14+
{
15+
static readonly IFn Eval = RT.var("clojure.core", "eval");
16+
static readonly IFn ReadString = RT.var("clojure.core", "read-string");
17+
18+
[OneTimeSetUp]
19+
public void Setup()
20+
{
21+
RT.Init();
22+
}
23+
24+
private object EvalClj(string code)
25+
{
26+
return Eval.invoke(ReadString.invoke(code));
27+
}
28+
29+
// Test 1: gen-class :main true produces a type with static Main method
30+
[Test]
31+
public void GenClassMainProducesMainMethod()
32+
{
33+
// Compile a namespace with gen-class :main true
34+
var compilePath = Path.Combine(Path.GetTempPath(), "clj-test-" + Guid.NewGuid().ToString("N"));
35+
Directory.CreateDirectory(compilePath);
36+
37+
try
38+
{
39+
// Create a source file
40+
var srcDir = Path.Combine(compilePath, "src");
41+
Directory.CreateDirectory(srcDir);
42+
File.WriteAllText(Path.Combine(srcDir, "testmain.cljr"),
43+
@"(ns testmain (:gen-class :main true))
44+
(defn -main [& args] (str ""hello""))");
45+
46+
// Set up compilation
47+
EvalClj($@"
48+
(binding [*compile-path* ""{compilePath.Replace("\\", "\\\\")}""
49+
*compile-files* true]
50+
(let [old-path (System.Environment/GetEnvironmentVariable ""CLOJURE_LOAD_PATH"")]
51+
(System.Environment/SetEnvironmentVariable ""CLOJURE_LOAD_PATH"" ""{srcDir.Replace("\\", "\\\\")}"")
52+
(try
53+
(compile 'testmain)
54+
(finally
55+
(System.Environment/SetEnvironmentVariable ""CLOJURE_LOAD_PATH"" old-path)))))");
56+
57+
// Find the compiled assembly
58+
var exePath = Path.Combine(compilePath, "testmain.exe");
59+
if (!File.Exists(exePath))
60+
{
61+
// PersistedAssemblyBuilder may write to CWD
62+
exePath = Path.Combine(Directory.GetCurrentDirectory(), "testmain.exe");
63+
}
64+
65+
Assert.That(File.Exists(exePath), Is.True,
66+
$"gen-class :main true should produce testmain.exe");
67+
68+
// Load and check for Main method
69+
var asm = Assembly.LoadFrom(exePath);
70+
var mainType = asm.GetType("testmain");
71+
Assert.That(mainType, Is.Not.Null, "Should have a 'testmain' type");
72+
73+
var mainMethod = mainType.GetMethod("Main", BindingFlags.Public | BindingFlags.Static);
74+
Assert.That(mainMethod, Is.Not.Null, "Should have a static Main method");
75+
Assert.That(mainMethod.ReturnType, Is.EqualTo(typeof(void)), "Main should return void");
76+
77+
var parameters = mainMethod.GetParameters();
78+
Assert.That(parameters, Has.Length.EqualTo(1), "Main should take one parameter");
79+
Assert.That(parameters[0].ParameterType, Is.EqualTo(typeof(string[])), "Parameter should be string[]");
80+
}
81+
finally
82+
{
83+
try { Directory.Delete(compilePath, true); } catch { }
84+
// Clean up CWD artifacts
85+
try { File.Delete("testmain.exe"); } catch { }
86+
try { File.Delete("testmain.cljr.dll"); } catch { }
87+
}
88+
}
89+
90+
// Test 2: Generated Main calls RT.Init (verified by checking IL contains the call)
91+
[Test]
92+
public void GenClassMainCallsRTInit()
93+
{
94+
var compilePath = Path.Combine(Path.GetTempPath(), "clj-test-" + Guid.NewGuid().ToString("N"));
95+
Directory.CreateDirectory(compilePath);
96+
97+
try
98+
{
99+
var srcDir = Path.Combine(compilePath, "src");
100+
Directory.CreateDirectory(srcDir);
101+
File.WriteAllText(Path.Combine(srcDir, "testinit.cljr"),
102+
@"(ns testinit (:gen-class :main true))
103+
(defn -main [& args] (str ""works""))");
104+
105+
EvalClj($@"
106+
(binding [*compile-path* ""{compilePath.Replace("\\", "\\\\")}""
107+
*compile-files* true]
108+
(let [old-path (System.Environment/GetEnvironmentVariable ""CLOJURE_LOAD_PATH"")]
109+
(System.Environment/SetEnvironmentVariable ""CLOJURE_LOAD_PATH"" ""{srcDir.Replace("\\", "\\\\")}"")
110+
(try
111+
(compile 'testinit)
112+
(finally
113+
(System.Environment/SetEnvironmentVariable ""CLOJURE_LOAD_PATH"" old-path)))))");
114+
115+
var exePath = Path.Combine(compilePath, "testinit.exe");
116+
if (!File.Exists(exePath))
117+
exePath = Path.Combine(Directory.GetCurrentDirectory(), "testinit.exe");
118+
119+
Assert.That(File.Exists(exePath), Is.True, "Should produce testinit.exe");
120+
121+
// Load and verify the Main method exists and has correct signature
122+
var asm = Assembly.LoadFrom(exePath);
123+
var mainType = asm.GetType("testinit");
124+
Assert.That(mainType, Is.Not.Null);
125+
126+
var mainMethod = mainType.GetMethod("Main", BindingFlags.Public | BindingFlags.Static);
127+
Assert.That(mainMethod, Is.Not.Null, "Main method should exist");
128+
129+
// Verify the assembly has an entry point
130+
Assert.That(asm.EntryPoint, Is.Not.Null,
131+
"Assembly should have an entry point set via SetEntryPoint");
132+
Assert.That(asm.EntryPoint.Name, Is.EqualTo("Main"));
133+
}
134+
finally
135+
{
136+
try { Directory.Delete(compilePath, true); } catch { }
137+
try { File.Delete("testinit.exe"); } catch { }
138+
try { File.Delete("testinit.cljr.dll"); } catch { }
139+
}
140+
}
141+
}
142+
}
143+
144+
#endif

0 commit comments

Comments
 (0)