JNet usage
JNet exposes Java™ classes directly in .NET, letting you write C# code against the same types available in the official Java™ packages. If a class or method has not been mapped yet, see What to do if an API was not yet implemented.
Environment setup
JNet accepts many command-line switches to customize its behavior. The full list is available at the Command line switch page.
JVM™ identification
One of the most important command-line switches is JVMPath, available in JCOBridge switches: it can be used to set the location of the JVM™ library (jvm.dll / libjvm.so) if JCOBridge is not able to identify a suitable JRE installation.
If you are embedding JNet in your own product, you can override the JVMPath property as shown below:
class MyJNetCore : JNetCore<MyJNetCore>
{
// Override JVMPath when JCOBridge cannot auto-detect the JRE/JDK installation,
// or when you need to pin a specific JVM version in your application.
public override string JVMPath
{
get
{
string pathToJVM = "Set here the path to the JVM library (jvm.dll / libjvm.so)";
return pathToJVM;
}
}
}
Important
pathToJVM must be properly escaped:
string pathToJVM = "C:\\Program Files\\Eclipse Adoptium\\jdk-11.0.18.10-hotspot\\bin\\server\\jvm.dll";string pathToJVM = @"C:\Program Files\Eclipse Adoptium\jdk-11.0.18.10-hotspot\bin\server\jvm.dll";
Special initialization conditions
JCOBridge attempts to locate a suitable JRE/JDK installation using standard mechanisms: the JAVA_HOME environment variable or the Windows registry (where available).
If the application fails with InvalidOperationException: Missing Java Key in registry, neither JAVA_HOME nor the Windows registry contains a reference to a JRE/JDK installation.
Diagnose the issue:
- Open a command prompt and run
set | findstr JAVA_HOME. - If a value is returned, it may be set at user level rather than system level, making it invisible to the JNet process that raised the exception.
Fix the issue (choose one):
- Set
JAVA_HOMEat system level, e.g.JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-11.0.18.10-hotspot\ - Set
JCOBRIDGE_JVMPathat system level to point directly to the JVM library, e.g.JCOBRIDGE_JVMPath=C:\Program Files\Eclipse Adoptium\jdk-11.0.18.10-hotspot\bin\server\jvm.dll
Important
- At least one of
JCOBRIDGE_JVMPath,JAVA_HOME, or the Windows registry (on Windows) must be available. JCOBRIDGE_JVMPathtakes precedence overJAVA_HOMEand the Windows registry: setting it to the full path ofjvm.dllavoids the need to overrideJVMPathin code.- After first initialization,
JVMPath(set in code) takes precedence over both environment variables and the registry.
Intel CET and JNet
JNet uses an embedded JVM™ through JCOBridge. However, JVM™ initialization is incompatible with CET (Control-flow Enforcement Technology) because the code used to identify the CPU attempts to modify the return address, which CET treats as a violation — see this issue comment.
From .NET 9 preview 6, CET is enabled by default on supported hardware when the build output is an executable (i.e. the .csproj contains <OutputType>Exe</OutputType>).
If the application fails at startup with error 0xc0000409 (subcode 0x30), CET is enabled and conflicting with JVM™ initialization.
Tip
Solutions 2 and 3 are the recommended approaches for most projects. Solution 1 requires targeting an older .NET version; solution 4 requires elevated privileges and a registry change.
There are four possible workarounds:
Target a .NET version that does not enable CET by default, such as .NET 8.
Disable CET for the executable in the
.csproj(JNet project templates include this automatically):
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<!--see https://learn.microsoft.com/en-us/dotnet/core/compatibility/interop/9.0/cet-support-->
<CETCompat>false</CETCompat>
</PropertyGroup>
- Run via the
dotnetapp host instead of the native executable, as described in this comment:
dotnet MyApplication.dll
instead of:
MyApplication.exe
- Register a CET mitigation for the specific executable from an elevated shell:
reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MyApplication.exe" /v MitigationOptions /t REG_BINARY /d "0000000000000000000000000000002000" /f
then run:
MyApplication.exe
Basic example
Below is a basic example demonstrating how to create a JNet-based program, including generics and exception handling. Comments in the code explain each step.
using Java.Util;
using MASES.JNet.Extensions;
using System.Diagnostics;
using Java.Lang;
namespace MASES.JNetExample
{
// Define a concrete implementation of JNetCore<> for this application.
class MyJNetCore : JNetCore<MyJNetCore>
{
}
class Program
{
static void Main(string[] args)
{
// Mandatory first step: allocate the JVM and prepare the interop environment.
MyJNetCore.CreateGlobalInstance();
// Arguments not consumed by JNet/JCOBridge are available here,
// just like standard command-line args.
var appArgs = MyJNetCore.FilteredArgs;
try
{
// Allocate a java.util.Set<String> in the JVM via Collections.Singleton,
// returned as a Java.Util.Set<string> on the .NET side.
Java.Util.Set<string> set = Collections.Singleton("test");
// Attempt to add an element if one was passed on the command line.
// Collections.Singleton returns an immutable Set, so this will throw.
// See: https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#singleton(T)
if (appArgs.Length != 0) set.Add(appArgs[0]);
}
// JNet translates Java exceptions into equivalent .NET exceptions,
// so UnsupportedOperationException is caught here just like any C# exception.
catch (UnsupportedOperationException)
{
System.Console.WriteLine("Operation not supported as expected");
}
// Catch-all: print any unexpected exception and let the application exit cleanly.
catch (System.Exception ex) { System.Console.WriteLine(ex.Message); }
}
}
}
Avoiding Java.Lang.NullPointerException — Understanding .NET/JVM GC interaction
Occasionally, a Java.Lang.NullPointerException is raised with no obvious cause in the .NET code. This is a cross-boundary GC issue: the .NET Garbage Collector may collect a JNet wrapper object while the JVM™ is still using the underlying Java object it references.
In the basic example above, Collections.Singleton("test") creates a wrapper held by set, which remains reachable until set.Add(appArgs[0]) completes — so the GC does not collect it prematurely.
Consider this slightly different snippet:
using Java.Util;
using MASES.JNet.Extensions;
using System.Diagnostics;
using Java.Lang;
namespace MASES.JNetExample
{
class MyJNetCore : JNetCore<MyJNetCore> { }
class Program
{
static void Main(string[] args)
{
MyJNetCore.CreateGlobalInstance();
try
{
Java.Util.Set<string> set = Collections.Singleton("test");
ArrayList<string> arrayList = new();
arrayList.AddAll(0, set); // Java.Lang.NullPointerException may occur here
}
catch (System.Exception ex) { System.Console.WriteLine(ex.Message); }
}
}
}
At the point arrayList.AddAll(0, set) is called:
Java.Util.Set<string>is a .NET wrapper around a JVM™java.util.Set<String>.- The call passes the JVM™ reference across the boundary, but from .NET's perspective the wrapper
sethas no further uses and is eligible for collection. - If the .NET GC runs at this moment — which it may do arbitrarily based on memory pressure — the wrapper is collected and the JVM™ receives a null reference.
Most of the time the code works fine, but the failure is non-deterministic and hard to reproduce. The solutions below prevent it.
Tip
The using pattern is the most idiomatic approach in modern C# and should be preferred in new code.
The SuppressFinalize/ReRegisterForFinalize pattern is useful when refactoring to using blocks is not practical.
using or try-finally with Dispose
All JNet classes implement IDisposable. Wrapping the object in a using block keeps it alive and releases the JVM reference deterministically:
using Java.Util;
using MASES.JNet.Extensions;
using System.Diagnostics;
using Java.Lang;
namespace MASES.JNetExample
{
class MyJNetCore : JNetCore<MyJNetCore> { }
class Program
{
static void Main(string[] args)
{
MyJNetCore.CreateGlobalInstance();
try
{
using (Java.Util.Set<string> set = Collections.Singleton("test"))
{
ArrayList<string> arrayList = new();
arrayList.AddAll(0, set);
}
}
catch (System.Exception ex) { System.Console.WriteLine(ex.Message); }
}
}
}
Or equivalently with try-finally:
using Java.Util;
using MASES.JNet.Extensions;
using System.Diagnostics;
using Java.Lang;
namespace MASES.JNetExample
{
class MyJNetCore : JNetCore<MyJNetCore> { }
class Program
{
static void Main(string[] args)
{
MyJNetCore.CreateGlobalInstance();
try
{
Java.Util.Set<string> set = null;
try
{
set = Collections.Singleton("test");
ArrayList<string> arrayList = new();
arrayList.AddAll(0, set);
}
finally { set?.Dispose(); }
}
catch (System.Exception ex) { System.Console.WriteLine(ex.Message); }
}
}
}
SuppressFinalize/ReRegisterForFinalize pattern
When restructuring to using is not practical, you can suppress finalization for the duration of the cross-boundary call:
using Java.Util;
using MASES.JNet.Extensions;
using System.Diagnostics;
using Java.Lang;
namespace MASES.JNetExample
{
class MyJNetCore : JNetCore<MyJNetCore> { }
class Program
{
static void Main(string[] args)
{
MyJNetCore.CreateGlobalInstance();
try
{
Java.Util.Set<string> set = Collections.Singleton("test");
try
{
System.GC.SuppressFinalize(set);
ArrayList<string> arrayList = new();
arrayList.AddAll(0, set);
}
finally { System.GC.ReRegisterForFinalize(set); }
}
catch (System.Exception ex) { System.Console.WriteLine(ex.Message); }
}
}
}