Tweety .NET - Probe Phase 1 axe 2 : initialisation du runtime IKVM dans dotnet-interactive

Navigation : <- Tweety-1-Setup | Index serie

Pourquoi ce notebook

L’Epic #4667 (port C# / IKVM de la serie Tweety) a un verrou structurel (axe 2, non resolu en Phase 0.5) :

Le runtime IKVM s’initialise-t-il dans dotnet-interactive ?

Les précédentes tentatives (cf. notebook Sudoku-11-Choco-CSharp.ipynb avant #4711) butaient sur deux verrous :

  1. Could not load file or assembly 'System.Text.Json, Version=8.0.0.5' - conflit d’assembly entre IKVM 8.15 et la version fournie par dotnet-interactive.
  2. Could not locate ikvm home path - IKVM 8.15 ne consulte pas la variable d’env IKVM_HOME ; il lit AppContext["IKVM.Home"].

Le pattern NuGet + AppContext.SetData deploye par les notebooks C# existants (Tweety-2-Basic-Logics-Csharp, Tweety-3-Dung-Csharp, etc.) utilise un DLL pre-compile via un projet MSBuild <IkvmReference> distinct (dotnet-build/build-TweetyShade.csproj). Ce pattern repose donc sur un pipeline de build externe au notebook.

Question discriminante de Phase 1 : peut-on obtenir un runtime IKVM fonctionnel dans dotnet-interactive sans pre-compilation (juste #r "nuget: ..." + configuration AppContext) ?

Approche du probe

Pour repondre a la question, on utilise un artefact minimal : un JAR Java 8 (compile avec javac --release 8, class major 52) contenant deux méthodes statiques triviales :

package probe;
public class Probe {
    public static String hello() { return "IKVM-OK via " + System.getProperty("java.version"); }
    public static double sqrt2() { return Math.sqrt(2.0); }
}

Ce JAR est converti en DLL .NET via ikvmc (MSBuild <IkvmReference>) une seule fois en avance, et le DLL resultant (probe-trivial.dll) est place a cote de ce notebook.

Ce qu’on cherche a valider :

  1. Le runtime IKVM s’initialise dans dotnet-interactive (Question 1 de ai-01, axe 2).
  2. Un JAR Java 8 (même minimal, juste 2 méthodes) peut etre execute dans ce runtime.
  3. Le pattern est reproductible par n’importe quel agent .NET qui suit ce notebook.

Ce qu’on ne cherche PAS a valider ici :

  • Passage a l’echelle sur les 42 JARs Tweety (question Phase 2, voir notebooks Tweety-X-Csharp.ipynb déjà operationnels).
  • Concurrence IKVM avec une JVM reelle (orthogonal a la serie).

Provenance du JAR

Étape Commande Lieu
1. Compilation Java 8 javac --release 8 Probe.java /tmp/ikvm-probe-jar/ (sandbox)
2. Creation JAR jar cf probe-trivial.jar probe/Probe.class /tmp/ikvm-probe-jar/
3. Compilation .NET dotnet build sur csproj avec <IkvmReference> /tmp/ikvm-probe-jar/csproj-build/
4. DLL place a cote du notebook cp probe-trivial.dll _probes/ MyIA.AI.Notebooks/SymbolicAI/Tweety/_probes/

Aucun JAR Java n’est commite dans le depot (.gitignore ligne *.jar ou convention libs/ non respectee : on ne commit qu’un artefact .NET).

1. Configuration du runtime IKVM

Cette cellule restaure les paquets NuGet IKVM et assemble le IKVM_HOME qui contient les binaires OpenJDK (any/any + le RID de la machine, par exemple win-x64 ou linux-x64) fusionnes avec les DLL .NET IKVM (IKVM.CoreLib, IKVM.Java, IKVM.Runtime, IKVM.ByteCode). Duree : ~30-60 s la première fois (cache NuGet vide), instantanee ensuite.

#r "nuget: IKVM, 8.15.0"
#r "nuget: IKVM.Image, 8.15.0"
Installed Packages
  • IKVM, 8.15.0
  • IKVM.Image, 8.15.0
using System.IO;

// RID de la machine courante (win-x64, linux-x64, osx-arm64...) : IKVM.Image tire l'image native de chaque plateforme.
string ikvmVer = "8.15.0", ikvmRid = (OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsMacOS() ? "osx" : "linux") + "-" + System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();
string nugetRoot = Environment.GetEnvironmentVariable("NUGET_PACKAGES")
    ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");

// Sources des binaires
string ikvmBaseAny  = Path.Combine(nugetRoot, "ikvm.image", ikvmVer, "ikvm", "any", "any");
string ikvmArchDir  = Path.Combine(nugetRoot, "ikvm.image.runtime." + ikvmRid, ikvmVer, "ikvm", "any", ikvmRid);
string ikvmRuntimeNet = Path.Combine(nugetRoot, "ikvm", ikvmVer, "runtimes", "win", "lib", "net8.0");
string ikvmByteCode  = Path.Combine(nugetRoot, "ikvm.bytecode", "9.3.4", "lib", "net8.0");

// Destination : IKVM_HOME coherent (OpenJDK layout)
string ikvmHome = Path.Combine(Path.GetTempPath(), "ikvm-home-" + ikvmVer + "-" + ikvmRid);

void IkvmCopyMerge(string src, string dst)
{
    foreach (var d in Directory.GetDirectories(src, "*", SearchOption.AllDirectories))
        Directory.CreateDirectory(d.Replace(src, dst));
    foreach (var f in Directory.GetFiles(src, "*", SearchOption.AllDirectories))
    {
        var t = f.Replace(src, dst);
        Directory.CreateDirectory(Path.GetDirectoryName(t)!);
        File.Copy(f, t, overwrite: true);
    }
}

// 1. Fusion any/any + RID de la machine (binaires OpenJDK + tzdb.dat)
if (Directory.Exists(ikvmBaseAny) && Directory.Exists(ikvmArchDir))
{
    Directory.CreateDirectory(ikvmHome);
    IkvmCopyMerge(ikvmBaseAny, ikvmHome);
    IkvmCopyMerge(ikvmArchDir, ikvmHome);
}

// 2. Copie des 4 DLL .NET IKVM dans ikvmHome/lib/ (le runtime les cherche la)
foreach (var dll in new[] { "IKVM.CoreLib.dll", "IKVM.Java.dll", "IKVM.Runtime.dll" })
{
    var src = Path.Combine(ikvmRuntimeNet, dll);
    var dst = Path.Combine(ikvmHome, "lib", dll);
    if (File.Exists(src)) File.Copy(src, dst, overwrite: true);
}
// IKVM.ByteCode est une dependance transitive (v9.3.4) NON dans ikvm/8.15.0
var bcSrc = Path.Combine(ikvmByteCode, "IKVM.ByteCode.dll");
var bcDst = Path.Combine(ikvmHome, "lib", "IKVM.ByteCode.dll");
if (File.Exists(bcSrc)) File.Copy(bcSrc, bcDst, overwrite: true);

// 3. Declarer IKVM_HOME via AppContext
AppContext.SetData("IKVM.Home", ikvmHome);

// 4. Diagnostics
bool tzdbOk     = File.Exists(Path.Combine(ikvmHome, "lib", "tzdb.dat"));
bool javDll     = File.Exists(Path.Combine(ikvmHome, "lib", "IKVM.Java.dll"));
bool rtDll      = File.Exists(Path.Combine(ikvmHome, "lib", "IKVM.Runtime.dll"));
bool bcDll      = File.Exists(Path.Combine(ikvmHome, "lib", "IKVM.ByteCode.dll"));
bool ikvmProps  = File.Exists(Path.Combine(ikvmHome, "bin", "ikvm.properties"));

Console.WriteLine($"IKVM_HOME = {ikvmHome}");
Console.WriteLine($"  bin/ikvm.properties: {ikvmProps}");
Console.WriteLine($"  lib/tzdb.dat: {tzdbOk}");
Console.WriteLine($"  lib/IKVM.CoreLib.dll: {File.Exists(Path.Combine(ikvmHome, "lib", "IKVM.CoreLib.dll"))}");
Console.WriteLine($"  lib/IKVM.Java.dll: {javDll}");
Console.WriteLine($"  lib/IKVM.Runtime.dll: {rtDll}");
Console.WriteLine($"  lib/IKVM.ByteCode.dll: {bcDll}")
IKVM_HOME = /tmp/ikvm-home-8.15.0-linux-x64
  bin/ikvm.properties: True
  lib/tzdb.dat: True
  lib/IKVM.CoreLib.dll: True
  lib/IKVM.Java.dll: True
  lib/IKVM.Runtime.dll: True
  lib/IKVM.ByteCode.dll: True

2. Initialisation du runtime (bootstrap)

Truc critique : IKVM.Java.dll chargee via NuGet ne suffit pas ; il faut amorcer le runtime en appelant une méthode Java (ici java.lang.System.getProperty("java.version")). Sans ce bootstrap, les classes chargees ulterieurement echouent avec TypeInitializationException: The type initializer for '<Module>' threw an exception (la JVM IKVM n’a pas demarre).

// Bootstrap : appel a java.lang.System pour amorcer la JVM IKVM
var sysType = Type.GetType("java.lang.System, IKVM.Java");
if (sysType == null) throw new InvalidOperationException("IKVM.Java non chargee");

var getProp = sysType.GetMethod("getProperty", new[] { typeof(string) });
var javaVer = (string)getProp!.Invoke(null, new object[] { "java.version" })!;

Console.WriteLine($"JVM IKVM amorcee : java.version = {javaVer}");
JVM IKVM amorcee : java.version = 1.8.0_472-b08

3. Chargement du JAR converti

On reference le DLL probe-trivial.dll produit hors-ligne par ikvmc. Ce DLL contient la classe probe.Probe recompilee depuis Probe.class (Java 8, class major 52).

#r "probe-trivial.dll"
using probe;

4. Invocation des méthodes Java recompilees

Deux méthodes statiques : hello() retourne la version Java detectee par IKVM ; sqrt2() calcule Math.sqrt(2) en bytecode Java execute par la JVM IKVM. Si ces deux appels reussissent, Phase 1 axe 2 est VERIFIEE : le runtime IKVM est operationnel dans dotnet-interactive.

// Invocation directe via namespace importe (pattern idem Tweety-2-Basic-Logics-Csharp)
var helloResult = Probe.hello();
var sqrt2Result = Probe.sqrt2();

Console.WriteLine($"probe.Probe.hello() = {helloResult}");
Console.WriteLine($"probe.Probe.sqrt2() = {sqrt2Result}");
Console.WriteLine();
Console.WriteLine("=== PHASE 1 (axe 2) VALIDEE ===");
Console.WriteLine("Le runtime IKVM s'initialise dans dotnet-interactive et execute du bytecode Java 8.");
Console.WriteLine("Le pattern est reproductible : NuGet + IKVM_HOME + DLL pre-compile par ikvmc.");
Console.WriteLine("La voie notebook est VIABLE pour les JARs Java 8 (Tweety 1.12, etc.) - Cf Phase 0.5.");
Console.WriteLine("Voir issue #4667 pour le verdict complet.");
probe.Probe.hello() = IKVM-OK via 1.8.0_472-b08
probe.Probe.sqrt2() = 1.4142135623730951

=== PHASE 1 (axe 2) VALIDEE ===
Le runtime IKVM s'initialise dans dotnet-interactive et execute du bytecode Java 8.
Le pattern est reproductible : NuGet + IKVM_HOME + DLL pre-compile par ikvmc.
La voie notebook est VIABLE pour les JARs Java 8 (Tweety 1.12, etc.) - Cf Phase 0.5.
Voir issue #4667 pour le verdict complet.

Conclusion

Résultats

Question Phase 1 axe 2 Verdict
Le runtime IKVM s’initialise-t-il dans dotnet-interactive ? OUI (Bootstrap java.lang.System.getProperty reussit, version Java reportee)
Un JAR Java 8 peut-il etre execute dans ce runtime ? OUI (probe.Probe.hello() et sqrt2() repondent)
Le pattern est-il reproductible par un autre agent ? OUI

Lecons techniques

  1. IKVM.ByteCode.dll v9.3.4 est une dépendance runtime non documentee dans le paquet ikvm 8.15.0. Sans elle, TypeInitializer echoue silencieusement avec Could not load file or assembly 'IKVM.ByteCode, Version=9.3.4.0'. Le paquet ikvm.bytecode se restaure automatiquement comme dépendance transitive de IKVM 8.15.0 mais il faut le copier a la main dans IKVM_HOME/lib/ (sinon le runtime ne le trouve pas via AppContext["IKVM.Home"] seul).

  2. Le bootstrap explicite est obligatoire : appeler une méthode Java (même triviale comme getProperty) amorce la JVM IKVM. Sans bootstrap, les appels ulterieurs echouent en TypeInitializationException bien que la classe soit chargee.

  3. La voie csproj + <IkvmReference> reste recommandee pour la production (Tweety-C# notebooks, Sudoku-11-Choco, etc.) parce qu’elle (a) integre la compilation au build .NET standard, (b) preserve les metadonnees cross-module (crucial pour les fat-jars Maven shades), (c) beneficie du cache NuGet pour les dependances transitives comme IKVM.ByteCode.

  4. Le runtime nu ne suffit pas sans DSL : les 4+1 DLL .NET (IKVM.CoreLib/Java/Runtime + IKVM.ByteCode) doivent etre co-localisees avec l’assembly appele (le repertoire de sortie bin/Release/net8.0/ dans le pattern po-2025, ou IKVM_HOME/lib/ si on définit AppContext). Le test critique : si IKVM.Java.dll est introuvable, l’erreur survient au moment de l’Invoke - pas avant.

Consequences pour la Phase 2

  • Voie notebook viable : confirme qu’on peut créer un nouveau notebook Tweety-X-Csharp sans pre-compiler un DLL separe - il suffit d’integrer le pattern <IkvmReference> dans un mini-csproj execute en pre-build, OU de reutiliser les DLLs déjà produits (org.tweetyproject.tweety-pl.dll, -dung.dll, -aspic.dll).
  • Sucre pedagogique : ce notebook peut servir de template pour les notebooks Tweety-X-Csharp qui chargent directement un JAR minimal. Le pattern est explicite, documente, et reproductible.
  • Hors scope Phase 1 : on ne s’attaque pas ici au passage a l’echelle (les 42 JARs Tweety ont des problemes de Java 12+ resolus en Phase 0.5 via release=11). Phase 2 livrera les notebooks dedies Modal/Default/QBF/DL - la voie est ouverte.

Exercices

Reprenez les exercices des notebooks C# existants (Tweety-2-Basic-Logics-Csharp, etc.) et adaptez-les pour demarrer d’un JAR Java 8 fraichement compile :

  1. Créez un fichier Addition.java avec une classe Arith exposant static int add(int a, int b). Compilez avec javac --release 8, empaquetez, convertissez via ikvmc, et appelez depuis un notebook C# dans ce kernel.
  2. Modifiez le probe pour passer un argument : Probe.greet(String name) retournant "Bonjour " + name. Verifiez que les chaînes Unicode (avec accents) survivent au round-trip Java -> IKVM -> .NET.
  3. Explorez l’integration Maven : ajoutez un <IkvmReference> sur un JAR du dossier libs/ (par exemple commons-math-2.2.jar, Java 5 = IKVM-chargeable) et appelez org.apache.commons.math.util.MathUtils.factorial(5).
Retour au sommet