Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions external/Java.Interop/Documentation/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ conversion and argument-state creation/cleanup. Primitive, string, and array
conversion remain supported. `JavaObject` and `JavaException` maintain managed
peers and their JNI reference ownership.

## Native callback ownership

`JniType.RegisterNativeMethods` retains the original array and its owner only after
successful registration. Callers must not modify the array or its entries through
any alias while its callbacks can be invoked. Only the latest successfully registered
array is retained, so callers must not start another JNI registration on the same
instance before successful unregistration. This safety requirement is not checked
at runtime. Failed or partial JNI registration does not retain the supplied array;
the caller must keep its delegates rooted until successful unregistration and
completion of all callbacks. Registration errors propagate as exceptions without
attempting to recover from or roll back partial native registration.

Disposal unregisters callbacks before removing the runtime's owner root or releasing
the JNI reference. Failed unregistration preserves those roots and the reference.
Prevent new calls and wait for callbacks already running to finish before
unregistering or disposing the owner.

Direct `JniEnvironment.Types.RegisterNatives` calls do not retain callback roots
after returning. Their callers must keep delegates or raw callback storage callable
and rooted for as long as JNI can invoke them, including after failed or partial
registration. Prevent new calls to the registered native methods before
unregistration. Release callback storage only after unregistration succeeds and
all callbacks already running have finished.

`JniNativeMethod` name/signature storage must remain readable and NUL-terminated
through registration. Its raw callback must match the JNI descriptor's ABI,
including the environment and receiver/class arguments.

## Naming conventions

Types such as `JavaObject` and `JavaArray<T>` provide managed wrappers for Java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,35 @@ static string JavaClassToJniType (string value)
return value.Replace ('.', '/');
}

/// <safety>
/// The caller must keep the Marshaler delegates in <paramref name="methods"/> strongly
/// reachable for as long as JNI can invoke their callbacks, including after a failed
/// or partially completed registration. Returning from this call does not end that lifetime.
/// Release those roots only after successful unregistration and completion of all
/// in-flight callbacks.
/// Each delegate's parameter and return types, calling convention, and marshaling
/// attributes must match its JNI descriptor, including the leading JNIEnv and
/// receiver/class pointers. JNI scalar values must not be interpreted as managed references.
/// </safety>
[RequiresDynamicCode ("Native method registration via JniNativeMethodRegistration[] requires dynamic code generation. Use the blittable RegisterNatives(JniObjectReference, ReadOnlySpan<JniNativeMethod>) overload with statically-compiled function pointers for Native AOT compatibility.")]
public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods)
public static unsafe void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods)
{
RegisterNatives (type, methods, methods == null ? 0 : methods.Length);
unsafe {
// SAFETY: Caller supplies JNI-compatible delegates and keeps them rooted while JNI can invoke them.
RegisterNatives (type, methods, methods == null ? 0 : methods.Length);
}
}

