JNet: JVM™ callbacks
One of the features of JCOBridge is callback management from the JVM™. Java™ callbacks are typically expressed as interfaces, which Java™ code can satisfy with lambda expressions. Since JNet operates at the JNI boundary, lambdas are not directly usable — a concrete class implementing the interface is required instead. This page explains how JNet and JCOBridge handle this transparently.
JNet Callback internals
JNet is based on JCOBridge, a bridge between the CLR (CoreCLR) and the JVM™. Events are generally expressed as interfaces in Java™, and a lambda expression is compiled into a concrete object at build time. Alternatively, a developer can implement a Java™ class that implements the interface directly — with JCOBridge, this follows a structured but familiar approach.
In JNet, several callbacks are ready-made. This tutorial uses the Predicate interface (java.util.function.Predicate) as an example.
The concrete JVM™ class implementing the interface looks like this:
public final class JNetPredicate extends JCListener implements Predicate {
public JNetPredicate(String key) throws JCNativeException {
super(key);
}
@Override
public boolean test(Object e) {
raiseEvent("test", e);
Object retVal = getReturnData();
return (boolean) retVal;
}
}
The structure follows the JCOBridge guidelines:
- It must extend the base class
JCListener(or implement the interfaceIJCListener). This is a requirement of JCOBridge.JCListenerprovides many ready-made methods for event handling. If the callback is not based on an interface, the class can implementIJCListenerdirectly. - The concrete class must have at least one constructor accepting a
String. - Within the interface method implementation (here,
testfrom thePredicateinterface), the methodraiseEventnotifies the CLR that the method was invoked, passing the event key ("test") and all associated objects:- If the interface declares multiple methods, each one must have its own
raiseEventcall. - The key passed to
raiseEventdoes not need to match the name of the calling method — it is a convention for mapping to the CLR side, which will become clearer when looking at the C# code below.
- If the interface declares multiple methods, each one must have its own
With the JVM™-side class in place, the corresponding CLR-side class in C# looks like this:
public class Predicate<TObject> : JVMBridgeListener
{
public override string ClassName => "org.mases.jnet.util.function.JNetPredicate";
Func<TObject, bool> executionFunction = null;
public virtual Func<TObject, bool> OnTest { get { return executionFunction; } }
public Predicate(Func<TObject, bool> func = null, bool attachEventHandler = true)
{
if (func != null) executionFunction = func;
else executionFunction = Test;
if (attachEventHandler)
{
AddEventHandler("test", new EventHandler<CLRListenerEventArgs<CLREventData<TObject>>>(EventHandler));
}
}
void EventHandler(object sender, CLRListenerEventArgs<CLREventData<TObject>> data)
{
var retVal = OnTest(data.EventData.TypedEventData);
data.SetReturnValue(retVal);
}
public virtual bool Test(TObject obj) { return false; }
}
The structure follows the JCOBridge guidelines:
- It must derive from the base class
JVMBridgeListener. This is a requirement of JCOBridge;JVMBridgeListenercontains all the infrastructure needed to handle events from the JVM™. - The
ClassNameproperty tells the base class which JVM™ class is associated with this event handler. - In the constructor,
AddEventHandlerregisters a .NETEventHandlerfor the corresponding JVM™ method. Note that the key string must match the key used inraiseEventon the JVM™ side:- The constructor accepts an optional
Func, allowing the caller to pass a lambda expression from C#. - The handler uses specific data types:
CLRListenerEventArgsis mandatory and is used internally byJVMBridgeListener.TObjectrepresents the CLR equivalent of the corresponding Java type.
- The constructor accepts an optional
- When the JVM™ invokes the callback (
testin this case), the CLR callsEventHandler:- The first parameter is accessed via the
TypedEventDataproperty. - Once complete, the return value is sent back to the JVM™ via
SetReturnValue.
- The first parameter is accessed via the
- The class also supports two extension points:
- Subclassing
Predicate<TObject>and overridingOnTestfor a class-based approach. - Assigning a handler directly to the
OnTestproperty without subclassing.
- Subclassing
JNet Callback lifecycle
The lifecycle of a JCOBridge-managed callback differs from standard .NET object lifetime.
To prevent the .NET Garbage Collector from collecting an active JVMBridgeListener instance —
which would break the JVM™ callback link — the instance is automatically registered during
initialization. This automatic registration can be disabled by setting the AutoInit property
to false, in which case the developer is responsible for managing the registration manually.
Because of this registration, the instance must be explicitly disposed when no longer needed
to avoid a resource leak. The recommended pattern uses a using block so the instance is
allocated once and disposed deterministically:
using (var handler = new Predicate<int>((o1) =>
{
if (o1 > 10) return true;
return false;
}))
{
while (!resetEvent.WaitOne(0))
{
if (o.CanSend(i, handler)) o.Send(i);
i++;
}
}
Warning
Avoid instantiating the callback inline at the call site, as in the example below:
var result = o.CanSend(i, new Predicate<int>((o1) =>
{
if (o1 > 10) return true;
return false;
}));
This pattern has two significant drawbacks:
- It creates a resource leak: the
Predicate<int>instance cannot be programmatically disposed. - It incurs unnecessary overhead: the JVM™ event-handling infrastructure is allocated on every call instead of being reused.