Table of Contents

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:

  1. string pathToJVM = "C:\\Program Files\\Eclipse Adoptium\\jdk-11.0.18.10-hotspot\\bin\\server\\jvm.dll";
  2. 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:

  1. Open a command prompt and run set | findstr JAVA_HOME.
  2. 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_HOME at system level, e.g. JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-11.0.18.10-hotspot\
  • Set JCOBRIDGE_JVMPath at 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_JVMPath takes precedence over JAVA_HOME and the Windows registry: setting it to the full path of jvm.dll avoids the need to override JVMPath in 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:

  1. Target a .NET version that does not enable CET by default, such as .NET 8.

  2. 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>
  1. Run via the dotnet app host instead of the native executable, as described in this comment:
dotnet MyApplication.dll

instead of:

MyApplication.exe
  1. 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 set has 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); }
        }
    }
}