Dual x86_64/arm64 Java application running under x86_64 emulation on a M1 machine

I have built a Java application that is bundled with universal binaries for the Java Runtime Environment, so that the application can run natively on both x86_64 and arm64 machines.

That is, the executable file within the application is a shell script that calls the embedded java binary to actually start the Java program. The java binary itself is a “universal binary” created with lipo, with support for both x86_64 and arm64.

Trying to start the application from the command line, with open -a MyApplication.app, on my M1-powered machine yields the expected behaviour: the application is running natively, as can be seen in the Activity Monitor, where the corresponding process is shown as running under an “Apple” CPU kind. (It can also be seen by the mere fact that the application is much more responsive than it is when it is executed by a x86_64 JRE).

However, when the very same application is executed from the Finder, by double-clicking on MyApplication.app, it then runs under x86_64 emulation, as can be seen in the Activity Monitor, where the corresponding process is shown as running under an “Intel” CPU kind.

How comes that the same application runs under emulation or not depending on whether it is started from the command line or from the Finder? How can I force the application to run natively when it is started from the Finder?

Adding the LSRequiresNativeExecution key in the Info.plist file does not change anything, nor does adding the LSArchitecturePriority key.

My workaround for now is to build two variants of my application, one with a x86_64 JRE for Intel users and one with a arm64 JRE for my Apple Silicon users. But I’d really like to be able to provide them with a single application that runs natively on both architectures.

Any help welcome. Happy to provide more details if needed.

Accepted Reply

How comes that the same application runs under emulation or not depending on whether it is started from the command line or from the Finder?

There can be all sorts of reasons for that. However, my initial port of call would be this:

That is, the executable file within the application is a shell script

Using a shell script for your main executable is an ongoing source of problems [1] and something I specifically recommend against. If you must do this trampoline thing, switch to a native trampoline. I recently helped a developer with a different issue caused by the same problem and, as part of that, I created a simple trampoline in 39 lines of C (see below).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] For example, the TCC issue described in in On File System Permissions.


#include <libgen.h>
#include <mach-o/dyld.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>
#include <unistd.h>

extern char **environ;

static void fail(const char * message) {
    fprintf(stderr, "%s\n", message);
    exit(EXIT_FAILURE);
}

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)

    char execPath[MAXPATHLEN];
    uint32_t execPathSize = sizeof(execPath);
    
    int success = _NSGetExecutablePath(execPath, &execPathSize) >= 0;
    if ( ! success ) { fail("_NSGetExecutablePath failed"); }
    
    const char * dirPath = dirname(execPath);
    if (dirPath == NULL) { fail("dirname failed"); }
    
    char * newPath;
    success = asprintf(&newPath, "%s/../MacOS/RealExecutable", dirPath) >= 0;
    if ( ! success ) { fail("asprintf failed"); }

    char * resourcePath;
    success = asprintf(&resourcePath, "%s/../Resources/Greeting.txt", dirPath) >= 0;
    if ( ! success ) { fail("asprintf failed"); }

    char * arguments[3] = { newPath, resourcePath, NULL };
    (void) execve(newPath, arguments, environ);
    fail("execve failed");
}

Replies

How comes that the same application runs under emulation or not depending on whether it is started from the command line or from the Finder?

There can be all sorts of reasons for that. However, my initial port of call would be this:

That is, the executable file within the application is a shell script

Using a shell script for your main executable is an ongoing source of problems [1] and something I specifically recommend against. If you must do this trampoline thing, switch to a native trampoline. I recently helped a developer with a different issue caused by the same problem and, as part of that, I created a simple trampoline in 39 lines of C (see below).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] For example, the TCC issue described in in On File System Permissions.


#include <libgen.h>
#include <mach-o/dyld.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>
#include <unistd.h>

extern char **environ;

static void fail(const char * message) {
    fprintf(stderr, "%s\n", message);
    exit(EXIT_FAILURE);
}

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)

    char execPath[MAXPATHLEN];
    uint32_t execPathSize = sizeof(execPath);
    
    int success = _NSGetExecutablePath(execPath, &execPathSize) >= 0;
    if ( ! success ) { fail("_NSGetExecutablePath failed"); }
    
    const char * dirPath = dirname(execPath);
    if (dirPath == NULL) { fail("dirname failed"); }
    
    char * newPath;
    success = asprintf(&newPath, "%s/../MacOS/RealExecutable", dirPath) >= 0;
    if ( ! success ) { fail("asprintf failed"); }

    char * resourcePath;
    success = asprintf(&resourcePath, "%s/../Resources/Greeting.txt", dirPath) >= 0;
    if ( ! success ) { fail("asprintf failed"); }

    char * arguments[3] = { newPath, resourcePath, NULL };
    (void) execve(newPath, arguments, environ);
    fail("execve failed");
}

I came to the same conclusion (using a compiled launcher instead of a script). Provided the compiled launcher itself is a universal binary with support for both x86_64 and arm64, this seems to work exactly as I wanted. I initially thought of it as a bad hack, but nice to see that this is in fact the recommended solution. :)

Thanks!