/// <safety>
/// The caller must keep the Marshaler delegates in the first <paramref name="numMethods"/>
/// entries of <paramref name="methods"/> strongly reachable for as long as JNI can invoke
/// their callbacks, including after a failed or partially completed registration.
/// Returning from this call does not end that lifetime. Release those roots only after
/// successful unregistration and completion of all in-flight callbacks.
/// Each registered delegate's parameter and return types, calling convention, and
/// marshaling attributes must match its JNI descriptor, including the leading JNIEnv
/// and receiver/class pointers. JNI scalar values must not be interpreted as managed references.
/// </safety>
[RequiresDynamicCode ("Native method registration via JniNativeMethodRegistration[] requires dynamic code generation. Use the blittable RegisterNatives(JniObjectReference, ReadOnlySpan<JniNativeMethod>) overload with statically-compiled function pointers for Native AOT compatibility.")]
public static unsafe void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods, int numMethods)
{
Expand Down Expand Up @@ -271,16 +294,21 @@ public static unsafe void RegisterNatives (JniObjectReference type, JniNativeMet
unmanagedStrings [i * 2] = name;
IntPtr sig = Marshal.StringToCoTaskMemUTF8 (m.Signature);
unmanagedStrings [i * 2 + 1] = sig;
natives [i] = new JniNativeMethod ((byte*) name, (byte*) sig, Marshal.GetFunctionPointerForDelegate (m.Marshaler));
unsafe {
// SAFETY: UTF-8 allocations remain live through registration; caller guarantees the delegate ABI.
natives [i] = new JniNativeMethod ((byte*) name, (byte*) sig, Marshal.GetFunctionPointerForDelegate (m.Marshaler));
}
}
unsafe {
// SAFETY: Names/signatures stay allocated until finally; caller guarantees callback ABI and lifetime.
RegisterNatives (type, natives);
}
RegisterNatives (type, natives);
// Keep the Marshaler delegates alive at least until JNI has consumed the function pointers.
GC.KeepAlive (methods);
} finally {
for (int i = 0; i < unmanagedStrings.Length; ++i) {
if (unmanagedStrings [i] != IntPtr.Zero)
Marshal.ZeroFreeCoTaskMemUTF8 (unmanagedStrings [i]);
}
GC.KeepAlive (methods);
}
}

