Write JVM agents in C#. Generate the native bindings. Test the actual binaries.
JvmBridge combines generated JNI/JVMTI declarations, explicit Java reference ownership, and NativeAOT agent entry-point generation in one NuGet package. It works with ordinary Java applications and provides a standalone JVM-hosting API for .NET applications.
The initial package is available as a CI artifact. Public NuGet publication is configured but has not yet been performed.
Create a .NET 10 class library and reference JvmBridge:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<JvmBridgeAgent>true</JvmBridgeAgent>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JvmBridge" Version="YOUR_PACKAGE_VERSION" />
</ItemGroup>
</Project>Use the exact version from the downloaded .nupkg, or a published version once available. Restore a downloaded package from its local directory together with nuget.org. PublishAot is explicit because the SDK evaluates native runtime-pack requirements before package build targets.
using JvmBridge.Agents;
using JvmBridge.Runtime;
public sealed class MyAgent : JavaAgent
{
public override void OnLoad(AgentContext context)
{
Console.Error.WriteLine("Agent loaded: " + context.Options);
}
public override void OnVmInit(AgentContext context, JavaEnvironment environment)
{
using JavaLocalReference text = environment.NewString("Hello from C#!");
Console.Error.WriteLine(environment.GetString(text.Handle));
}
}No attribute or manually exported entry point is required. The generator finds the single concrete JavaAgent subclass and emits Agent_OnLoad, Agent_OnAttach, and Agent_OnUnload into your assembly. Override Configure to request capabilities before events are installed. OnAttach runs after callbacks are active, so it can immediately retransform existing classes. Abstract agent base classes are allowed; ambiguous or invalid agents produce compiler errors.
Publish for the JVM process's operating system and architecture:
dotnet publish -c Release -r linux-arm64
java -agentpath:/absolute/path/MyAgent.so=hello -jar application.jarWindows uses .dll; macOS uses .dylib; Linux uses .so. Native publishing requires the platform's C/C++ toolchain. The resulting library includes its .NET runtime; Java users do not need to install .NET.
On Unix HotSpot, use the JDK's signal-chaining library when hosting both runtimes in one process. For a typical Linux JDK:
LD_PRELOAD="$JAVA_HOME/lib/libjsig.so" java -agentpath:/absolute/path/MyAgent.so -jar application.jarLocations differ across distributions and Java versions; Java 8 commonly places the library beneath jre/lib/<architecture>. macOS uses DYLD_INSERT_LIBRARIES with libjsig.dylib. The test runner discovers the correct library. Do not inject HotSpot's library into OpenJ9. The fixtures test both Java exceptions and successful process shutdown.
- Generated native API: JNI/JVMTI types, constants, function-table entries, and callback signatures from pinned OpenJDK headers. C# raw declarations live in
JvmBridge.Nativeand preserve native identifiers. - Runtime helpers: JVM creation, thread attachment, UTF-16 string conversion, modified UTF-8 names/options, method calls, native registration, and owned local/global references.
- Agent lifecycle: startup and late attachment, VM initialization/death, class preparation, class-file transformation, capability negotiation, and retransformation.
- One package: the runtime, source generator, and MSBuild integration ship together. Consumers do not need Clang or header files.
- Real validation: CI publishes the example against the packed NuGet package and loads it into each available pinned JVM.
Raw function pointers are intentionally unsafe escape hatches. Check version/capability/phase requirements before using them. Variadic and va_list entries are exposed as addresses; use typed argument-array *A calls from managed code. JvmBridge supplies class bytes and JVMTI transformation plumbing, not a general Java bytecode editor.
The manifest tracks every NativeAOT target in scope and every Java major from 8 through 26, with HotSpot and OpenJ9 inventories. Archive availability varies by version and architecture. Mobile targets and platforms without a suitable JVM are explicitly experimental or unavailable.
See the generated compatibility inventory. It shows available downloads, not a blanket support claim. The matching commit's CI reports show which native builds and runtime tests actually passed. Historical patch releases are not exhaustively tested. JVM agent support does not follow automatically from .NET target support.
- HelloAgent: registers native methods, preserves Unicode across threads/global references, transforms a fixture class, and supports late attachment.
- JavaHost: creates a JVM from an explicit native-library path, invokes Java, handles exceptions, and demonstrates ownership.
For NativeAOT executables that embed a JVM, set <JvmBridgeHost>true</JvmBridgeHost>. On Intel macOS this reserves a 1 MiB null guard rather than the default low 4 GiB reservation, leaving room for OpenJ9 compressed-reference metadata. On Windows AMD64 it opts the host executable out of CET shadow-stack compatibility because HotSpot CPU feature probes are incompatible with it; no operating-system mitigation policy is changed. An explicit CETCompat property takes precedence. The JavaHost sample includes this setting.
Agent callbacks borrow their JNI environment. Local references must be disposed before the callback returns. Promote a reference to a global reference before retaining it or passing it to another attached thread. Attachments only detach threads they attached. Dispose JVM-owned resources before destroying a hosted JVM. JNI failures preserve their result in JniException.ErrorCode; some OpenJ9 builds return JNI_ERR from DestroyJavaVM even in a plain C host. The test reports record this native baseline explicitly rather than claiming successful embedded shutdown. Checked-JNI warnings are compared with an uninstrumented Java launch; additional agent warnings fail validation. Generated entry points retain the native module until process termination, including when a JVM releases its own agent-library handle. OnUnload performs exactly-once logical cleanup at VM death, with the native unload export as a fallback.
Install the SDK from global.json, Python 3.12+, and the native publishing toolchain:
dotnet tool restore
python eng/check.py
dotnet pack src/JvmBridge -c Release -o artifacts/packages -p:Version=0.1.0-local.1
python eng/test_agent.py --rid linux-x64 --version 0.1.0-local.1The last command builds the actual package consumers and executes the entire pinned target-specific JVM matrix, downloading one JDK at a time. CI compiles portable Java 8 fixture bytecode once with a pinned JDK 17 and runs it unchanged on all target JVMs; local runs can compile fixtures with the supplied JDK. It includes startup, late attachment, native exports, ABI comparisons, JNI calls, Unicode, references, native threads, transformation/retransformation, failure containment, and shutdown.
For a focused run with an installed JDK:
python eng/test_agent.py --rid linux-arm64 --version 0.1.0-local.1 --jdk-home /path/to/jdk --java 25Logs, native/managed ABI reports, and per-cell results are written to artifacts/results. python eng/report.py --verify requires all available manifest cells to have passed; it is intended for the aggregated CI run. Do not treat a subset run as full coverage.
python eng/generate.py --check
python eng/abi.py --check
python eng/report.py --checkGeneration verifies header checksums and uses a fixed Clang target and shim headers to avoid host-dependent declarations. The shims cover only unused stdio declarations and Clang's target-specific va_list. Upstream headers are unmodified. Native C probes independently validate the actual target ABI, including capability bits and callback/function-table offsets.
Scheduled JDK maintenance discovers stable releases, pins archive checksums, regenerates bindings, and opens tested update PRs. Renovate maintains the SDK, packages, tools, and actions. Compatible updates may merge after protected checks pass. Public API changes and new platforms require review. Release-please owns versions, release notes, and tags.
See contributing, automation setup, and agent instructions. Original code is MIT licensed; see upstream notices for header provenance.