1

I want to call NetworkExtension framework functions from my java application. As a first step, I am calling NETunnelProviderManager.loadAllFromPreferencesWithCompletionHandler. I am using Rococoa library compiled from source (0.8.3-SNAPSHOT).

My NETunnelProviderManager class is as follows:

package ai.safekids.infra.jna.ext.mac.networkextension;

import ai.safekids.infra.jna.ext.mac.data.NSArrayRef;
import ai.safekids.infra.jna.ext.mac.data.NSErrorRef;
import com.sun.jna.Callback;
import org.rococoa.Rococoa;
import org.rococoa.ObjCClass;
import org.rococoa.cocoa.foundation.NSArray;

public abstract class NETunnelProviderManager extends NEVPNManager {
    public static final _Class CLASS = Rococoa.createClass("NETunnelProviderManager", _Class.class);

    public interface LoadAllFromPreferencesCompletionHandler extends Callback {
        void callback(NSArrayRef managers, NSErrorRef error);
    }

    public static NETunnelProviderManager new_() {
        return Rococoa.create("NETunnelProviderManager", NETunnelProviderManager.class);
    }

    public interface _Class extends ObjCClass {
        void loadAllFromPreferencesWithCompletionHandler(LoadAllFromPreferencesCompletionHandler completionHandler);
    }

    public static void loadAllFromPreferencesWithCompletionHandler(LoadAllFromPreferencesCompletionHandler completionHandler) {
        CLASS.loadAllFromPreferencesWithCompletionHandler(completionHandler);
    }
}

and NEVPNManager class is:

package ai.safekids.infra.jna.ext.mac.networkextension;

import com.sun.jna.Callback;
import org.rococoa.Rococoa;
import org.rococoa.cocoa.foundation.NSError;
import org.rococoa.cocoa.foundation.NSObject;
import org.rococoa.ObjCClass;

public abstract class NEVPNManager extends NSObject {
    public static final _Class CLASS = Rococoa.createClass("NEVPNManager", _Class.class);

    public interface LoadFromPreferencesCompletionHandler extends Callback {
        void invoke(NSError error);
    }

    public interface _Class extends ObjCClass {
        NEVPNManager sharedManager();
    }

    public static NEVPNManager new_() {
        return Rococoa.create("NEVPNManager", NEVPNManager.class);
    }

    static public final NEVPNManager sharedManager = NEVPNManager.CLASS.sharedManager();

    static public NEVPNManager sharedManager() {
        return sharedManager;
    }

    public abstract void loadFromPreferencesWithCompletionHandler(LoadFromPreferencesCompletionHandler completionHandler);
}

The unit-test that crash the VM is:

public void testLoadAllPreferences() throws Exception {

    NETunnelProviderManager.LoadAllFromPreferencesCompletionHandler completionHandler = new NETunnelProviderManager.LoadAllFromPreferencesCompletionHandler() {
        public void callback(NSArrayRef managers, NSErrorRef error) {
            Assert.assertEquals(true, true);
        }
    };

    NETunnelProviderManager.loadAllFromPreferencesWithCompletionHandler(completionHandler);
    TimeUnit.SECONDS.sleep(5);
}

My NSArrayRef class is

package ai.safekids.infra.jna.ext.mac.data;

import org.rococoa.ObjCObjectByReference;

public class NSArrayRef extends ObjCObjectByReference {
}

The mvn test output is

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running ai.safekids.infra.jna.ext.mac.NETunnelProviderManagerTest
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by net.sf.cglib.core.ReflectUtils$1 (file:/Users/mac/.m2/repository/cglib/cglib/3.3.0/cglib-3.3.0.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of net.sf.cglib.core.ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007fff2049ebc3, pid=12628, tid=5891
#
# JRE version: OpenJDK Runtime Environment (15.0.2+7) (build 15.0.2+7)
# Java VM: OpenJDK 64-Bit Server VM (15.0.2+7, mixed mode, sharing, tiered, compressed oops, g1 gc, bsd-amd64)
# Problematic frame:
# C  [libobjc.A.dylib+0x5bc3]  objc_retain+0x23
#
# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/mac/code/safekids/packet-proxy/hs_err_pid12628.log
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
/bin/sh: line 1: 12628 Abort trap: 6           /usr/local/Cellar/openjdk/15.0.2/libexec/openjdk.jdk/Contents/Home/bin/java -jar /Users/mac/code/safekids/packet-proxy/target/surefire/surefirebooter783506685843365516.jar /Users/mac/code/safekids/packet-proxy/target/surefire/surefire517337276897757629tmp /Users/mac/code/safekids/packet-proxy/target/surefire/surefire_011227232066576961507tmp

Results :

Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.094 s
[INFO] Finished at: 2021-05-25T16:31:12+05:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project packet-proxy: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test failed: The forked VM terminated without saying properly goodbye. VM crash or System.exit called ? -> [Help 1]

A few questions:

  • Does Rococoa library support callbacks?
  • Whats the cause of VM crash and how can I fix it?
  • Should I use NSError/NSArray in the LoadAllFromPreferencesCompletionHandler or NSErrorRef/NSArrayRef
Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
goto
  • 433
  • 3
  • 13
  • I suspect your callback arguments may be GC'd as unreachable before your code executes. Try creating a strong reference in Java that maintains scope until after the callback executes. If this works I'll type up a more complete answer. – Daniel Widdis May 26 '21 at 05:53
  • Also I'll only be addressing the "VM Crash" part of your question. Please focus on one topic per question. – Daniel Widdis May 26 '21 at 05:56
  • @DanielWiddis how can I make a strong reference in Java that maintains scope until after the callback executes? – goto May 26 '21 at 06:12

1 Answers1

1

The Rococoa library is primarily just an object oriented interface to the underlying JNA library, which does support callbacks.

The likely cause of the JVM crash is that your callback is going out of reachability scope. As soon as this line is executed in Java, the JVM compiler thinks you are done with the completionHandler variable and it is eligible for garbage collection.

NETunnelProviderManager.loadAllFromPreferencesWithCompletionHandler(completionHandler);

If you are using JDK 9+, this is most easily solved with a Reachability Fence. Simply add this line after your callback to ensure a strong reference is retained:

Reference.reachabilityFence(completionHandler);

If you are using JDK8 or earlier, the best workaround is to use a try ... finally block and do something with the object in the finally block, e.g.,

try {
    NETunnelProviderManager.loadAllFromPreferencesWithCompletionHandler(completionHandler);
    TimeUnit.SECONDS.sleep(5);
} finally {
    // do something with completionHandler
}

The "do something" can be a trivial execution (e.g., a toString()) that you don't even need to output, but will ensure Java holds on to the object.

One possible "do something" that I have read works in OpenJDK 8 (Hotspot) is to define another method elsewhere that will work similarly to JDK9+'s reachability fence. I have not verified this; try it at your own risk. This may not work with other JVMs.

static void reachabilityFence(Object obj) {
    // do nothing
}

Another possible workaround is to make your callback closeable, putting its creation in a try-with-resources block, which will retain the reference until the block completes and it calls close() (which can be a no-op method).

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63