Expand All @@ -289,17 +317,30 @@ public static unsafe void RegisterNatives (JniObjectReference type, JniNativeMet
/// with raw function pointers and UTF-8 name/signature pointers.
/// Calls the JNI RegisterNatives function directly without delegate marshaling.
/// </summary>
/// <safety>
/// Each method's name and signature must address readable NUL-terminated UTF-8 memory
/// through this call. Each callback must match its JNI descriptor's ABI, including
/// the leading JNIEnv and receiver/class pointers. Keep callback storage callable and
/// rooted for as long as JNI can invoke it, including after failed or partial registration.
/// Prevent new calls to the registered native methods before unregistration. Release
/// callback storage only after unregistration succeeds and all callbacks already
/// running have finished.
/// </safety>
public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan<JniNativeMethod> methods)
{
if (!type.IsValid)
throw new ArgumentException ("Handle must be valid.", nameof (type));

IntPtr env = JniEnvironment.EnvironmentPointer;
int r;
fixed (JniNativeMethod* methodsPtr = methods) {
var registerNatives = (delegate* unmanaged<IntPtr, IntPtr, JniNativeMethod*, int, int>)
(void*) (*((JNIEnv**)env))->RegisterNatives;
r = registerNatives (env, type.Handle, methodsPtr, methods.Length);
unsafe {
// SAFETY: The runtime supplies this thread's JNIEnv and the span is pinned for the call.
// Caller guarantees valid name/signature storage and callback ABI and lifetime.
fixed (JniNativeMethod* methodsPtr = methods) {
var registerNatives = (delegate* unmanaged<IntPtr, IntPtr, JniNativeMethod*, int, int>)
(void*) (*((JNIEnv**)env))->RegisterNatives;
r = registerNatives (env, type.Handle, methodsPtr, methods.Length);
}
}

// Surface (and clear) any pending Java exception raised by JNI::RegisterNatives()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public unsafe struct JniNativeMethod
byte* signature;
IntPtr functionPointer;

/// <remarks>
/// This constructor stores addresses without reading their memory or invoking the callback.
/// When registering this descriptor, <paramref name="name"/> and <paramref name="signature"/>
/// must address readable NUL-terminated UTF-8 memory through registration. <paramref name="functionPointer"/>
/// must have the JNI-compatible ABI for the signature, including the environment and
/// receiver arguments, and remain callable and rooted for as long as JNI can invoke it,
/// including after failed or partial registration. Prevent new calls to the registered
/// native methods before unregistration. Release callback storage only after
/// unregistration succeeds and all callbacks already running have finished.
/// </remarks>
public JniNativeMethod (byte* name, byte* signature, IntPtr functionPointer)
{
this.name = name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,9 @@ void ClearTrackedReferences ()
{
List<IDisposable> values;
lock (TrackedInstances) {
// Dispose untracks each owner only after successful unregistration.
// Pre-clearing would drop callback roots if unregistration fails.
values = new List<IDisposable> (TrackedInstances.Values);
TrackedInstances.Clear ();
}

foreach (var d in values)
Expand Down
46 changes: 38 additions & 8 deletions external/Java.Interop/src/Java.Interop/Java.Interop/JniType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,18 @@ internal static JniType GetCachedJniType ([NotNull] ref JniType? cachedType, Int
return cachedType;
}

/// <remarks>
/// Prevent new calls to the registered native methods and wait for callbacks already
/// running to finish before disposing this type.
/// </remarks>
public void Dispose ()
{
if (!PeerReference.IsValid)
return;
if (registered)
JniEnvironment.Runtime.UnTrack (PeerReference.Handle);
if (methods != null)
UnregisterNativeMethods ();
if (registered)
JniEnvironment.Runtime.UnTrack (PeerReference.Handle);
JniObjectReference.Dispose (ref peerReference);
}

Expand Down Expand Up @@ -172,30 +176,56 @@ public bool IsInstanceOfType (JniObjectReference value)
return JniEnvironment.Types.IsInstanceOf (value, PeerReference);
}

#pragma warning disable 0414
// This isn't used anywhere; it's just present so that the GC won't collect the referenced delegates.
JniNativeMethodRegistration[]? methods;
#pragma warning restore 0414

/// <remarks>
/// After successful registration, retains the original array without copying it.
/// A failed registration does not retain the supplied array, even if JNI installed
/// some callbacks before reporting the failure. Registration exceptions propagate
/// without attempting to roll back partial native registration.
/// </remarks>
/// <safety>
/// The caller must not start another JNI registration on this instance before
/// successful unregistration, including after a failed or partial JNI attempt.
/// This is not checked: only the latest successfully registered array is retained,
/// so registering again can allow earlier delegates to be collected while JNI still uses them.
/// Do not modify <paramref name="methods"/> or its entries through any alias during
/// this call. After successful registration, ownership of the array transfers to this
/// instance: do not modify it until successful unregistration and completion of all
/// callbacks. If registration fails, the caller must keep the delegates strongly
/// reachable until successful unregistration and completion of all callbacks.
/// The caller must ensure each Marshaler delegate in <paramref name="methods"/> has
/// parameter and return types, calling convention, and marshaling attributes compatible
/// with its JNI method descriptor, including the leading JNIEnv and receiver/class pointers.
/// Managed by-ref parameters must not be used to receive JNI scalar values.
/// This method retains delegates but does not validate their ABI compatibility.
/// </safety>
[RequiresDynamicCode ("Native method registration via JniNativeMethodRegistration[] requires dynamic code generation. Use the blittable RegisterNatives(JniObjectReference, ReadOnlySpan<JniNativeMethod>) overload with statically-compiled function pointers for Native AOT compatibility.")]
public void RegisterNativeMethods (params JniNativeMethodRegistration[] methods)
public unsafe void RegisterNativeMethods (params JniNativeMethodRegistration[] methods)
{
AssertValid ();

if (methods == null)
throw new ArgumentNullException (nameof (methods));

JniEnvironment.Types.RegisterNatives (PeerReference, methods, checked ((int)methods.Length));
// Prevents method delegates from being GC'd so long as this type remains
unsafe {
// SAFETY: Caller guarantees callback ABI and roots delegates on failure; success retains the array.
JniEnvironment.Types.RegisterNatives (PeerReference, methods, checked ((int)methods.Length));
}
this.methods = methods;
Comment thread
simonrozsival marked this conversation as resolved.
RegisterWithRuntime ();
}

/// <remarks>
/// Prevent new calls to the registered native methods and wait for callbacks already
/// running to finish before calling this method.
/// </remarks>
public void UnregisterNativeMethods ()
{
AssertValid ();

JniEnvironment.Types.UnregisterNatives (PeerReference);
methods = null;
}

public JniMethodInfo GetConstructor (string signature)
Expand Down
Loading
Loading