From 57251e36ae093f1a34aac4efb0f8132d79f3b8b4 Mon Sep 17 00:00:00 2001 From: Simon Urbanek Date: Mon, 16 Sep 2024 17:23:14 +1200 Subject: [PATCH 01/63] add support for dynamically loaded JNI --- src/djni.c | 198 ++++++++++++++++++++++++ src/djni.h | 19 +++ src/jni.h | 434 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 651 insertions(+) create mode 100644 src/djni.c create mode 100644 src/djni.h create mode 100644 src/jni.h diff --git a/src/djni.c b/src/djni.c new file mode 100644 index 0000000..0675868 --- /dev/null +++ b/src/djni.c @@ -0,0 +1,198 @@ +#include + +#include "djni.h" + +static JNI_GetDefaultJavaVMInitArgs_fnptr JNI_GetDefaultJavaVMInitArgs_fn; +static JNI_CreateJavaVM_fnptr JNI_CreateJavaVM_fn; +static JNI_GetCreatedJavaVMs_fnptr JNI_GetCreatedJavaVMs_fn; + +static void *jni_dl = 0; + +static const char *last_error = 0; + +static int load_sym(const char *sym, void **ptr) { + void *v = dlsym(jni_dl, sym); + if (!v) { + last_error = dlerror(); + dlclose(jni_dl); + jni_dl = 0; + return -1; + } + *ptr = v; + return 0; +} + +/* path: path to libjvm + returns: 0 = success, -1 = already loaded, -2 = dlopen error, -3 symbols not found */ +int djni_load(const char *path) { + last_error = 0; + if (jni_dl) return -1; + jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW); + if (!jni_dl) { + last_error = dlerror(); + return -2; + } + if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void**) &JNI_GetDefaultJavaVMInitArgs_fn) || + load_sym("JNI_CreateJavaVM", (void**) &JNI_CreateJavaVM_fn) || + load_sym("JNI_GetCreatedJavaVMs", (void**) &JNI_GetCreatedJavaVMs_fn)) { + JNI_GetDefaultJavaVMInitArgs_fn = 0; + JNI_CreateJavaVM_fn = 0; + JNI_GetCreatedJavaVMs_fn = 0; + return -3; + } + return 0; +} + +/* returns: 0 = success, -1 = noting loaded, -2 = unload failed */ +int djni_unload(void) { + if (jni_dl) { + if (dlclose(jni_dl)) + return -2; + jni_dl = 0; + return 0; + } + return -1; +} + +/* The following are the "normal" JNI API calls which are routed to libjvm JNI API. + If no JNI was loaded, they return -99 to distinguish it from JNI error codes. */ + +jint JNI_GetDefaultJavaVMInitArgs(void *args) { + return JNI_GetDefaultJavaVMInitArgs_fn ? JNI_GetDefaultJavaVMInitArgs_fn(args) : -99; +} + +jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args) { + return JNI_CreateJavaVM_fn ? JNI_CreateJavaVM_fn(pvm, penv, args) : -99; +} + +jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { + return JNI_GetCreatedJavaVMs_fn ? JNI_GetCreatedJavaVMs_fn(vmBuf, bufLen, nVMs) : -99; +} + +/* define DJNI_MAIN_TEST to test the load + create + print version + destroy + unload cycle + it will create an executable - just compile djni.c alone for this (not used in rJava) */ +#ifdef DJNI_MAIN_TEST + +#include +#include +#include + +/* calls java.lang.System.getProperty("java.version") and returns the result or NULL + if something went wrong. Any non-NULL returns must be free()d */ +static char *java_version(JNIEnv *eenv) { + char *jver = 0; + jclass sys = (*eenv)->FindClass(eenv, "java/lang/System"); + if (sys) { + jmethodID mid = (*eenv)->GetStaticMethodID(eenv, sys, "getProperty", "(Ljava/lang/String;)Ljava/lang/String;"); + if (mid) { + jstring jvs = (*eenv)->NewStringUTF(eenv, "java.version"); + if (jvs) { + jobject jres = (*eenv)->CallStaticObjectMethod(eenv, sys, mid, jvs); + (*eenv)->DeleteLocalRef(eenv, jvs); + if (jres) { + const char *str = (*eenv)->GetStringUTFChars(eenv, (jstring) jres, NULL); + if (str) { + jver = strdup(str); + (*eenv)->ReleaseStringUTFChars(eenv, (jstring) jres, str); + } + (*eenv)->DeleteLocalRef(eenv, jres); + } + } + } + (*eenv)->DeleteLocalRef(eenv, sys); + } + return jver; +} + +/* create a JVM, call java_version(), destroy the JVM */ +static int run_jvm_test(void) { + JavaVM *jvm; + JNIEnv *eenv; + JavaVMOption vm_options[4]; + JavaVMInitArgs vm_args; + vm_args.version = JNI_VERSION_1_2; + if(JNI_GetDefaultJavaVMInitArgs(&vm_args) != JNI_OK) { + fprintf(stderr, "JNI 1.2 or higher is required\n"); + return -1; + } + vm_args.ignoreUnrecognized = JNI_TRUE; + vm_args.options = vm_options; + vm_args.nOptions = 0; + jint res = JNI_CreateJavaVM(&jvm,(void **)&eenv, &vm_args); + if (res != 0) { + fprintf(stderr, "Cannot create Java virtual machine (JNI_CreateJavaVM returned %d)\n", (int) res); + return -1; + } + if (!eenv) { + fprintf(stderr, "Cannot obtain JVM environment"); + return -1; + } + printf("JVM initialized, JNI version: %x\n", (*eenv)->GetVersion(eenv)); + { + char *jver = java_version(eenv); + if (jver) { + printf("Java version: %s\n", jver); + free(jver); + } + } + /* this shouldn't be even needed, but was last-ditch effort - makes no difference */ + if ((*jvm)->DetachCurrentThread(jvm) != JNI_OK) + fprintf(stderr, "FWIW: cannot detach thread.\n"); + /* destroy the VM */ + if ((*jvm)->DestroyJavaVM(jvm) != JNI_OK) { + fprintf(stderr, "ERROR: cannot destroy VM\n"); + return -1; + } + printf("JVM destroyed.\n"); + return 0; +} + +int test_jvm(const char *path) { + int e = djni_load(path); + if (e) { + printf("Cannot load: %d (%s)\n", e, last_error ? last_error : "not a dl error"); + return 1; + } + printf("JVM Loaded!\n"); + if (!run_jvm_test()) { + if (djni_unload()) + fprintf(stderr, "ERROR: cannot unload JNI (%s)\n", last_error ? last_error : "no information"); + else + printf("JNI unloaded.\n"); + } + return 0; +} + +/* for debugging on macOS - prints all dynamic loads */ +#ifdef __APPLE__ +#include + +static void add_image(const struct mach_header* mh, intptr_t vmaddr_slide) { + Dl_info DlInfo; + dladdr(mh, &DlInfo); + printf("LOADED: %s\n", DlInfo.dli_fname); +} + +static void rm_image(const struct mach_header* mh, intptr_t vmaddr_slide) { + Dl_info DlInfo; + dladdr(mh, &DlInfo); + printf("REMOVE: %s\n", DlInfo.dli_fname); +} +#endif + +int main(int ac, char **av) { +#ifdef __APPLE__ + _dyld_register_func_for_add_image(add_image); + _dyld_register_func_for_remove_image(rm_image); +#endif + + test_jvm("/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home/lib/server/libjvm.dylib"); +#ifdef SAME_VM /* option 1: try the same VM twice (will load but fail to create VM) */ + test_jvm("/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home/lib/server/libjvm.dylib"); +#else /* option 2: will even fail to load */ + test_jvm("/Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home/lib/server/libjvm.dylib"); +#endif + return 0; +} + +#endif diff --git a/src/djni.h b/src/djni.h new file mode 100644 index 0000000..a1ba697 --- /dev/null +++ b/src/djni.h @@ -0,0 +1,19 @@ +/* Dynamic JNI + Allows to load JVM dynamically into a process. + + (C)2024 Simon Urbanek + License: LGPL-2.1 or MIT +*/ +#ifndef DJNI_H__ +#define DJNI_H__ + +#include "jni.h" + +int djni_load(const char *path); +int djni_unload(void); + +jint JNI_GetDefaultJavaVMInitArgs(void *args); +jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); +jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs); + +#endif diff --git a/src/jni.h b/src/jni.h new file mode 100644 index 0000000..1db2a39 --- /dev/null +++ b/src/jni.h @@ -0,0 +1,434 @@ +/* JNI compatibility header - based on JNI 1.2 specification */ + +#ifndef RJAVA_JNI_H__ +#define RJAVA_JNI_H__ + +/* This header only matches JNI 1.2 - we don't need more + (and we don't support anything lower due to the way the + JVM initialization was changed in 1.2) */ +#define JNI_VERSION_1_2 0x00010002 + +/* call API uses va_list */ +#include + +/* basic native types */ +#ifdef _LP64 +typedef long jlong; +#else +typedef long long jlong; +#endif + +/* Windows support is currently untested */ +#ifdef WIN32 +#define JNICALL __stdcall +#else +#define JNICALL +#endif + +typedef signed char jbyte; +typedef int jint; +typedef unsigned char jboolean; +typedef unsigned short jchar; +typedef short jshort; +typedef float jfloat; +typedef double jdouble; +typedef jint jsize; + +struct jni_jobject; + +typedef struct jni_jobject *jobject; +typedef jobject jclass; +typedef jobject jthrowable; +typedef jobject jstring; +typedef jobject jarray; +typedef jobject jweak; +typedef jarray jbooleanArray; +typedef jarray jbyteArray; +typedef jarray jcharArray; +typedef jarray jshortArray; +typedef jarray jintArray; +typedef jarray jlongArray; +typedef jarray jfloatArray; +typedef jarray jdoubleArray; +typedef jarray jobjectArray; + +typedef union jvalue { + jboolean z; + jbyte b; + jchar c; + jshort s; + jint i; + jlong j; + jfloat f; + jdouble d; + jobject l; +} jvalue; + +/* basic defines */ +#define JNI_FALSE 0 +#define JNI_TRUE 1 +#define JNI_OK 0 +#define JNI_ERR (-1) + +/* additonal types */ +struct jni_jfieldID; +typedef struct jni_jfieldID *jfieldID; + +struct jni_jmethodID; +typedef struct jni_jmethodID *jmethodID; + +typedef struct { + char *name; + char *signature; + void *fnPtr; +} JNINativeMethod; + +struct JNINativeInterface_; +struct JNIEnv_; + +typedef const struct JNINativeInterface_ *JNIEnv; + +struct JavaVM_; + +typedef const struct JNIInvokeInterface_ *JavaVM; + +typedef struct JavaVMOption { + char *optionString; + void *extraInfo; +} JavaVMOption; + +typedef struct JavaVMInitArgs { + jint version; + jint nOptions; + JavaVMOption *options; + jboolean ignoreUnrecognized; +} JavaVMInitArgs; + +typedef struct JavaVMAttachArgs { + jint version; + char *name; + jobject group; +} JavaVMAttachArgs; + +struct JNIInvokeInterface_ { + void *reserved0; + void *reserved1; + void *reserved2; + + jint (JNICALL *DestroyJavaVM)(JavaVM *vm); + jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args); + jint (JNICALL *DetachCurrentThread)(JavaVM *vm); + jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); + jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args); +}; + +struct JavaVM_ { + const struct JNIInvokeInterface_ *functions; +}; + +/* typedefs for the direct API symbols */ +typedef jint (JNICALL *JNI_GetDefaultJavaVMInitArgs_fnptr)(void *); +typedef jint (JNICALL *JNI_CreateJavaVM_fnptr)(JavaVM **, void **, void *); +typedef jint (JNICALL *JNI_GetCreatedJavaVMs_fnptr)(JavaVM **, jsize, jsize *); + +/* actual JNI interface */ +struct JNINativeInterface_ { + void *reserved0; + void *reserved1; + void *reserved2; + void *reserved3; + jint (JNICALL *GetVersion)(JNIEnv *env); + + jclass (JNICALL *DefineClass)(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len); + jclass (JNICALL *FindClass)(JNIEnv *env, const char *name); + + jmethodID (JNICALL *FromReflectedMethod)(JNIEnv *env, jobject method); + jfieldID (JNICALL *FromReflectedField)(JNIEnv *env, jobject field); + + jobject (JNICALL *ToReflectedMethod)(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic); + + jclass (JNICALL *GetSuperclass)(JNIEnv *env, jclass sub); + jboolean (JNICALL *IsAssignableFrom)(JNIEnv *env, jclass sub, jclass sup); + + jobject (JNICALL *ToReflectedField)(JNIEnv *env, jclass cls, jfieldID fieldID, jboolean isStatic); + + jint (JNICALL *Throw)(JNIEnv *env, jthrowable obj); + jint (JNICALL *ThrowNew)(JNIEnv *env, jclass clazz, const char *msg); + jthrowable (JNICALL *ExceptionOccurred)(JNIEnv *env); + void (JNICALL *ExceptionDescribe)(JNIEnv *env); + void (JNICALL *ExceptionClear)(JNIEnv *env); + void (JNICALL *FatalError)(JNIEnv *env, const char *msg); + + jint (JNICALL *PushLocalFrame)(JNIEnv *env, jint capacity); + jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result); + + jobject (JNICALL *NewGlobalRef)(JNIEnv *env, jobject lobj); + void (JNICALL *DeleteGlobalRef)(JNIEnv *env, jobject gref); + void (JNICALL *DeleteLocalRef)(JNIEnv *env, jobject obj); + jboolean (JNICALL *IsSameObject)(JNIEnv *env, jobject obj1, jobject obj2); + jobject (JNICALL *NewLocalRef)(JNIEnv *env, jobject ref); + jint (JNICALL *EnsureLocalCapacity)(JNIEnv *env, jint capacity); + + jobject (JNICALL *AllocObject)(JNIEnv *env, jclass clazz); + jobject (JNICALL *NewObject)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jobject (JNICALL *NewObjectV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jobject (JNICALL *NewObjectA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jclass (JNICALL *GetObjectClass)(JNIEnv *env, jobject obj); + jboolean (JNICALL *IsInstanceOf)(JNIEnv *env, jobject obj, jclass clazz); + + jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); + + jobject (JNICALL *CallObjectMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jobject (JNICALL *CallObjectMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jobject (JNICALL *CallObjectMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue * args); + + jboolean (JNICALL *CallBooleanMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jboolean (JNICALL *CallBooleanMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jboolean (JNICALL *CallBooleanMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue * args); + + jbyte (JNICALL *CallByteMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jbyte (JNICALL *CallByteMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jbyte (JNICALL *CallByteMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + jchar (JNICALL *CallCharMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jchar (JNICALL *CallCharMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jchar (JNICALL *CallCharMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + jshort (JNICALL *CallShortMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jshort (JNICALL *CallShortMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jshort (JNICALL *CallShortMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + jint (JNICALL *CallIntMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jint (JNICALL *CallIntMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jint (JNICALL *CallIntMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + jlong (JNICALL *CallLongMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jlong (JNICALL *CallLongMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jlong (JNICALL *CallLongMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + jfloat (JNICALL *CallFloatMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jfloat (JNICALL *CallFloatMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jfloat (JNICALL *CallFloatMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + jdouble (JNICALL *CallDoubleMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + jdouble (JNICALL *CallDoubleMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + jdouble (JNICALL *CallDoubleMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); + + void (JNICALL *CallVoidMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); + void (JNICALL *CallVoidMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); + void (JNICALL *CallVoidMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue * args); + + jobject (JNICALL *CallNonvirtualObjectMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jobject (JNICALL *CallNonvirtualObjectMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jobject (JNICALL *CallNonvirtualObjectMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue * args); + + jboolean (JNICALL *CallNonvirtualBooleanMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jboolean (JNICALL *CallNonvirtualBooleanMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jboolean (JNICALL *CallNonvirtualBooleanMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue * args); + + jbyte (JNICALL *CallNonvirtualByteMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jbyte (JNICALL *CallNonvirtualByteMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jbyte (JNICALL *CallNonvirtualByteMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args); + + jchar (JNICALL *CallNonvirtualCharMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jchar (JNICALL *CallNonvirtualCharMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jchar (JNICALL *CallNonvirtualCharMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args); + + jshort (JNICALL *CallNonvirtualShortMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jshort (JNICALL *CallNonvirtualShortMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jshort (JNICALL *CallNonvirtualShortMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args); + + jint (JNICALL *CallNonvirtualIntMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jint (JNICALL *CallNonvirtualIntMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jint (JNICALL *CallNonvirtualIntMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args); + + jlong (JNICALL *CallNonvirtualLongMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jlong (JNICALL *CallNonvirtualLongMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jlong (JNICALL *CallNonvirtualLongMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args); + + jfloat (JNICALL *CallNonvirtualFloatMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jfloat (JNICALL *CallNonvirtualFloatMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jfloat (JNICALL *CallNonvirtualFloatMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args); + + jdouble (JNICALL *CallNonvirtualDoubleMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + jdouble (JNICALL *CallNonvirtualDoubleMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + jdouble (JNICALL *CallNonvirtualDoubleMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, + const jvalue *args); + + void (JNICALL *CallNonvirtualVoidMethod)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); + void (JNICALL *CallNonvirtualVoidMethodV)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + void (JNICALL *CallNonvirtualVoidMethodA)(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue * args); + + jfieldID (JNICALL *GetFieldID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); + + jobject (JNICALL *GetObjectField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jboolean (JNICALL *GetBooleanField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jbyte (JNICALL *GetByteField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jchar (JNICALL *GetCharField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jshort (JNICALL *GetShortField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jint (JNICALL *GetIntField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jlong (JNICALL *GetLongField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jfloat (JNICALL *GetFloatField)(JNIEnv *env, jobject obj, jfieldID fieldID); + jdouble (JNICALL *GetDoubleField)(JNIEnv *env, jobject obj, jfieldID fieldID); + + void (JNICALL *SetObjectField)(JNIEnv *env, jobject obj, jfieldID fieldID, jobject val); + void (JNICALL *SetBooleanField)(JNIEnv *env, jobject obj, jfieldID fieldID, jboolean val); + void (JNICALL *SetByteField)(JNIEnv *env, jobject obj, jfieldID fieldID, jbyte val); + void (JNICALL *SetCharField)(JNIEnv *env, jobject obj, jfieldID fieldID, jchar val); + void (JNICALL *SetShortField)(JNIEnv *env, jobject obj, jfieldID fieldID, jshort val); + void (JNICALL *SetIntField)(JNIEnv *env, jobject obj, jfieldID fieldID, jint val); + void (JNICALL *SetLongField)(JNIEnv *env, jobject obj, jfieldID fieldID, jlong val); + void (JNICALL *SetFloatField)(JNIEnv *env, jobject obj, jfieldID fieldID, jfloat val); + void (JNICALL *SetDoubleField)(JNIEnv *env, jobject obj, jfieldID fieldID, jdouble val); + + jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); + + jobject (JNICALL *CallStaticObjectMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jobject (JNICALL *CallStaticObjectMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jobject (JNICALL *CallStaticObjectMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jboolean (JNICALL *CallStaticBooleanMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jboolean (JNICALL *CallStaticBooleanMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jboolean (JNICALL *CallStaticBooleanMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jbyte (JNICALL *CallStaticByteMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jbyte (JNICALL *CallStaticByteMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jbyte (JNICALL *CallStaticByteMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jchar (JNICALL *CallStaticCharMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jchar (JNICALL *CallStaticCharMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jchar (JNICALL *CallStaticCharMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jshort (JNICALL *CallStaticShortMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jshort (JNICALL *CallStaticShortMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jshort (JNICALL *CallStaticShortMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jint (JNICALL *CallStaticIntMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jint (JNICALL *CallStaticIntMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jint (JNICALL *CallStaticIntMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jlong (JNICALL *CallStaticLongMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jlong (JNICALL *CallStaticLongMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jlong (JNICALL *CallStaticLongMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jfloat (JNICALL *CallStaticFloatMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jfloat (JNICALL *CallStaticFloatMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jfloat (JNICALL *CallStaticFloatMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + jdouble (JNICALL *CallStaticDoubleMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); + jdouble (JNICALL *CallStaticDoubleMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); + jdouble (JNICALL *CallStaticDoubleMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args); + + void (JNICALL *CallStaticVoidMethod)(JNIEnv *env, jclass cls, jmethodID methodID, ...); + void (JNICALL *CallStaticVoidMethodV)(JNIEnv *env, jclass cls, jmethodID methodID, va_list args); + void (JNICALL *CallStaticVoidMethodA)(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue * args); + + jfieldID (JNICALL *GetStaticFieldID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); + jobject (JNICALL *GetStaticObjectField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jboolean (JNICALL *GetStaticBooleanField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jbyte (JNICALL *GetStaticByteField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jchar (JNICALL *GetStaticCharField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jshort (JNICALL *GetStaticShortField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jint (JNICALL *GetStaticIntField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jlong (JNICALL *GetStaticLongField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jfloat (JNICALL *GetStaticFloatField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + jdouble (JNICALL *GetStaticDoubleField)(JNIEnv *env, jclass clazz, jfieldID fieldID); + + void (JNICALL *SetStaticObjectField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jobject value); + void (JNICALL *SetStaticBooleanField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jboolean value); + void (JNICALL *SetStaticByteField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jbyte value); + void (JNICALL *SetStaticCharField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jchar value); + void (JNICALL *SetStaticShortField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jshort value); + void (JNICALL *SetStaticIntField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jint value); + void (JNICALL *SetStaticLongField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jlong value); + void (JNICALL *SetStaticFloatField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jfloat value); + void (JNICALL *SetStaticDoubleField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jdouble value); + + jstring (JNICALL *NewString)(JNIEnv *env, const jchar *unicode, jsize len); + jsize (JNICALL *GetStringLength)(JNIEnv *env, jstring str); + const jchar *(JNICALL *GetStringChars)(JNIEnv *env, jstring str, jboolean *isCopy); + void (JNICALL *ReleaseStringChars)(JNIEnv *env, jstring str, const jchar *chars); + + jstring (JNICALL *NewStringUTF)(JNIEnv *env, const char *utf); + jsize (JNICALL *GetStringUTFLength)(JNIEnv *env, jstring str); + const char* (JNICALL *GetStringUTFChars)(JNIEnv *env, jstring str, jboolean *isCopy); + void (JNICALL *ReleaseStringUTFChars)(JNIEnv *env, jstring str, const char* chars); + + + jsize (JNICALL *GetArrayLength)(JNIEnv *env, jarray array); + + jobjectArray (JNICALL *NewObjectArray)(JNIEnv *env, jsize len, jclass clazz, jobject init); + jobject (JNICALL *GetObjectArrayElement)(JNIEnv *env, jobjectArray array, jsize index); + void (JNICALL *SetObjectArrayElement)(JNIEnv *env, jobjectArray array, jsize index, jobject val); + + jbooleanArray (JNICALL *NewBooleanArray)(JNIEnv *env, jsize len); + jbyteArray (JNICALL *NewByteArray)(JNIEnv *env, jsize len); + jcharArray (JNICALL *NewCharArray)(JNIEnv *env, jsize len); + jshortArray (JNICALL *NewShortArray)(JNIEnv *env, jsize len); + jintArray (JNICALL *NewIntArray)(JNIEnv *env, jsize len); + jlongArray (JNICALL *NewLongArray)(JNIEnv *env, jsize len); + jfloatArray (JNICALL *NewFloatArray)(JNIEnv *env, jsize len); + jdoubleArray (JNICALL *NewDoubleArray)(JNIEnv *env, jsize len); + + jboolean * (JNICALL *GetBooleanArrayElements)(JNIEnv *env, jbooleanArray array, jboolean *isCopy); + jbyte * (JNICALL *GetByteArrayElements)(JNIEnv *env, jbyteArray array, jboolean *isCopy); + jchar * (JNICALL *GetCharArrayElements)(JNIEnv *env, jcharArray array, jboolean *isCopy); + jshort * (JNICALL *GetShortArrayElements)(JNIEnv *env, jshortArray array, jboolean *isCopy); + jint * (JNICALL *GetIntArrayElements)(JNIEnv *env, jintArray array, jboolean *isCopy); + jlong * (JNICALL *GetLongArrayElements)(JNIEnv *env, jlongArray array, jboolean *isCopy); + jfloat * (JNICALL *GetFloatArrayElements)(JNIEnv *env, jfloatArray array, jboolean *isCopy); + jdouble * (JNICALL *GetDoubleArrayElements)(JNIEnv *env, jdoubleArray array, jboolean *isCopy); + + void (JNICALL *ReleaseBooleanArrayElements)(JNIEnv *env, jbooleanArray array, jboolean *elems, jint mode); + void (JNICALL *ReleaseByteArrayElements)(JNIEnv *env, jbyteArray array, jbyte *elems, jint mode); + void (JNICALL *ReleaseCharArrayElements)(JNIEnv *env, jcharArray array, jchar *elems, jint mode); + void (JNICALL *ReleaseShortArrayElements)(JNIEnv *env, jshortArray array, jshort *elems, jint mode); + void (JNICALL *ReleaseIntArrayElements)(JNIEnv *env, jintArray array, jint *elems, jint mode); + void (JNICALL *ReleaseLongArrayElements)(JNIEnv *env, jlongArray array, jlong *elems, jint mode); + void (JNICALL *ReleaseFloatArrayElements)(JNIEnv *env, jfloatArray array, jfloat *elems, jint mode); + void (JNICALL *ReleaseDoubleArrayElements)(JNIEnv *env, jdoubleArray array, jdouble *elems, jint mode); + + void (JNICALL *GetBooleanArrayRegion)(JNIEnv *env, jbooleanArray array, jsize start, jsize l, jboolean *buf); + void (JNICALL *GetByteArrayRegion)(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf); + void (JNICALL *GetCharArrayRegion)(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf); + void (JNICALL *GetShortArrayRegion)(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf); + void (JNICALL *GetIntArrayRegion)(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf); + void (JNICALL *GetLongArrayRegion)(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf); + void (JNICALL *GetFloatArrayRegion)(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf); + void (JNICALL *GetDoubleArrayRegion)(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf); + + void (JNICALL *SetBooleanArrayRegion)(JNIEnv *env, jbooleanArray array, jsize start, jsize l, const jboolean *buf); + void (JNICALL *SetByteArrayRegion)(JNIEnv *env, jbyteArray array, jsize start, jsize len, const jbyte *buf); + void (JNICALL *SetCharArrayRegion)(JNIEnv *env, jcharArray array, jsize start, jsize len, const jchar *buf); + void (JNICALL *SetShortArrayRegion)(JNIEnv *env, jshortArray array, jsize start, jsize len, const jshort *buf); + void (JNICALL *SetIntArrayRegion)(JNIEnv *env, jintArray array, jsize start, jsize len, const jint *buf); + void (JNICALL *SetLongArrayRegion)(JNIEnv *env, jlongArray array, jsize start, jsize len, const jlong *buf); + void (JNICALL *SetFloatArrayRegion)(JNIEnv *env, jfloatArray array, jsize start, jsize len, const jfloat *buf); + void (JNICALL *SetDoubleArrayRegion)(JNIEnv *env, jdoubleArray array, jsize start, jsize len, const jdouble *buf); + + jint (JNICALL *RegisterNatives)(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods); + jint (JNICALL *UnregisterNatives)(JNIEnv *env, jclass clazz); + + jint (JNICALL *MonitorEnter)(JNIEnv *env, jobject obj); + jint (JNICALL *MonitorExit)(JNIEnv *env, jobject obj); + + jint (JNICALL *GetJavaVM)(JNIEnv *env, JavaVM **vm); + + /* new since JNI 1.2 (we don't really use any of those) */ + void (JNICALL *GetStringRegion)(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf); + void (JNICALL *GetStringUTFRegion)(JNIEnv *env, jstring str, jsize start, jsize len, char *buf); + + void * (JNICALL *GetPrimitiveArrayCritical)(JNIEnv *env, jarray array, jboolean *isCopy); + void (JNICALL *ReleasePrimitiveArrayCritical)(JNIEnv *env, jarray array, void *carray, jint mode); + + const jchar * (JNICALL *GetStringCritical)(JNIEnv *env, jstring string, jboolean *isCopy); + void (JNICALL *ReleaseStringCritical)(JNIEnv *env, jstring string, const jchar *cstring); + + jweak (JNICALL *NewWeakGlobalRef)(JNIEnv *env, jobject obj); + void (JNICALL *DeleteWeakGlobalRef)(JNIEnv *env, jweak ref); + + jboolean (JNICALL *ExceptionCheck)(JNIEnv *env); +}; + +#endif From 95d0fe3d7d66cf356687381acb39dc4191910535 Mon Sep 17 00:00:00 2001 From: Simon Urbanek Date: Wed, 18 Sep 2024 15:20:55 +1200 Subject: [PATCH 02/63] add dynamic JNI load/unload support --- R/jfirst.R | 2 +- R/jinit.R | 33 +++++++++++++++++++++++++++++++++ mkdist | 1 + src/Rglue.c | 35 +++++++++++++++++++++++++++++++++-- src/djni.c | 10 ++++++++++ src/djni.h | 5 +++++ src/init.c | 41 +++++++++++++++++++++++++++++------------ src/rJava.h | 6 ++++-- 8 files changed, 116 insertions(+), 17 deletions(-) diff --git a/R/jfirst.R b/R/jfirst.R index 8f40142..a32fada 100644 --- a/R/jfirst.R +++ b/R/jfirst.R @@ -22,7 +22,7 @@ "RgetShortArrayCont", "RgetStringArrayCont", "RidenticalRef", "RgetSimpleClassNames", "RisAssignableFrom", "RpollException", "RsetField", "RthrowException", "javaObjectCache", "initRJavaTools", "newRJavaLookupTable", "useDynamicSymbols", - "RgetJVMstate", + "RgetJVMstate", "RloadJVM", "RunloadJVM", # .External "RcreateObject", "RgetStringValue", "RinitJVM", "RtoString", "RcallMethod", # .C diff --git a/R/jinit.R b/R/jinit.R index 736fc8e..9a53b4b 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -9,8 +9,41 @@ .need.init <- function() .Call(RJava_needs_init) +.jloadJVM <- function(path, silent=FALSE) + .Call(RloadJVM, path.expand(path), silent) + +.junloadJVM <- function() + .Call(RunloadJVM) + +.jfindAndLoadJVM <- function() { + home <- Sys.getenv("JAVA_HOME") + if (!nzchar(home) || !isTRUE(dir.exists(home))) { + ## try java_home first if present + if (file.exists("/usr/libexec/java_home")) { + home <- tryCatch(system("/usr/libexec/java_home", intern=TRUE, ignore.stderr=TRUE), + error=function(...) "") + } + ## otherwise rely on java in PATH + if (!nzchar(home) || !isTRUE(dir.exists(home))) { + home <- tryCatch(system(paste("java", "-cp", + shQuote(system.file("java", package="rJava")), "getsp", "java.home"), + intern=TRUE, ignore.stderr=TRUE), error=function(...) "") + } + } + if (!nzchar(home) || !isTRUE(dir.exists(home))) + stop("Cannot find Java. Either make sure java executable is on the PATH or set the JAVA_HOME environment variable.") + libjvm <- list.files(home, "^libjvm[.]", recursive=TRUE, full.names=TRUE) + if (!length(libjvm)) + stop("Cannot find libjvm in ", home) + message("Loading JVM from ", home) + .jloadJVM(libjvm) +} + ## initialization .jinit <- function(classpath=NULL, parameters=getOption("java.parameters", NA), ..., silent=FALSE, force.init=FALSE) { + st <- .jvmState() + if (st$state == "not-loaded") + .jfindAndLoadJVM() running.classpath <- character() if (!.need.init()) { running.classpath <- .jclassPath() diff --git a/mkdist b/mkdist index 2574b31..503a1be 100644 --- a/mkdist +++ b/mkdist @@ -166,6 +166,7 @@ echo "Copy compiled Java classes ..." # copy all complied Java classes and sources cp src/java/*.class inst/java cp src/java/*.java inst/java +cp tools/getsp* inst/java # move RJavaClassLoader into boot area since it will be loaded by the system class loader mv inst/java/RJavaClassLoader* inst/java/boot # move javadoc directory diff --git a/src/Rglue.c b/src/Rglue.c index c76da2f..a514ad9 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1121,8 +1121,11 @@ REPC SEXP RgetJVMstate(void) { const char *st = "unknown"; switch (rJava_JVM_state) { case JVM_STATE_NONE: /* could be detached */ - st = (existingJVMs() > 0) ? "detached" : "none"; - break; + { + int evm = existingJVMs(); + st = (evm == -1) ? "not-loaded" : ((evm > 0) ? "detached" : "none"); + break; + } case JVM_STATE_CREATED: st = "created"; break; case JVM_STATE_ATTACHED: st = "attached"; break; case JVM_STATE_DEAD: st = "dead"; break; @@ -1133,3 +1136,31 @@ REPC SEXP RgetJVMstate(void) { UNPROTECT(1); return res; } + +REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { + const char *path; + int res, silent = Rf_asInteger(sSilent); + if (TYPEOF(sPath) != STRSXP || LENGTH(sPath) != 1) + Rf_error("Invalid path to JVM library"); + /* FIXME: worry about encoding? */ + path = CHAR(STRING_ELT(sPath, 0)); + res = djni_load(path); + if (silent) + return Rf_ScalarInteger(res); + switch(res) { + case -1: Rf_error("JVM run-time is already loaded"); + case -2: Rf_error("Cannot load JVM run-time (%s)", djni_last_error() ? djni_last_error() : "unsuccessful"); + case -3: Rf_error("JVM run-time does not have required entry points"); + } + return Rf_ScalarLogical(1); +} + +REPC SEXP RunloadJVM(void) { + int res; + if (rJava_JVM_state == JVM_STATE_CREATED || + rJava_JVM_state == JVM_STATE_ATTACHED) { + destroyJVM(); + } + res = djni_unload(); + return Rf_ScalarLogical((res < -1) ? 0 : 1); +} diff --git a/src/djni.c b/src/djni.c index 0675868..c35655f 100644 --- a/src/djni.c +++ b/src/djni.c @@ -38,6 +38,8 @@ int djni_load(const char *path) { JNI_GetDefaultJavaVMInitArgs_fn = 0; JNI_CreateJavaVM_fn = 0; JNI_GetCreatedJavaVMs_fn = 0; + dlclose(jni_dl); + jni_dl = 0; return -3; } return 0; @@ -54,6 +56,14 @@ int djni_unload(void) { return -1; } +int djni_loaded(void) { + return jni_dl ? 1 : 0; +} + +const char* djni_last_error(void) { + return last_error; +} + /* The following are the "normal" JNI API calls which are routed to libjvm JNI API. If no JNI was loaded, they return -99 to distinguish it from JNI error codes. */ diff --git a/src/djni.h b/src/djni.h index a1ba697..8abfa7d 100644 --- a/src/djni.h +++ b/src/djni.h @@ -9,8 +9,13 @@ #include "jni.h" +/* returned if no run-time is loaded */ +#define JNI_NO_DJNI -99 + int djni_load(const char *path); int djni_unload(void); +const char* djni_last_error(void); +int djni_loaded(void); jint JNI_GetDefaultJavaVMInitArgs(void *args); jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); diff --git a/src/init.c b/src/init.c index d44d92d..8f76143 100644 --- a/src/init.c +++ b/src/init.c @@ -89,10 +89,12 @@ static void JNICALL exit_hook(int status) { exit(status); } +/* -1 if no run-time is loaded, otherwise number of active VMs */ int existingJVMs(void) { jsize vms = 0; JavaVM *jvms[32]; - return (JNI_GetCreatedJavaVMs(jvms, 32, &vms) >= 0) ? vms : 0; + jint res = JNI_GetCreatedJavaVMs(jvms, 32, &vms); + return (res == JNI_NO_DJNI) ? -1 : ((res >= 0) ? vms : 0); } /* in reality WIN64 implies WIN32 but to make sure ... */ @@ -118,7 +120,9 @@ static int initJVM(const char *user_classpath, int opts, char **optv, int hooks, if(!user_classpath) user_classpath = ""; vm_args.version = JNI_VERSION_1_2; - if(JNI_GetDefaultJavaVMInitArgs(&vm_args) != JNI_OK) { + if((res = JNI_GetDefaultJavaVMInitArgs(&vm_args)) != JNI_OK) { + if (res == JNI_NO_DJNI) /* DJNI has not loaded a run-time yet */ + error("No Java run-time is not loaded."); error("JNI 1.2 or higher is required"); return -1; } @@ -193,8 +197,11 @@ static int initJVM(const char *user_classpath, int opts, char **optv, int hooks, if (disableGuardPages && (res != 0 || !eenv)) return -2; /* perhaps this VM does not allow disabling guard pages */ - if (res != 0) - error("Cannot create Java virtual machine (JNI_CreateJavaVM returned %ld)", (long int) res); + if (res != 0) { + if (rJava_JVM_state != JVM_STATE_NONE) + Rf_warning("Attempt to re-create a second JVM in a process - most known Java implementations do not support it."); + Rf_error("Cannot create Java virtual machine (JNI_CreateJavaVM returned %d)", (int) res); + } if (!eenv) error("Cannot obtain JVM environment"); @@ -760,21 +767,31 @@ static SEXP RinitJVM_jsw(SEXP par) { /** RinitJVM(classpath) initializes JVM with the specified class path */ REP SEXP RinitJVM(SEXP par) { - + if (!djni_loaded()) + Rf_error("No Java run-time is loaded."); #ifndef JVM_STACK_WORKAROUND - return RinitJVM_real(par, 0); + return RinitJVM_real(par, 0); #else - return RinitJVM_jsw(par); + return RinitJVM_jsw(par); #endif } -REP void doneJVM(void) { - (*jvm)->DestroyJavaVM(jvm); - jvm = 0; - eenv = 0; - rJava_JVM_state = JVM_STATE_DESTROYED; +/* detaches (if only attached) or destroys (if created) JVM */ +HIDE void destroyJVM(void) { + if (!jvm) + return; + if (rJava_JVM_state == JVM_STATE_CREATED || + rJava_JVM_state == JVM_STATE_ATTACHED) { + (*jvm)->DetachCurrentThread(jvm); + if (rJava_JVM_state == JVM_STATE_CREATED) + (*jvm)->DestroyJavaVM(jvm); + jvm = 0; + eenv = 0; + rJava_JVM_state = (rJava_JVM_state == JVM_STATE_CREATED) ? JVM_STATE_DESTROYED : JVM_STATE_DETACHED; + } } + /** * Initializes the cached values of classes and methods used internally * These classes and methods are the ones that are in rJava (RJavaTools, ...) diff --git a/src/rJava.h b/src/rJava.h index 15c2337..783f027 100644 --- a/src/rJava.h +++ b/src/rJava.h @@ -19,7 +19,7 @@ 0.2 - uses S4 classes 0.1 - first public release */ -#include +#include "djni.h" #include #include #include @@ -177,7 +177,8 @@ extern int rJava_initialized; #define JVM_STATE_CREATED 1 /* JVM was created by us */ #define JVM_STATE_ATTACHED 2 /* we attached to another JVM */ #define JVM_STATE_DEAD 4 /* set when Java exit handler was called */ -#define JVM_STATE_DESTROYED 8 /* JVM was destroyed */ +#define JVM_STATE_DETACHED 8 /* detached from existing JVM */ +#define JVM_STATE_DESTROYED 9 /* JVM was destroyed */ extern int rJava_JVM_state; @@ -205,6 +206,7 @@ extern jmethodID mid_RJavaImport_lookup ; extern jmethodID mid_RJavaImport_exists ; HIDE void init_rJava(void); +HIDE void destroyJVM(void); /* in otables.c */ // turn this for debugging in otables.c From da504fcd2fa59eb991f797cc6f7fd12dc6178c8c Mon Sep 17 00:00:00 2001 From: Simon Urbanek Date: Wed, 18 Sep 2024 16:19:42 +1200 Subject: [PATCH 03/63] make dynload the default in this branch; minor fixes --- R/jinit.R | 2 +- R/zzz.R.in | 49 +------------------------------------------------ configure.ac | 7 +++++-- src/rJava.c | 5 +++++ src/rjstring.h | 2 +- 5 files changed, 13 insertions(+), 52 deletions(-) diff --git a/R/jinit.R b/R/jinit.R index 9a53b4b..e45cc2c 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -36,7 +36,7 @@ if (!length(libjvm)) stop("Cannot find libjvm in ", home) message("Loading JVM from ", home) - .jloadJVM(libjvm) + .jloadJVM(libjvm[1]) } ## initialization diff --git a/R/zzz.R.in b/R/zzz.R.in index 93d0643..284afde 100644 --- a/R/zzz.R.in +++ b/R/zzz.R.in @@ -1,52 +1,5 @@ +# this should no longer be needed as an autoconf-edited file .onLoad <- function(libname, pkgname) { - ## we can skip all the detection logic if forced - assuming the user has loaded JVM already - if (!nzchar(Sys.getenv("RJAVA_FORCE_LOAD"))) { - ## On OS X with Oracle Java we may need to work around Oracle bug: - ## https://bugs.openjdk.java.net/browse/JDK-7131356 - use.dynload <- FALSE - @USE_DYNLOAD_TRUE@use.dynload <- TRUE - if (length(grep("^darwin", R.version$os))) { - jh <- Sys.getenv("JAVA_HOME") - if (nzchar(jh) && !dir.exists(jh)) { - warning("JAVA_HOME is set incorrectly! Ingoring.") - jh <- "" - } - if (!nzchar(jh) && file.exists("/usr/libexec/java_home")) - jh <- system("/usr/libexec/java_home", intern=TRUE)[1L] - if (!nzchar(jh)) { - if (use.dynload) - stop("Cannot find Java. Install Java run-time and use R CMD javareconf.\nAs a last resort you can set JAVA_HOME appropriately.") - warning("Cannot find Java. Install Java run-time and use R CMD javareconf.\nAs a last resort you can set JAVA_HOME if you know the correct value.") - } - if (file.exists(file.path(jh, "jre/lib"))) jh <- file.path(jh, "jre") - dlp <- Sys.getenv("DYLD_LIBRARY_PATH") - if (nzchar(dlp)) dlp <- paste0(":", dlp) - ## pre-load JLI due to Oracle bug - if (file.exists(jli <- file.path(jh, "lib/jli/libjli.dylib"))) - dyn.load(jli, FALSE) - ## this is mandatory - we need the JVM - if (file.exists(jvm <- file.path(jh, "lib/server/libjvm.dylib"))) { - dyn.load(jvm, FALSE) - Sys.setenv(DYLD_LIBRARY_PATH=paste0(file.path(jh, "lib/server"), dlp)) - } else { - warning("Cannot find JVM library '", jvm, "'\nInstall Java and/or check JAVA_HOME (if in doubt, do NOT set it, it will be detected)") - if (use.dynload) stop("JVM could not be found") - } - } else if (use.dynload) { - jh <- Sys.getenv("JAVA_HOME") - if (nzchar(jh) && !dir.exists(jh)) { - warning("JAVA_HOME is set incorrectly! Ingoring.") - jh <- "" - } - if (!nzchar(jh)) - stop("JAVA_HOME is requied for dynamically loaded JVM, but is not set.\nRe-run R CMD javareconf or set JAVA_HOME correctly.") - if (file.exists(file.path(jh, "jre/lib"))) jh <- file.path(jh, "jre") - if (!file.exists(jvm <- file.path(jh, "lib/server/libjvm.so"))) - stop("Cannot find ", jvm,", re-configure R or use valid JAVA_HOME") - dyn.load(jvm, FALSE) - } - } ## RJAVA_FORCE_LOAD - library.dynam("rJava", pkgname, libname) # pass on to the system-independent part .jfirst(libname, pkgname) diff --git a/configure.ac b/configure.ac index afcb9be..6c32cd8 100644 --- a/configure.ac +++ b/configure.ac @@ -64,9 +64,9 @@ AC_ARG_ENABLE([Xrs], ## enable dynloaded JVM AC_ARG_ENABLE([dynload], -[AS_HELP_STRING([--enable-dynload],[load JVM dynamically (without linking). @<:@auto@:>@])], +[AS_HELP_STRING([--enable-dynload],[load JVM dynamically (without linking). @<:@yes@:>@])], [want_dynload="${enableval}"], -[want_dynload=auto]) +[want_dynload=yes]) ## enable debug flags AC_ARG_ENABLE([debug], @@ -253,6 +253,8 @@ AC_MSG_CHECKING([whether JVM will be loaded dynamically]) AC_MSG_RESULT(${use_dynload}) AM_CONDITIONAL(USE_DYNLOAD, [test "x${use_dynload}" = xyes]) +if test x${use_dynload} = xno; then + AC_MSG_CHECKING([whether JNI programs can be compiled]) AC_LINK_IFELSE([AC_LANG_SOURCE([[ #include @@ -289,6 +291,7 @@ int main(void) { } ]])],[AC_MSG_RESULT([ok])],[AC_MSG_ERROR([One or more JNI types differ from the corresponding native type. You may need to use non-standard compiler flags or a different compiler in order to fix this.])],[]) +fi ## dynload = no if test "${want_jri}" = auto; then AC_MSG_CHECKING([whether JRI should be compiled (autodetect)]) diff --git a/src/rJava.c b/src/rJava.c index 33bd33d..e9a3aff 100644 --- a/src/rJava.c +++ b/src/rJava.c @@ -11,6 +11,11 @@ int use_eenv = 1; /* cached environment. Do NOT use directly! Always use getJNIEnv()! */ JNIEnv *eenv; +/* quick hack - we have to get rid of R_GetCurrentEnv() soon anyway */ +#if R_VERSION < R_Version(3,6,0) +#define R_GetCurrentEnv() R_GlobalEnv +#endif + static SEXP getCurrentCall(void) { SEXP cexp, sys_calls = PROTECT(install("sys.calls")); cexp = PROTECT(lang1(sys_calls)); diff --git a/src/rjstring.h b/src/rjstring.h index 6fd24a1..b73fa63 100644 --- a/src/rjstring.h +++ b/src/rjstring.h @@ -1,7 +1,7 @@ #ifndef RJ_STRING_H__ #define RJ_STRING_H__ -#include /* for jchar */ +#include "jni.h" /* for jchar */ #include /* for SEXP */ /* --- API --- */ From fe30e9aba67ea24fd79f427bafbef81bebe95eb8 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Thu, 25 Dec 2025 16:48:13 +0100 Subject: [PATCH 04/63] feat: Implement dynamic JVM loading on Windows, removing static `jvm.dll` linking and adding build documentation. --- R/jinit.R | 454 +++++++++++++++++++++++++++++++++---------- R/windows/FirstLib.R | 110 ++++++----- src/Makevars.win | 3 +- src/djni.c | 90 +++++++++ test_rjava_win.R | 37 ++++ 5 files changed, 537 insertions(+), 157 deletions(-) create mode 100644 test_rjava_win.R diff --git a/R/jinit.R b/R/jinit.R index e45cc2c..824966a 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -4,46 +4,113 @@ ## ## $Id$ -.check.JVM <- function() - .Call(RJava_checkJVM) -.need.init <- function() - .Call(RJava_needs_init) +.check.JVM <- function() { + .Call(RJava_checkJVM) +} +.need.init <- function() { + .Call(RJava_needs_init) +} -.jloadJVM <- function(path, silent=FALSE) +.jloadJVM <- function(path, silent = FALSE) { .Call(RloadJVM, path.expand(path), silent) +} -.junloadJVM <- function() +.junloadJVM <- function() { .Call(RunloadJVM) +} .jfindAndLoadJVM <- function() { + # Platform dispatch + if (.Platform$OS.type == "windows") { + # Try to find JAVA_HOME using the logic we put in FirstLib.R or Sys.getenv + # Since .find.java.runtime is internal, we might need to duplicate logic or export it. + # Ideally, check JAVA_HOME env first (user might have set it after loading rJava) + home <- Sys.getenv("JAVA_HOME") + + if (!nzchar(home)) { + # Try to recover using the logic from FirstLib if available, or simple registry check + # For simplicity, we assume the user sets JAVA_HOME if automatic detection failed in .onLoad + # But we can try to call the internal helper if we are inside the package + if (exists(".find.java.runtime", envir = asNamespace("rJava"))) { + home <- asNamespace("rJava")$.find.java.runtime() + } + } + + if (is.null(home) || !nzchar(home)) { + stop( + "JAVA_HOME cannot be determined from the Registry or JAVA_HOME environment variable." + ) + } + + # Look for jvm.dll + libjvm <- list.files( + home, + "^jvm\\.dll$", + recursive = TRUE, + full.names = TRUE + ) + if (!length(libjvm)) { + stop("Cannot find jvm.dll in ", home) + } + + # Pick the one that matches architecture (simplistic check, ideally check arch path) + # Usually list.files finds bin/server/jvm.dll + dll <- libjvm[1] + message("Loading JVM from ", dll) + .jloadJVM(dll) + } else { home <- Sys.getenv("JAVA_HOME") if (!nzchar(home) || !isTRUE(dir.exists(home))) { - ## try java_home first if present - if (file.exists("/usr/libexec/java_home")) { - home <- tryCatch(system("/usr/libexec/java_home", intern=TRUE, ignore.stderr=TRUE), - error=function(...) "") - } - ## otherwise rely on java in PATH - if (!nzchar(home) || !isTRUE(dir.exists(home))) { - home <- tryCatch(system(paste("java", "-cp", - shQuote(system.file("java", package="rJava")), "getsp", "java.home"), - intern=TRUE, ignore.stderr=TRUE), error=function(...) "") - } + ## try java_home first if present + if (file.exists("/usr/libexec/java_home")) { + home <- tryCatch( + system("/usr/libexec/java_home", intern = TRUE, ignore.stderr = TRUE), + error = function(...) "" + ) + } + ## otherwise rely on java in PATH + if (!nzchar(home) || !isTRUE(dir.exists(home))) { + home <- tryCatch( + system( + paste( + "java", + "-cp", + shQuote(system.file("java", package = "rJava")), + "getsp", + "java.home" + ), + intern = TRUE, + ignore.stderr = TRUE + ), + error = function(...) "" + ) + } + } + if (!nzchar(home) || !isTRUE(dir.exists(home))) { + stop( + "Cannot find Java. Either make sure java executable is on the PATH or set the JAVA_HOME environment variable." + ) + } + libjvm <- list.files( + home, + "^libjvm[.]", + recursive = TRUE, + full.names = TRUE + ) + if (!length(libjvm)) { + stop("Cannot find libjvm in ", home) } - if (!nzchar(home) || !isTRUE(dir.exists(home))) - stop("Cannot find Java. Either make sure java executable is on the PATH or set the JAVA_HOME environment variable.") - libjvm <- list.files(home, "^libjvm[.]", recursive=TRUE, full.names=TRUE) - if (!length(libjvm)) - stop("Cannot find libjvm in ", home) message("Loading JVM from ", home) .jloadJVM(libjvm[1]) + } } ## initialization .jinit <- function(classpath=NULL, parameters=getOption("java.parameters", NA), ..., silent=FALSE, force.init=FALSE) { st <- .jvmState() - if (st$state == "not-loaded") - .jfindAndLoadJVM() + if (st$state == "not-loaded") { + .jfindAndLoadJVM() + } running.classpath <- character() if (!.need.init()) { running.classpath <- .jclassPath() @@ -76,33 +143,53 @@ if (!is.null(classpath)) { classpath <- as.character(classpath) - if (length(classpath)) - classpath <- paste(classpath,collapse=path.sep) + if (length(classpath)) { + classpath <- paste(classpath, collapse = path.sep) + } } - + # merge CLASSPATH environment variable if present - cp<-Sys.getenv("CLASSPATH") + cp <- Sys.getenv("CLASSPATH") if (!is.null(cp)) { - if (is.null(classpath)) - classpath<-cp - else - classpath<-paste(classpath,cp,sep=path.sep) + if (is.null(classpath)) { + classpath <- cp + } else { + classpath <- paste(classpath, cp, sep = path.sep) + } } - + # set rJava/java/boot for boostrap (so we can get RJavaClassLoader) - boot.classpath <- file.path(.rJava.base.path,"java","boot") + boot.classpath <- file.path(.rJava.base.path, "java", "boot") # if running in a sub-arch, append -Dr.arch in case someone gets the idea to start JRI - if (is.character(.Platform$r_arch) && nzchar(.Platform$r_arch) && length(grep("-Dr.arch", parameters, fixed=TRUE)) == 0L) - parameters <- c(paste("-Dr.arch=/", .Platform$r_arch, sep=''), as.character(parameters)) + if ( + is.character(.Platform$r_arch) && + nzchar(.Platform$r_arch) && + length(grep("-Dr.arch", parameters, fixed = TRUE)) == 0L + ) { + parameters <- c( + paste("-Dr.arch=/", .Platform$r_arch, sep = ''), + as.character(parameters) + ) + } ## unfortunately Sys/setlocale()/Sys.getlocale() have incompatible interfaces so there ## is no good way to get/set locales -- so we have to hack around it ... - locale.list <- c("LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LC_PAPER", "LC_MEASUREMENT") + locale.list <- c( + "LC_COLLATE", + "LC_CTYPE", + "LC_MONETARY", + "LC_NUMERIC", + "LC_TIME", + "LC_MESSAGES", + "LC_PAPER", + "LC_MEASUREMENT" + ) ## RStudio on Windows crashes due to missing error handlers if locales ## are queried that Windows does not support so we remove those to work around it - if (identical(.Platform$OS.type, "windows")) - locale.list <- locale.list[-(6:8)] + if (identical(.Platform$OS.type, "windows")) { + locale.list <- locale.list[-(6:8)] + } locales <- sapply(locale.list, Sys.getlocale) loc.sig <- Sys.getlocale() @@ -112,73 +199,167 @@ xr <- .External(RinitJVM, boot.classpath, parameters) ## we have to re-set the locales right away - suppressWarnings(try(if (!identical(Sys.getlocale(), loc.sig)) for (i in names(locales)) try(Sys.setlocale(i, locales[i]), silent=TRUE), - silent=TRUE)) + suppressWarnings(try( + if (!identical(Sys.getlocale(), loc.sig)) { + for (i in names(locales)) { + try(Sys.setlocale(i, locales[i]), silent = TRUE) + } + }, + silent = TRUE + )) - if (xr==-1) stop("Unable to initialize JVM.") - if (xr==-2) stop("Another VM is already running and rJava was unable to attach to that VM.") + if (xr == -1) { + stop("Unable to initialize JVM.") + } + if (xr == -2) { + stop( + "Another VM is already running and rJava was unable to attach to that VM." + ) + } # we'll handle xr==1 later because we need fully initialized rJava for that # this should remove any lingering .jclass objects from the global env # left there by previous versions of rJava - pj <- grep("^\\.jclass",ls(1,all.names=TRUE),value=TRUE) - if (length(pj)>0) { - rm(list=pj,pos=1) - if (exists(".jniInitialized",1)) rm(list=".jniInitialized",pos=1) - if (!silent) warning("rJava found hidden Java objects in your workspace. Internal objects from previous versions of rJava were deleted. Please note that Java objects cannot be saved in the workspace.") + pj <- grep("^\\.jclass", ls(1, all.names = TRUE), value = TRUE) + if (length(pj) > 0) { + rm(list = pj, pos = 1) + if (exists(".jniInitialized", 1)) { + rm(list = ".jniInitialized", pos = 1) + } + if (!silent) { + warning( + "rJava found hidden Java objects in your workspace. Internal objects from previous versions of rJava were deleted. Please note that Java objects cannot be saved in the workspace." + ) + } } ##--- HACK-WARNING: we're operating directly on the namespace environment ## this could be dangerous. - for (x in .delayed.variables) unlockBinding(x, .env) + for (x in .delayed.variables) { + unlockBinding(x, .env) + } assign(".jniInitialized", TRUE, .env) # get cached class objects for reflection - assign(".jclassObject", .jcall("java/lang/Class","Ljava/lang/Class;","forName","java.lang.Object"), .env) - assign(".jclassClass", .jcall("java/lang/Class","Ljava/lang/Class;","forName","java.lang.Class"), .env) - assign(".jclassString", .jcall("java/lang/Class","Ljava/lang/Class;","forName","java.lang.String"), .env) + assign( + ".jclassObject", + .jcall( + "java/lang/Class", + "Ljava/lang/Class;", + "forName", + "java.lang.Object" + ), + .env + ) + assign( + ".jclassClass", + .jcall( + "java/lang/Class", + "Ljava/lang/Class;", + "forName", + "java.lang.Class" + ), + .env + ) + assign( + ".jclassString", + .jcall( + "java/lang/Class", + "Ljava/lang/Class;", + "forName", + "java.lang.String" + ), + .env + ) - assign(".jclass.int", .jfield("java/lang/Integer", "Ljava/lang/Class;", "TYPE"), .env) - assign(".jclass.double", .jfield("java/lang/Double", "Ljava/lang/Class;", "TYPE"), .env) - assign(".jclass.float", .jfield("java/lang/Float", "Ljava/lang/Class;", "TYPE"), .env) - assign(".jclass.boolean", .jfield("java/lang/Boolean", "Ljava/lang/Class;", "TYPE"), .env) - assign(".jclass.void", .jfield("java/lang/Void", "Ljava/lang/Class;", "TYPE"), .env) + assign( + ".jclass.int", + .jfield("java/lang/Integer", "Ljava/lang/Class;", "TYPE"), + .env + ) + assign( + ".jclass.double", + .jfield("java/lang/Double", "Ljava/lang/Class;", "TYPE"), + .env + ) + assign( + ".jclass.float", + .jfield("java/lang/Float", "Ljava/lang/Class;", "TYPE"), + .env + ) + assign( + ".jclass.boolean", + .jfield("java/lang/Boolean", "Ljava/lang/Class;", "TYPE"), + .env + ) + assign( + ".jclass.void", + .jfield("java/lang/Void", "Ljava/lang/Class;", "TYPE"), + .env + ) ## if NOAWT is set, set AWT to headless - if (nzchar(Sys.getenv("NOAWT"))) .jcall("java/lang/System","S","setProperty","java.awt.headless","true") + if (nzchar(Sys.getenv("NOAWT"))) { + .jcall("java/lang/System", "S", "setProperty", "java.awt.headless", "true") + } lib <- "libs" - if (nchar(.Platform$r_arch)) lib <- file.path("libs", .Platform$r_arch) + if (nchar(.Platform$r_arch)) { + lib <- file.path("libs", .Platform$r_arch) + } rjcl <- NULL - if (xr==1) { # && nchar(classpath)>0) { + if (xr == 1) { + # && nchar(classpath)>0) { # ok, so we're attached to some other JVM - now we need to make sure that # we can load our class loader. If we can't then we have to use our bad hack # to be able to squeeze our loader in # first, see if this is actually JRIBootstrap so we have a loader already rjcl <- .Call(RJava_primary_class_loader) - if (is.null(rjcl) || .jidenticalRef(rjcl,.jzeroRef)) rjcl <- NULL - else rjcl <- new("jobjRef", jobj=rjcl, jclass="RJavaClassLoader") - if (is.jnull(rjcl)) - rjcl <- .jnew("RJavaClassLoader", .rJava.base.path, - file.path(.rJava.base.path, lib), check=FALSE) - .jcheck(silent=TRUE) + if (is.null(rjcl) || .jidenticalRef(rjcl, .jzeroRef)) { + rjcl <- NULL + } else { + rjcl <- new("jobjRef", jobj = rjcl, jclass = "RJavaClassLoader") + } + if (is.jnull(rjcl)) { + rjcl <- .jnew( + "RJavaClassLoader", + .rJava.base.path, + file.path(.rJava.base.path, lib), + check = FALSE + ) + } + .jcheck(silent = TRUE) if (is.jnull(rjcl)) { ## it's a hack, so we run it in try(..) in case BadThings(TM) happen ... - cpr <- try(.jmergeClassPath(boot.classpath), silent=TRUE) + cpr <- try(.jmergeClassPath(boot.classpath), silent = TRUE) if (inherits(cpr, "try-error")) { - .jcheck(silent=TRUE) - if (!silent) warning("Another VM is running already and the VM did not allow me to append paths to the class path.") + .jcheck(silent = TRUE) + if (!silent) { + warning( + "Another VM is running already and the VM did not allow me to append paths to the class path." + ) + } assign(".jinit.merge.error", cpr, .env) } - if (length(parameters)>0 && any(parameters!=getOption("java.parameters")) && !silent) + if ( + length(parameters) > 0 && + any(parameters != getOption("java.parameters")) && + !silent + ) { warning("Cannot set VM parameters, because VM is running already.") + } } } - if (is.jnull(rjcl)) - rjcl <- .jnew("RJavaClassLoader", .rJava.base.path, - file.path(.rJava.base.path, lib), check=FALSE ) + if (is.jnull(rjcl)) { + rjcl <- .jnew( + "RJavaClassLoader", + .rJava.base.path, + file.path(.rJava.base.path, lib), + check = FALSE + ) + } if (!is.jnull(rjcl)) { ## init class loader @@ -190,16 +371,21 @@ ## now it's time to add any additional class paths cpc <- unique(strsplit(classpath, .Platform$path.sep)[[1]]) if (length(cpc)) .jaddClassPath(cpc) - } else stop("Unable to create a Java class loader.") - + } else { + stop("Unable to create a Java class loader.") + } + ##.Call(RJava_new_class_loader, .rJava.base.path, file.path(.rJava.base.path, lib)) ## lock namespace bindings - for (x in .delayed.variables) lockBinding(x, .env) - + for (x in .delayed.variables) { + lockBinding(x, .env) + } + ## now we need to update the attached namespace (package env) as well m <- match(paste("package", getNamespaceName(.env), sep = ":"), search())[1] - if (!is.na(m)) { ## only is it is attached + if (!is.na(m)) { + ## only is it is attached pe <- as.environment(m) for (x in .delayed.export.variables) { unlockBinding(x, pe) @@ -207,15 +393,15 @@ lockBinding(x, pe) } } - - # FIXME: is this the best place or should this be done + + # FIXME: is this the best place or should this be done # internally right after the RJavaClassLoader is instantiated # init the cached RJavaTools class in the jni side .Call(initRJavaTools) - + # not yet # import( c( "java.lang", "java.util") ) - + invisible(xr) } @@ -223,7 +409,7 @@ # class loader strategy, we should add some sort of hook to let people # define how they want this to be done .jmergeClassPath <- function(cp) { - ccp <- .jcall("java/lang/System","S","getProperty","java.class.path") + ccp <- .jcall("java/lang/System", "S", "getProperty", "java.class.path") ccpc <- strsplit(ccp, .Platform$path.sep)[[1]] cpc <- strsplit(cp, .Platform$path.sep)[[1]] rcp <- unique(cpc[!(cpc %in% ccpc)]) @@ -231,9 +417,11 @@ # the loader requires directories to include trailing slash # Windows: need / or \ ? (untested) dirs <- which(file.info(rcp)$isdir) - for (i in dirs) - if (substr(rcp[i],nchar(rcp[i]),nchar(rcp[i]))!=.Platform$file.sep) - rcp[i]<-paste(rcp[i], .Platform$file.sep, sep='') + for (i in dirs) { + if (substr(rcp[i], nchar(rcp[i]), nchar(rcp[i])) != .Platform$file.sep) { + rcp[i] <- paste(rcp[i], .Platform$file.sep, sep = '') + } + } ## this is a hack, really, that exploits the fact that the system class loader ## is in fact a subclass of URLClassLoader and it also subverts protection @@ -246,37 +434,89 @@ ## it should probably be run in try(..) because chances are that it will ## break if Sun changes something... - cl <- .jcall("java/lang/ClassLoader", "Ljava/lang/ClassLoader;", "getSystemClassLoader") - urlc <- .jcall("java/lang/Class", "Ljava/lang/Class;", "forName", "java.net.URL") - clc <- .jcall("java/lang/Class", "Ljava/lang/Class;", "forName", "java.net.URLClassLoader") - ar <- .jcall("java/lang/reflect/Array", "Ljava/lang/Object;", - "newInstance", .jclassClass, 1:1) - .jcall("java/lang/reflect/Array", "V", "set", - .jcast(ar, "java/lang/Object"), 0:0, - .jcast(urlc, "java/lang/Object")) - m<-.jcall(clc, "Ljava/lang/reflect/Method;", "getDeclaredMethod", "addURL", .jcast(ar,"[Ljava/lang/Class;")) + cl <- .jcall( + "java/lang/ClassLoader", + "Ljava/lang/ClassLoader;", + "getSystemClassLoader" + ) + urlc <- .jcall( + "java/lang/Class", + "Ljava/lang/Class;", + "forName", + "java.net.URL" + ) + clc <- .jcall( + "java/lang/Class", + "Ljava/lang/Class;", + "forName", + "java.net.URLClassLoader" + ) + ar <- .jcall( + "java/lang/reflect/Array", + "Ljava/lang/Object;", + "newInstance", + .jclassClass, + 1:1 + ) + .jcall( + "java/lang/reflect/Array", + "V", + "set", + .jcast(ar, "java/lang/Object"), + 0:0, + .jcast(urlc, "java/lang/Object") + ) + m <- .jcall( + clc, + "Ljava/lang/reflect/Method;", + "getDeclaredMethod", + "addURL", + .jcast(ar, "[Ljava/lang/Class;") + ) .jcall(m, "V", "setAccessible", TRUE) - ar <- .jcall("java/lang/reflect/Array", "Ljava/lang/Object;", - "newInstance", .jclassObject, 1:1) - + ar <- .jcall( + "java/lang/reflect/Array", + "Ljava/lang/Object;", + "newInstance", + .jclassObject, + 1:1 + ) + for (fn in rcp) { f <- .jnew("java/io/File", fn) url <- .jcall(f, "Ljava/net/URL;", "toURL") - .jcall("java/lang/reflect/Array", "V", "set", - .jcast(ar, "java/lang/Object"), 0:0, - .jcast(url, "java/lang/Object")) - .jcall(m, "Ljava/lang/Object;", "invoke", - .jcast(cl, "java/lang/Object"), .jcast(ar, "[Ljava/lang/Object;")) + .jcall( + "java/lang/reflect/Array", + "V", + "set", + .jcast(ar, "java/lang/Object"), + 0:0, + .jcast(url, "java/lang/Object") + ) + .jcall( + m, + "Ljava/lang/Object;", + "invoke", + .jcast(cl, "java/lang/Object"), + .jcast(ar, "[Ljava/lang/Object;") + ) } # also adjust the java.class.path property to not confuse others - if (length(ccp)>1 || (length(ccp)==1 && nchar(ccp[1])>0)) + if (length(ccp) > 1 || (length(ccp) == 1 && nchar(ccp[1]) > 0)) { rcp <- c(ccp, rcp) - acp <- paste(rcp, collapse=.Platform$path.sep) - .jcall("java/lang/System","S","setProperty","java.class.path",as.character(acp)) + } + acp <- paste(rcp, collapse = .Platform$path.sep) + .jcall( + "java/lang/System", + "S", + "setProperty", + "java.class.path", + as.character(acp) + ) } # if #rcp>0 - invisible(.jcall("java/lang/System","S","getProperty","java.class.path")) + invisible(.jcall("java/lang/System", "S", "getProperty", "java.class.path")) } .jvmState <- function() .Call(RgetJVMstate) diff --git a/R/windows/FirstLib.R b/R/windows/FirstLib.R index 2c0da98..80edb68 100755 --- a/R/windows/FirstLib.R +++ b/R/windows/FirstLib.R @@ -1,65 +1,79 @@ .msg <- message -.onLoad <- -function(libname, pkgname) { - OPATH <- Sys.getenv("PATH") - javahome <- if (!is.null(getOption("java.home"))) getOption("java.home") else Sys.getenv("JAVA_HOME") +# Extract detection logic to a helper function +.find.java.runtime <- function() { + # Check options and env vars first + javahome <- if (!is.null(getOption("java.home"))) { + getOption("java.home") + } else { + Sys.getenv("JAVA_HOME") + } + if (nzchar(javahome) && !dir.exists(javahome)) { - .msg("java.home option: ", getOption("java.home")) - .msg("JAVA_HOME environment variable: ", Sys.getenv("JAVA_HOME")) - warning("Java home setting is INVALID, it will be ignored.\nPlease do NOT set it unless you want to override system settings.") - javahome <- "" + warning("Java home setting is INVALID, it will be ignored.") + javahome <- "" } - if(!nzchar(javahome)) { ## JAVA_HOME was not set explicitly + + # If not found, try Registry + if (!nzchar(javahome)) { find.java <- function() { - for (root in c("HLM", "HCU")) - for(key in c( - "Software\\JavaSoft\\JRE", - "Software\\JavaSoft\\JDK", - "Software\\JavaSoft\\Java Runtime Environment", - "Software\\JavaSoft\\Java Development Kit" - )) { - hive <- try(utils::readRegistry(key, root, 2), silent=TRUE) - if (!inherits(hive, "try-error")) return(hive) + for (root in c("HLM", "HCU")) { + for (key in c( + "Software\\JavaSoft\\JRE", + "Software\\JavaSoft\\JDK", + "Software\\JavaSoft\\Java Runtime Environment", + "Software\\JavaSoft\\Java Development Kit" + )) { + hive <- try( + utils::readRegistry(key, root, 2), + silent = TRUE + ) + if (!inherits(hive, "try-error")) return(hive) } + } hive } hive <- find.java() - if (inherits(hive, "try-error")) - stop("JAVA_HOME cannot be determined from the Registry") - if (!length(hive$CurrentVersion)) - stop("No CurrentVersion entry in Software/JavaSoft registry! Try re-installing Java and make sure R and Java have matching architectures.") - this <- hive[[hive$CurrentVersion]] - javahome <- this$JavaHome - paths <- if (is.character(this$RuntimeLib)) dirname(this$RuntimeLib) else character() # wrong on 64-bit - } else paths <- character() - if(is.null(javahome) || !length(javahome) || !nchar(javahome)) - stop("JAVA_HOME is not set and could not be determined from the registry") - #else cat("using JAVA_HOME =", javahome, "\n") + if (!inherits(hive, "try-error") && length(hive$CurrentVersion)) { + this <- hive[[hive$CurrentVersion]] + javahome <- this$JavaHome + } + } + return(javahome) +} + +.onLoad <- function(libname, pkgname) { + # Attempt to find Java to set up PATH if possible, but do NOT fail if missing + # This preserves 'old' behavior if Java is present, but allows 'new' behavior if not. + javahome <- .find.java.runtime() - ## we need to add Java-related library paths to PATH - curPath <- OPATH - paths <- c(paths, - file.path(javahome, "bin", "client"), # 32-bit - file.path(javahome, "bin", "server"), # 64-bit - file.path(javahome, "bin"), # base (now needed for MSVCRT in recent Sun Java) - file.path(javahome, "jre", "bin", "server"), # old 64-bit (or manual JAVA_HOME setting to JDK) - file.path(javahome, "jre", "bin", "client")) # old 32-bit (or manual JAVA_HOME setting to JDK) - cpc <- strsplit(curPath, ";", fixed=TRUE)[[1]] ## split it up so we can check presence/absence of a path + if (!is.null(javahome) && nzchar(javahome)) { + OPATH <- Sys.getenv("PATH") + # Add bin/server etc to path (logic copied from original) + paths <- character() + paths <- c( + paths, + file.path(javahome, "bin", "client"), + file.path(javahome, "bin", "server"), + file.path(javahome, "bin"), + file.path(javahome, "jre", "bin", "server"), + file.path(javahome, "jre", "bin", "client") + ) - ## add paths only if they are not in already and they exist - for (path in unique(paths)) - if (!path %in% cpc && file.exists(path)) curPath <- paste(path, curPath, sep=";") + cpc <- strsplit(OPATH, ";", fixed = TRUE)[[1]] + curPath <- OPATH + for (path in unique(paths)) { + if (!path %in% cpc && file.exists(path)) { + curPath <- paste(path, curPath, sep = ";") + } + } - ## set PATH only if it's not correct already (cannot use identical/isTRUE because of PATH name attribute) - if (curPath != OPATH) { - Sys.setenv(PATH = curPath) - # check the resulting PATH - if they don't match then Windows has truncated it - if (curPath != Sys.getenv("PATH")) - warning("*** WARNING: your Windows system seems to suffer from truncated PATH bug which will likely prevent rJava from loading.\n Either reduce your PATH or read http://support.microsoft.com/kb/906469 on how to fix your system.") + if (curPath != OPATH) Sys.setenv(PATH = curPath) } + # Load rJava.dll (now without jvm.dll dependency) library.dynam("rJava", pkgname, libname) - Sys.setenv(PATH = OPATH) + + # Initialize internal C variables .jfirst(libname, pkgname) } diff --git a/src/Makevars.win b/src/Makevars.win index 0b28d1b..db3df09 100644 --- a/src/Makevars.win +++ b/src/Makevars.win @@ -23,7 +23,6 @@ $(warning JAVA_HOME is $(JAVA_HOME)) # debugging flags like -DRJ_DEBUG or -DRJ_PROFILE to PKG_CFLAGS JAVA_INCLUDES=$(JAVA_HOME)/include $(JAVA_HOME)/include/win32 PKG_CPPFLAGS = -D_R_ -DWin32 -Ijvm-w32 -I$(RHOME)/src/include $(JAVA_INCLUDES:%=-I%) -PKG_LIBS = -Ljvm-w32 -ljvm.dll +PKG_LIBS = -Ljvm-w32 $(warning PKG_CPPFLAGS are $(PKG_CPPFLAGS)) - diff --git a/src/djni.c b/src/djni.c index c35655f..d9445ad 100644 --- a/src/djni.c +++ b/src/djni.c @@ -1,3 +1,92 @@ +#ifdef _WIN32 +#include +#include +#include "djni.h" + +static JNI_GetDefaultJavaVMInitArgs_fnptr JNI_GetDefaultJavaVMInitArgs_fn; +static JNI_CreateJavaVM_fnptr JNI_CreateJavaVM_fn; +static JNI_GetCreatedJavaVMs_fnptr JNI_GetCreatedJavaVMs_fn; + +static HMODULE jni_hmodule = NULL; +static char last_error_buf[1024]; + +static int load_sym(const char *sym, void **ptr) { + void *v = (void*)GetProcAddress(jni_hmodule, sym); + if (!v) { + DWORD err = GetLastError(); + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + last_error_buf, sizeof(last_error_buf), NULL); + FreeLibrary(jni_hmodule); + jni_hmodule = NULL; + return -1; + } + *ptr = v; + return 0; +} + +int djni_load(const char *path) { + last_error_buf[0] = 0; + if (jni_hmodule) return -1; + + // Use LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH to handle dependencies + // located in the same directory as jvm.dll (e.g. msvcr*.dll) + jni_hmodule = LoadLibraryExA(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); + + if (!jni_hmodule) { + DWORD err = GetLastError(); + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + last_error_buf, sizeof(last_error_buf), NULL); + return -2; + } + + if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void**) &JNI_GetDefaultJavaVMInitArgs_fn) || + load_sym("JNI_CreateJavaVM", (void**) &JNI_CreateJavaVM_fn) || + load_sym("JNI_GetCreatedJavaVMs", (void**) &JNI_GetCreatedJavaVMs_fn)) { + JNI_GetDefaultJavaVMInitArgs_fn = 0; + JNI_CreateJavaVM_fn = 0; + JNI_GetCreatedJavaVMs_fn = 0; + FreeLibrary(jni_hmodule); + jni_hmodule = NULL; + return -3; + } + return 0; +} + +int djni_unload(void) { + if (jni_hmodule) { + if (!FreeLibrary(jni_hmodule)) + return -2; + jni_hmodule = NULL; + return 0; + } + return -1; +} + +int djni_loaded(void) { + return jni_hmodule ? 1 : 0; +} + +const char* djni_last_error(void) { + return last_error_buf; +} + +// Pass-through functions +jint JNI_GetDefaultJavaVMInitArgs(void *args) { + return JNI_GetDefaultJavaVMInitArgs_fn ? JNI_GetDefaultJavaVMInitArgs_fn(args) : -99; +} + +jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args) { + return JNI_CreateJavaVM_fn ? JNI_CreateJavaVM_fn(pvm, penv, args) : -99; +} + +jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { + return JNI_GetCreatedJavaVMs_fn ? JNI_GetCreatedJavaVMs_fn(vmBuf, bufLen, nVMs) : -99; +} + +#else +/* ... Existing POSIX code goes here (keep existing src/djni.c content in #else block) ... */ #include #include "djni.h" @@ -206,3 +295,4 @@ int main(int ac, char **av) { } #endif +#endif diff --git a/test_rjava_win.R b/test_rjava_win.R new file mode 100644 index 0000000..9e8944f --- /dev/null +++ b/test_rjava_win.R @@ -0,0 +1,37 @@ +# test_rjava_win.R + +# 1. Load the package. +# Expected behavior: Should NOT fail even if JAVA_HOME is unset or invalid initially, +# because we removed the hard link to jvm.dll. +library(rJava) +message("rJava loaded.") + +# 2. Check JVM state +# Expected: "not-loaded" or "none" +print(rJava::.jvmState()) + +# 3. Simulate user setting JAVA_HOME after loading +# REPLACE THIS PATH with a valid JDK path on your Windows machine for testing! +# e.g. "C:/Program Files/Java/jdk-11" +my_java_home <- Sys.getenv("JAVA_HOME") +if (my_java_home == "") { + warning("Please set a valid JAVA_HOME for this test script to run fully.") +} else { + Sys.setenv(JAVA_HOME = my_java_home) + message("JAVA_HOME set to: ", my_java_home) +} + +# 4. Initialize +# Expected: Should find jvm.dll inside the JAVA_HOME set above and load it via LoadLibrary +.jinit() + +# 5. Verify +print(rJava::.jvmState()) +v <- .jcall("java/lang/System", "S", "getProperty", "java.version") +message("Java Version Loaded: ", v) + +if (nzchar(v)) { + message("SUCCESS: JVM loaded dynamically on Windows.") +} else { + stop("FAILURE: JVM did not load.") +} From 24d609aad2a11f90cd8d18c35be75b05dd14ea97 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 18:54:53 +0200 Subject: [PATCH 05/63] feat: defer JVM discovery and harden cross-platform dynamic loading --- .github/workflows/check.yaml | 1 + .github/workflows/dynamic.yaml | 76 ++ R/jinit.R | 442 +++-------- R/windows/FirstLib.R | 31 - configure | 1339 ++++++++++++++++++-------------- mkdist | 2 +- src/Rglue.c | 8 +- src/djni.c | 219 ++---- tests-integration/deferred.R | 46 ++ 9 files changed, 1058 insertions(+), 1106 deletions(-) create mode 100644 .github/workflows/dynamic.yaml create mode 100644 tests-integration/deferred.R diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 350d708..6c1f66c 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -4,6 +4,7 @@ name: check jobs: check: + if: github.ref != 'refs/heads/feature/dynamic-cross-platform' runs-on: ${{ matrix.os }} env: NOAWT: 1 diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml new file mode 100644 index 0000000..7ab5ad5 --- /dev/null +++ b/.github/workflows/dynamic.yaml @@ -0,0 +1,76 @@ +name: Deferred JVM integration +on: + push: + branches: [feature/dynamic-cross-platform] + workflow_dispatch: +jobs: + dynamic: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + env: + NOAWT: '1' + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + rtools-version: '45' + - uses: actions/setup-java@v5 + id: java17 + with: + distribution: temurin + java-version: '17' + - uses: actions/setup-java@v5 + id: java21 + with: + distribution: temurin + java-version: '21' + - name: Configure build Java + if: runner.os != 'Windows' + shell: bash + run: sudo R CMD javareconf + - name: Generate distribution and install in isolated library + shell: bash + run: | + set -euo pipefail + sh mkdist + mkdir -p "$RUNNER_TEMP/rjava-lib" + archive="$(ls ../rJava_*.tar.gz)" + R CMD INSTALL --library="$RUNNER_TEMP/rjava-lib" "$archive" + echo "R_LIBS_USER=$RUNNER_TEMP/rjava-lib" >> "$GITHUB_ENV" + - name: Check deferred loading in fresh processes + shell: bash + env: + RJE_JAVA_HOME_A: ${{ steps.java17.outputs.path }} + RJE_JAVA_HOME_B: ${{ steps.java21.outputs.path }} + run: | + set -euo pipefail + for scenario in select retry unload; do + Rscript --vanilla tests-integration/deferred.R "$scenario" + done + - name: Inspect native JVM linkage + shell: bash + run: | + set -euo pipefail + lib="$(Rscript --vanilla -e 'cat(system.file("libs", package="rJava"))')" + if [[ "$RUNNER_OS" == Windows ]]; then + objdump -p "$lib/x64/rJava.dll" > "$RUNNER_TEMP/linkage.txt" + elif [[ "$RUNNER_OS" == macOS ]]; then + otool -L "$lib/rJava.so" > "$RUNNER_TEMP/linkage.txt" + else + ldd "$lib/rJava.so" > "$RUNNER_TEMP/linkage.txt" + fi + cat "$RUNNER_TEMP/linkage.txt" + if grep -Ei '(libjvm|DLL Name: jvm.dll)' "$RUNNER_TEMP/linkage.txt"; then exit 1; fi + - uses: actions/upload-artifact@v4 + if: always() + with: + name: dynamic-${{ matrix.os }} + path: | + ../rJava_*.tar.gz + ${{ runner.temp }}/linkage.txt diff --git a/R/jinit.R b/R/jinit.R index 824966a..bdfa79e 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -4,113 +4,42 @@ ## ## $Id$ -.check.JVM <- function() { - .Call(RJava_checkJVM) -} -.need.init <- function() { - .Call(RJava_needs_init) -} +.check.JVM <- function() + .Call(RJava_checkJVM) +.need.init <- function() + .Call(RJava_needs_init) -.jloadJVM <- function(path, silent = FALSE) { +.jloadJVM <- function(path, silent=FALSE) .Call(RloadJVM, path.expand(path), silent) -} -.junloadJVM <- function() { +.junloadJVM <- function() .Call(RunloadJVM) -} .jfindAndLoadJVM <- function() { - # Platform dispatch - if (.Platform$OS.type == "windows") { - # Try to find JAVA_HOME using the logic we put in FirstLib.R or Sys.getenv - # Since .find.java.runtime is internal, we might need to duplicate logic or export it. - # Ideally, check JAVA_HOME env first (user might have set it after loading rJava) - home <- Sys.getenv("JAVA_HOME") - - if (!nzchar(home)) { - # Try to recover using the logic from FirstLib if available, or simple registry check - # For simplicity, we assume the user sets JAVA_HOME if automatic detection failed in .onLoad - # But we can try to call the internal helper if we are inside the package - if (exists(".find.java.runtime", envir = asNamespace("rJava"))) { - home <- asNamespace("rJava")$.find.java.runtime() - } - } - - if (is.null(home) || !nzchar(home)) { - stop( - "JAVA_HOME cannot be determined from the Registry or JAVA_HOME environment variable." - ) - } - - # Look for jvm.dll - libjvm <- list.files( - home, - "^jvm\\.dll$", - recursive = TRUE, - full.names = TRUE - ) - if (!length(libjvm)) { - stop("Cannot find jvm.dll in ", home) - } - - # Pick the one that matches architecture (simplistic check, ideally check arch path) - # Usually list.files finds bin/server/jvm.dll - dll <- libjvm[1] - message("Loading JVM from ", dll) - .jloadJVM(dll) - } else { - home <- Sys.getenv("JAVA_HOME") - if (!nzchar(home) || !isTRUE(dir.exists(home))) { - ## try java_home first if present - if (file.exists("/usr/libexec/java_home")) { - home <- tryCatch( - system("/usr/libexec/java_home", intern = TRUE, ignore.stderr = TRUE), - error = function(...) "" - ) - } - ## otherwise rely on java in PATH - if (!nzchar(home) || !isTRUE(dir.exists(home))) { - home <- tryCatch( - system( - paste( - "java", - "-cp", - shQuote(system.file("java", package = "rJava")), - "getsp", - "java.home" - ), - intern = TRUE, - ignore.stderr = TRUE - ), - error = function(...) "" - ) - } - } - if (!nzchar(home) || !isTRUE(dir.exists(home))) { - stop( - "Cannot find Java. Either make sure java executable is on the PATH or set the JAVA_HOME environment variable." - ) - } - libjvm <- list.files( - home, - "^libjvm[.]", - recursive = TRUE, - full.names = TRUE - ) - if (!length(libjvm)) { - stop("Cannot find libjvm in ", home) + home <- if (.Platform$OS.type == "windows") .find.java.runtime() else Sys.getenv("JAVA_HOME") + if (!length(home) || !nzchar(home)) { + if (file.exists("/usr/libexec/java_home")) + home <- tryCatch(system("/usr/libexec/java_home", intern=TRUE, ignore.stderr=TRUE), error=function(...) "") + if (!length(home) || !nzchar(home)) + home <- tryCatch(system2("java", c("-cp", shQuote(system.file("java", package="rJava")), "getsp", "java.home"), + stdout=TRUE, stderr=FALSE), error=function(...) "") } - message("Loading JVM from ", home) - .jloadJVM(libjvm[1]) - } + if (length(home) != 1L || is.na(home) || !nzchar(home) || !dir.exists(home)) + stop("Cannot find Java. Set JAVA_HOME to an existing Java home before initializing the JVM.") + pattern <- if (.Platform$OS.type == "windows") "^jvm[.]dll$" else "^libjvm[.](so|dylib)$" + libjvm <- list.files(home, pattern, recursive=TRUE, full.names=TRUE) + if (!length(libjvm)) stop("Cannot find JVM library in ", home) + ## Prefer the server VM when an older JDK also contains a client VM. + server <- grepl("/server/", libjvm, fixed=TRUE) + libjvm <- c(libjvm[server], libjvm[!server]) + .jloadJVM(normalizePath(libjvm[1], winslash="/", mustWork=TRUE)) } ## initialization .jinit <- function(classpath=NULL, parameters=getOption("java.parameters", NA), ..., silent=FALSE, force.init=FALSE) { st <- .jvmState() - if (st$state == "not-loaded") { - .jfindAndLoadJVM() - } + if (st$state == "not-loaded") + .jfindAndLoadJVM() running.classpath <- character() if (!.need.init()) { running.classpath <- .jclassPath() @@ -143,53 +72,33 @@ if (!is.null(classpath)) { classpath <- as.character(classpath) - if (length(classpath)) { - classpath <- paste(classpath, collapse = path.sep) - } + if (length(classpath)) + classpath <- paste(classpath,collapse=path.sep) } # merge CLASSPATH environment variable if present - cp <- Sys.getenv("CLASSPATH") + cp<-Sys.getenv("CLASSPATH") if (!is.null(cp)) { - if (is.null(classpath)) { - classpath <- cp - } else { - classpath <- paste(classpath, cp, sep = path.sep) - } + if (is.null(classpath)) + classpath<-cp + else + classpath<-paste(classpath,cp,sep=path.sep) } # set rJava/java/boot for boostrap (so we can get RJavaClassLoader) - boot.classpath <- file.path(.rJava.base.path, "java", "boot") + boot.classpath <- file.path(.rJava.base.path,"java","boot") # if running in a sub-arch, append -Dr.arch in case someone gets the idea to start JRI - if ( - is.character(.Platform$r_arch) && - nzchar(.Platform$r_arch) && - length(grep("-Dr.arch", parameters, fixed = TRUE)) == 0L - ) { - parameters <- c( - paste("-Dr.arch=/", .Platform$r_arch, sep = ''), - as.character(parameters) - ) - } + if (is.character(.Platform$r_arch) && nzchar(.Platform$r_arch) && length(grep("-Dr.arch", parameters, fixed=TRUE)) == 0L) + parameters <- c(paste("-Dr.arch=/", .Platform$r_arch, sep=''), as.character(parameters)) ## unfortunately Sys/setlocale()/Sys.getlocale() have incompatible interfaces so there ## is no good way to get/set locales -- so we have to hack around it ... - locale.list <- c( - "LC_COLLATE", - "LC_CTYPE", - "LC_MONETARY", - "LC_NUMERIC", - "LC_TIME", - "LC_MESSAGES", - "LC_PAPER", - "LC_MEASUREMENT" - ) + locale.list <- c("LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LC_PAPER", "LC_MEASUREMENT") ## RStudio on Windows crashes due to missing error handlers if locales ## are queried that Windows does not support so we remove those to work around it - if (identical(.Platform$OS.type, "windows")) { - locale.list <- locale.list[-(6:8)] - } + if (identical(.Platform$OS.type, "windows")) + locale.list <- locale.list[-(6:8)] locales <- sapply(locale.list, Sys.getlocale) loc.sig <- Sys.getlocale() @@ -199,167 +108,73 @@ xr <- .External(RinitJVM, boot.classpath, parameters) ## we have to re-set the locales right away - suppressWarnings(try( - if (!identical(Sys.getlocale(), loc.sig)) { - for (i in names(locales)) { - try(Sys.setlocale(i, locales[i]), silent = TRUE) - } - }, - silent = TRUE - )) + suppressWarnings(try(if (!identical(Sys.getlocale(), loc.sig)) for (i in names(locales)) try(Sys.setlocale(i, locales[i]), silent=TRUE), + silent=TRUE)) - if (xr == -1) { - stop("Unable to initialize JVM.") - } - if (xr == -2) { - stop( - "Another VM is already running and rJava was unable to attach to that VM." - ) - } + if (xr==-1) stop("Unable to initialize JVM.") + if (xr==-2) stop("Another VM is already running and rJava was unable to attach to that VM.") # we'll handle xr==1 later because we need fully initialized rJava for that # this should remove any lingering .jclass objects from the global env # left there by previous versions of rJava - pj <- grep("^\\.jclass", ls(1, all.names = TRUE), value = TRUE) - if (length(pj) > 0) { - rm(list = pj, pos = 1) - if (exists(".jniInitialized", 1)) { - rm(list = ".jniInitialized", pos = 1) - } - if (!silent) { - warning( - "rJava found hidden Java objects in your workspace. Internal objects from previous versions of rJava were deleted. Please note that Java objects cannot be saved in the workspace." - ) - } + pj <- grep("^\\.jclass",ls(1,all.names=TRUE),value=TRUE) + if (length(pj)>0) { + rm(list=pj,pos=1) + if (exists(".jniInitialized",1)) rm(list=".jniInitialized",pos=1) + if (!silent) warning("rJava found hidden Java objects in your workspace. Internal objects from previous versions of rJava were deleted. Please note that Java objects cannot be saved in the workspace.") } ##--- HACK-WARNING: we're operating directly on the namespace environment ## this could be dangerous. - for (x in .delayed.variables) { - unlockBinding(x, .env) - } + for (x in .delayed.variables) unlockBinding(x, .env) assign(".jniInitialized", TRUE, .env) # get cached class objects for reflection - assign( - ".jclassObject", - .jcall( - "java/lang/Class", - "Ljava/lang/Class;", - "forName", - "java.lang.Object" - ), - .env - ) - assign( - ".jclassClass", - .jcall( - "java/lang/Class", - "Ljava/lang/Class;", - "forName", - "java.lang.Class" - ), - .env - ) - assign( - ".jclassString", - .jcall( - "java/lang/Class", - "Ljava/lang/Class;", - "forName", - "java.lang.String" - ), - .env - ) - - assign( - ".jclass.int", - .jfield("java/lang/Integer", "Ljava/lang/Class;", "TYPE"), - .env - ) - assign( - ".jclass.double", - .jfield("java/lang/Double", "Ljava/lang/Class;", "TYPE"), - .env - ) - assign( - ".jclass.float", - .jfield("java/lang/Float", "Ljava/lang/Class;", "TYPE"), - .env - ) - assign( - ".jclass.boolean", - .jfield("java/lang/Boolean", "Ljava/lang/Class;", "TYPE"), - .env - ) - assign( - ".jclass.void", - .jfield("java/lang/Void", "Ljava/lang/Class;", "TYPE"), - .env - ) + assign(".jclassObject", .jcall("java/lang/Class","Ljava/lang/Class;","forName","java.lang.Object"), .env) + assign(".jclassClass", .jcall("java/lang/Class","Ljava/lang/Class;","forName","java.lang.Class"), .env) + assign(".jclassString", .jcall("java/lang/Class","Ljava/lang/Class;","forName","java.lang.String"), .env) + + assign(".jclass.int", .jfield("java/lang/Integer", "Ljava/lang/Class;", "TYPE"), .env) + assign(".jclass.double", .jfield("java/lang/Double", "Ljava/lang/Class;", "TYPE"), .env) + assign(".jclass.float", .jfield("java/lang/Float", "Ljava/lang/Class;", "TYPE"), .env) + assign(".jclass.boolean", .jfield("java/lang/Boolean", "Ljava/lang/Class;", "TYPE"), .env) + assign(".jclass.void", .jfield("java/lang/Void", "Ljava/lang/Class;", "TYPE"), .env) ## if NOAWT is set, set AWT to headless - if (nzchar(Sys.getenv("NOAWT"))) { - .jcall("java/lang/System", "S", "setProperty", "java.awt.headless", "true") - } + if (nzchar(Sys.getenv("NOAWT"))) .jcall("java/lang/System","S","setProperty","java.awt.headless","true") lib <- "libs" - if (nchar(.Platform$r_arch)) { - lib <- file.path("libs", .Platform$r_arch) - } + if (nchar(.Platform$r_arch)) lib <- file.path("libs", .Platform$r_arch) rjcl <- NULL - if (xr == 1) { - # && nchar(classpath)>0) { + if (xr==1) { # && nchar(classpath)>0) { # ok, so we're attached to some other JVM - now we need to make sure that # we can load our class loader. If we can't then we have to use our bad hack # to be able to squeeze our loader in # first, see if this is actually JRIBootstrap so we have a loader already rjcl <- .Call(RJava_primary_class_loader) - if (is.null(rjcl) || .jidenticalRef(rjcl, .jzeroRef)) { - rjcl <- NULL - } else { - rjcl <- new("jobjRef", jobj = rjcl, jclass = "RJavaClassLoader") - } - if (is.jnull(rjcl)) { - rjcl <- .jnew( - "RJavaClassLoader", - .rJava.base.path, - file.path(.rJava.base.path, lib), - check = FALSE - ) - } - .jcheck(silent = TRUE) + if (is.null(rjcl) || .jidenticalRef(rjcl,.jzeroRef)) rjcl <- NULL + else rjcl <- new("jobjRef", jobj=rjcl, jclass="RJavaClassLoader") + if (is.jnull(rjcl)) + rjcl <- .jnew("RJavaClassLoader", .rJava.base.path, + file.path(.rJava.base.path, lib), check=FALSE) + .jcheck(silent=TRUE) if (is.jnull(rjcl)) { ## it's a hack, so we run it in try(..) in case BadThings(TM) happen ... - cpr <- try(.jmergeClassPath(boot.classpath), silent = TRUE) + cpr <- try(.jmergeClassPath(boot.classpath), silent=TRUE) if (inherits(cpr, "try-error")) { - .jcheck(silent = TRUE) - if (!silent) { - warning( - "Another VM is running already and the VM did not allow me to append paths to the class path." - ) - } + .jcheck(silent=TRUE) + if (!silent) warning("Another VM is running already and the VM did not allow me to append paths to the class path.") assign(".jinit.merge.error", cpr, .env) } - if ( - length(parameters) > 0 && - any(parameters != getOption("java.parameters")) && - !silent - ) { + if (length(parameters)>0 && any(parameters!=getOption("java.parameters")) && !silent) warning("Cannot set VM parameters, because VM is running already.") - } } } - if (is.jnull(rjcl)) { - rjcl <- .jnew( - "RJavaClassLoader", - .rJava.base.path, - file.path(.rJava.base.path, lib), - check = FALSE - ) - } + if (is.jnull(rjcl)) + rjcl <- .jnew("RJavaClassLoader", .rJava.base.path, + file.path(.rJava.base.path, lib), check=FALSE ) if (!is.jnull(rjcl)) { ## init class loader @@ -371,21 +186,16 @@ ## now it's time to add any additional class paths cpc <- unique(strsplit(classpath, .Platform$path.sep)[[1]]) if (length(cpc)) .jaddClassPath(cpc) - } else { - stop("Unable to create a Java class loader.") - } + } else stop("Unable to create a Java class loader.") ##.Call(RJava_new_class_loader, .rJava.base.path, file.path(.rJava.base.path, lib)) ## lock namespace bindings - for (x in .delayed.variables) { - lockBinding(x, .env) - } + for (x in .delayed.variables) lockBinding(x, .env) ## now we need to update the attached namespace (package env) as well m <- match(paste("package", getNamespaceName(.env), sep = ":"), search())[1] - if (!is.na(m)) { - ## only is it is attached + if (!is.na(m)) { ## only is it is attached pe <- as.environment(m) for (x in .delayed.export.variables) { unlockBinding(x, pe) @@ -409,7 +219,7 @@ # class loader strategy, we should add some sort of hook to let people # define how they want this to be done .jmergeClassPath <- function(cp) { - ccp <- .jcall("java/lang/System", "S", "getProperty", "java.class.path") + ccp <- .jcall("java/lang/System","S","getProperty","java.class.path") ccpc <- strsplit(ccp, .Platform$path.sep)[[1]] cpc <- strsplit(cp, .Platform$path.sep)[[1]] rcp <- unique(cpc[!(cpc %in% ccpc)]) @@ -417,11 +227,9 @@ # the loader requires directories to include trailing slash # Windows: need / or \ ? (untested) dirs <- which(file.info(rcp)$isdir) - for (i in dirs) { - if (substr(rcp[i], nchar(rcp[i]), nchar(rcp[i])) != .Platform$file.sep) { - rcp[i] <- paste(rcp[i], .Platform$file.sep, sep = '') - } - } + for (i in dirs) + if (substr(rcp[i],nchar(rcp[i]),nchar(rcp[i]))!=.Platform$file.sep) + rcp[i]<-paste(rcp[i], .Platform$file.sep, sep='') ## this is a hack, really, that exploits the fact that the system class loader ## is in fact a subclass of URLClassLoader and it also subverts protection @@ -434,89 +242,37 @@ ## it should probably be run in try(..) because chances are that it will ## break if Sun changes something... - cl <- .jcall( - "java/lang/ClassLoader", - "Ljava/lang/ClassLoader;", - "getSystemClassLoader" - ) - urlc <- .jcall( - "java/lang/Class", - "Ljava/lang/Class;", - "forName", - "java.net.URL" - ) - clc <- .jcall( - "java/lang/Class", - "Ljava/lang/Class;", - "forName", - "java.net.URLClassLoader" - ) - ar <- .jcall( - "java/lang/reflect/Array", - "Ljava/lang/Object;", - "newInstance", - .jclassClass, - 1:1 - ) - .jcall( - "java/lang/reflect/Array", - "V", - "set", - .jcast(ar, "java/lang/Object"), - 0:0, - .jcast(urlc, "java/lang/Object") - ) - m <- .jcall( - clc, - "Ljava/lang/reflect/Method;", - "getDeclaredMethod", - "addURL", - .jcast(ar, "[Ljava/lang/Class;") - ) + cl <- .jcall("java/lang/ClassLoader", "Ljava/lang/ClassLoader;", "getSystemClassLoader") + urlc <- .jcall("java/lang/Class", "Ljava/lang/Class;", "forName", "java.net.URL") + clc <- .jcall("java/lang/Class", "Ljava/lang/Class;", "forName", "java.net.URLClassLoader") + ar <- .jcall("java/lang/reflect/Array", "Ljava/lang/Object;", + "newInstance", .jclassClass, 1:1) + .jcall("java/lang/reflect/Array", "V", "set", + .jcast(ar, "java/lang/Object"), 0:0, + .jcast(urlc, "java/lang/Object")) + m<-.jcall(clc, "Ljava/lang/reflect/Method;", "getDeclaredMethod", "addURL", .jcast(ar,"[Ljava/lang/Class;")) .jcall(m, "V", "setAccessible", TRUE) - ar <- .jcall( - "java/lang/reflect/Array", - "Ljava/lang/Object;", - "newInstance", - .jclassObject, - 1:1 - ) + ar <- .jcall("java/lang/reflect/Array", "Ljava/lang/Object;", + "newInstance", .jclassObject, 1:1) for (fn in rcp) { f <- .jnew("java/io/File", fn) url <- .jcall(f, "Ljava/net/URL;", "toURL") - .jcall( - "java/lang/reflect/Array", - "V", - "set", - .jcast(ar, "java/lang/Object"), - 0:0, - .jcast(url, "java/lang/Object") - ) - .jcall( - m, - "Ljava/lang/Object;", - "invoke", - .jcast(cl, "java/lang/Object"), - .jcast(ar, "[Ljava/lang/Object;") - ) + .jcall("java/lang/reflect/Array", "V", "set", + .jcast(ar, "java/lang/Object"), 0:0, + .jcast(url, "java/lang/Object")) + .jcall(m, "Ljava/lang/Object;", "invoke", + .jcast(cl, "java/lang/Object"), .jcast(ar, "[Ljava/lang/Object;")) } # also adjust the java.class.path property to not confuse others - if (length(ccp) > 1 || (length(ccp) == 1 && nchar(ccp[1]) > 0)) { + if (length(ccp)>1 || (length(ccp)==1 && nchar(ccp[1])>0)) rcp <- c(ccp, rcp) - } - acp <- paste(rcp, collapse = .Platform$path.sep) - .jcall( - "java/lang/System", - "S", - "setProperty", - "java.class.path", - as.character(acp) - ) + acp <- paste(rcp, collapse=.Platform$path.sep) + .jcall("java/lang/System","S","setProperty","java.class.path",as.character(acp)) } # if #rcp>0 - invisible(.jcall("java/lang/System", "S", "getProperty", "java.class.path")) + invisible(.jcall("java/lang/System","S","getProperty","java.class.path")) } .jvmState <- function() .Call(RgetJVMstate) diff --git a/R/windows/FirstLib.R b/R/windows/FirstLib.R index 80edb68..b59f08f 100755 --- a/R/windows/FirstLib.R +++ b/R/windows/FirstLib.R @@ -43,37 +43,6 @@ } .onLoad <- function(libname, pkgname) { - # Attempt to find Java to set up PATH if possible, but do NOT fail if missing - # This preserves 'old' behavior if Java is present, but allows 'new' behavior if not. - javahome <- .find.java.runtime() - - if (!is.null(javahome) && nzchar(javahome)) { - OPATH <- Sys.getenv("PATH") - # Add bin/server etc to path (logic copied from original) - paths <- character() - paths <- c( - paths, - file.path(javahome, "bin", "client"), - file.path(javahome, "bin", "server"), - file.path(javahome, "bin"), - file.path(javahome, "jre", "bin", "server"), - file.path(javahome, "jre", "bin", "client") - ) - - cpc <- strsplit(OPATH, ";", fixed = TRUE)[[1]] - curPath <- OPATH - for (path in unique(paths)) { - if (!path %in% cpc && file.exists(path)) { - curPath <- paste(path, curPath, sep = ";") - } - } - - if (curPath != OPATH) Sys.setenv(PATH = curPath) - } - - # Load rJava.dll (now without jvm.dll dependency) library.dynam("rJava", pkgname, libname) - - # Initialize internal C variables .jfirst(libname, pkgname) } diff --git a/configure b/configure index 12aab97..ef1f0a6 100755 --- a/configure +++ b/configure @@ -1,11 +1,11 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.72 for rJava 0.8. +# Generated by GNU Autoconf 2.73 for rJava 0.8. # # Report bugs to . # # -# Copyright (C) 1992-1996, 1998-2017, 2020-2023 Free Software Foundation, +# Copyright (C) 1992-1996, 1998-2017, 2020-2026 Free Software Foundation, # Inc. # # @@ -22,7 +22,7 @@ then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which - # is contrary to our usage. Disable this feature. + # contradicts POSIX and common usage. Disable this. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else case e in #( @@ -109,7 +109,7 @@ if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then - printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + printf '%s\n' "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi @@ -131,10 +131,13 @@ case $- in # (((( *x* ) as_opts=-x ;; * ) as_opts= ;; esac -exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} +case $# in # (( + 0) exec $CONFIG_SHELL $as_opts "$as_myself" ;; + *) exec $CONFIG_SHELL $as_opts "$as_myself" "$@" ;; +esac # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed 'exec'. -printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 +printf '%s\n' "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi # We don't want this to propagate to other subprocesses. @@ -145,7 +148,7 @@ then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which - # is contrary to our usage. Disable this feature. + # contradicts POSIX and common usage. Disable this. alias -g '\${1+\"\$@\"}'='\"\$@\"' setopt NO_GLOB_SUBST else case e in #( @@ -254,22 +257,25 @@ case $- in # (((( *x* ) as_opts=-x ;; * ) as_opts= ;; esac -exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} +case $# in # (( + 0) exec $CONFIG_SHELL $as_opts "$as_myself" ;; + *) exec $CONFIG_SHELL $as_opts "$as_myself" "$@" ;; +esac # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed 'exec'. -printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 +printf '%s\n' "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi if test x$as_have_required = xno then : - printf "%s\n" "$0: This script requires a shell more modern than all" - printf "%s\n" "$0: the shells that I found on your system." + printf '%s\n' "$0: This script requires a shell more modern than all" + printf '%s\n' "$0: the shells that I found on your system." if test ${ZSH_VERSION+y} ; then - printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should" - printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later." + printf '%s\n' "$0: In particular, zsh $ZSH_VERSION has bugs and should" + printf '%s\n' "$0: be upgraded to zsh 4.3.4 or later." else - printf "%s\n" "$0: Please tell bug-autoconf@gnu.org and + printf '%s\n' "$0: Please tell bug-autoconf@gnu.org and $0: Simon.Urbanek@r-project.org about your system, $0: including any error possibly output before this $0: message. Then install a modern shell, or manually run @@ -330,7 +336,7 @@ as_fn_mkdir_p () as_dirs= while :; do case $as_dir in #( - *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *\'*) as_qdir=`printf '%s\n' "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" @@ -339,7 +345,7 @@ $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -printf "%s\n" X"$as_dir" | +printf '%s\n' X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -422,9 +428,9 @@ as_fn_error () as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - printf "%s\n" "$as_me: error: $2" >&2 + printf '%s\n' "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -451,7 +457,7 @@ as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || -printf "%s\n" X/"$0" | +printf '%s\n' X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q @@ -497,7 +503,7 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits s/-\n.*// ' >$as_me.lineno && chmod +x "$as_me.lineno" || - { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } + { printf '%s\n' "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } # If we had to re-execute with $CONFIG_SHELL, we're ensured to have # already done that, so ensure we don't try to do so again and fall @@ -511,29 +517,6 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits exit } - -# Determine whether it's possible to make 'echo' print without a newline. -# These variables are no longer used directly by Autoconf, but are AC_SUBSTed -# for compatibility with existing Makefiles. -ECHO_C= ECHO_N= ECHO_T= -case `echo -n x` in #((((( --n*) - case `echo 'xy\c'` in - *c*) ECHO_T=' ';; # ECHO_T is single tab character. - xy) ECHO_C='\c';; - *) echo `echo ksh88 bug on AIX 6.1` > /dev/null - ECHO_T=' ';; - esac;; -*) - ECHO_N='-n';; -esac - -# For backward compatibility with old third-party macros, we provide -# the shell variables $as_echo and $as_echo_n. New code should use -# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. -as_echo='printf %s\n' -as_echo_n='printf %s' - rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -592,6 +575,7 @@ ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` # Initializations. # ac_default_prefix=/usr/local +ac_clean_CONFIG_STATUS= ac_clean_files= ac_config_libobj_dir=. LIBOBJS= @@ -663,13 +647,13 @@ LDFLAGS CFLAGS CC R_HOME +ECHO_T +ECHO_N +ECHO_C target_alias host_alias build_alias LIBS -ECHO_T -ECHO_N -ECHO_C DEFS mandir localedir @@ -833,7 +817,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" @@ -859,7 +843,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" @@ -1072,7 +1056,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" @@ -1088,7 +1072,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" @@ -1132,9 +1116,9 @@ Try '$0 --help' for more information" *) # FIXME: should be removed in autoconf 3.0. - printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2 + printf '%s\n' "$as_me: WARNING: you should use --build, --host, --target" >&2 expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && - printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2 + printf '%s\n' "$as_me: WARNING: invalid host type: $ac_option" >&2 : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" ;; @@ -1142,7 +1126,7 @@ Try '$0 --help' for more information" done if test -n "$ac_prev"; then - ac_option=--`echo $ac_prev | sed 's/_/-/g'` + ac_option=--`printf '%s\n' $ac_prev | sed 's/_/-/g'` as_fn_error $? "missing argument to $ac_option" fi @@ -1150,7 +1134,7 @@ if test -n "$ac_unrecognized_opts"; then case $enable_option_checking in no) ;; fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; - *) printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; + *) printf '%s\n' "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; esac fi @@ -1214,7 +1198,7 @@ $as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_myself" : 'X\(//\)[^/]' \| \ X"$as_myself" : 'X\(//\)$' \| \ X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || -printf "%s\n" X"$as_myself" | +printf '%s\n' X"$as_myself" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -1359,7 +1343,7 @@ Optional Features: programs to embed R. [auto] --enable-headless enable initialization in headless mode. [auto] --enable-Xrs use -Xrs in Java initialization. [auto] - --enable-dynload load JVM dynamically (without linking). [auto] + --enable-dynload load JVM dynamically (without linking). [yes] --enable-debug enable debug flags and output. [no] --enable-mem-profile enable memory profiling. [debug] --enable-callbacks enable the support for callbacks from Java into R. @@ -1394,9 +1378,9 @@ if test "$ac_init_help" = "recursive"; then case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf '%s\n' "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf '%s\n' "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -1433,7 +1417,7 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix echo && $SHELL "$ac_srcdir/configure" --help=recursive else - printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2 + printf '%s\n' "$as_me: WARNING: no configuration information is in $ac_dir" >&2 fi || ac_status=$? cd "$ac_pwd" || { ac_status=$?; break; } done @@ -1443,9 +1427,9 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF rJava configure 0.8 -generated by GNU Autoconf 2.72 +generated by GNU Autoconf 2.73 -Copyright (C) 2023 Free Software Foundation, Inc. +Copyright (C) 2026 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. _ACEOF @@ -1469,7 +1453,7 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_compile") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -1477,7 +1461,7 @@ printf "%s\n" "$ac_try_echo"; } >&5 cat conftest.er1 >&5 mv -f conftest.er1 conftest.err fi - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { test -z "$ac_c_werror_flag" || test ! -s conftest.err @@ -1485,7 +1469,7 @@ printf "%s\n" "$ac_try_echo"; } >&5 then : ac_retval=0 else case e in #( - e) printf "%s\n" "$as_me: failed program was:" >&5 + e) printf '%s\n' "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=1 ;; @@ -1503,7 +1487,7 @@ fi ac_fn_c_check_header_compile () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 printf %s "checking for $2... " >&6; } if eval test \${$3+y} then : @@ -1525,8 +1509,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi eval ac_res=\$$3 - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -printf "%s\n" "$ac_res" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf '%s\n' "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_c_check_header_compile @@ -1539,7 +1523,7 @@ ac_fn_check_decl () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack as_decl_name=`echo $2|sed 's/ *(.*//'` - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $as_decl_name is declared" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether $as_decl_name is declared" >&5 printf %s "checking whether $as_decl_name is declared... " >&6; } if eval test \${$3+y} then : @@ -1579,8 +1563,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext esac fi eval ac_res=\$$3 - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -printf "%s\n" "$ac_res" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf '%s\n' "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_check_decl @@ -1598,7 +1582,7 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_link") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -1606,7 +1590,7 @@ printf "%s\n" "$ac_try_echo"; } >&5 cat conftest.er1 >&5 mv -f conftest.er1 conftest.err fi - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { test -z "$ac_c_werror_flag" || test ! -s conftest.err @@ -1617,7 +1601,7 @@ printf "%s\n" "$ac_try_echo"; } >&5 then : ac_retval=0 else case e in #( - e) printf "%s\n" "$as_me: failed program was:" >&5 + e) printf '%s\n' "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=1 ;; @@ -1646,26 +1630,26 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { ac_try='./conftest$ac_exeext' { { case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; } then : ac_retval=0 else case e in #( - e) printf "%s\n" "$as_me: program exited with status $ac_status" >&5 - printf "%s\n" "$as_me: failed program was:" >&5 + e) printf '%s\n' "$as_me: program exited with status $ac_status" >&5 + printf '%s\n' "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=$ac_status ;; @@ -1681,7 +1665,7 @@ for ac_arg do case $ac_arg in *\'*) - ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append ac_configure_args_raw " '$ac_arg'" done @@ -1693,7 +1677,7 @@ case $ac_configure_args_raw in ac_unsafe_z='|&;<>()$`\\"*?[ '' ' # This string ends in space, tab. ac_unsafe_a="$ac_unsafe_z#~" ac_safe_unquote="s/ '\\([^$ac_unsafe_a][^$ac_unsafe_z]*\\)'/ \\1/g" - ac_configure_args_raw=` printf "%s\n" "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;; + ac_configure_args_raw=` printf '%s\n' "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;; esac cat >config.log <<_ACEOF @@ -1701,7 +1685,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. It was created by rJava $as_me 0.8, which was -generated by GNU Autoconf 2.72. Invocation command line was +generated by GNU Autoconf 2.73. Invocation command line was $ $0$ac_configure_args_raw @@ -1741,7 +1725,7 @@ do */) ;; *) as_dir=$as_dir/ ;; esac - printf "%s\n" "PATH: $as_dir" + printf '%s\n' "PATH: $as_dir" done IFS=$as_save_IFS @@ -1776,7 +1760,7 @@ do | -silent | --silent | --silen | --sile | --sil) continue ;; *\'*) - ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac case $ac_pass in 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; @@ -1805,31 +1789,22 @@ done { ac_configure_args0=; unset ac_configure_args0;} { ac_configure_args1=; unset ac_configure_args1;} -# When interrupted or exit'd, cleanup temporary files, and complete -# config.log. We remove comments because anyway the quotes in there -# would cause problems or look ugly. -# WARNING: Use '\'' to represent an apostrophe within the trap. -# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. -trap 'exit_status=$? - # Sanitize IFS. - IFS=" "" $as_nl" - # Save into config.log some information that might help in debugging. - { - echo - - printf "%s\n" "## ---------------- ## -## Cache variables. ## -## ---------------- ##" - echo - # The following way of writing the cache mishandles newlines in values, +# Dump the cache to stdout. It can be in a pipe (this is a requirement). +ac_cache_dump () +{ + # The following way of writing the cache mishandles newlines in values, +# but we know of no workaround that is simple, portable, and efficient. +# So, we kill variables containing newlines. +# Ultrix sh set writes to stderr and can't be redirected directly, +# and sets the high bit in the cache file unless we assign to the vars. ( - for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do + for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do eval ac_val=\$$ac_var case $ac_val in #( *${as_nl}*) case $ac_var in #( - *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + *_cv_*) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf '%s\n' "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( @@ -1838,67 +1813,95 @@ printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} esac ;; esac done + (set) 2>&1 | - case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( + case $as_nl`(ac_space=' '; set) 2>&1` in #( *${as_nl}ac_space=\ *) + # 'set' does not quote correctly, so add quotes: double-quote + # substitution turns \\\\ into \\, and sed turns \\ into \. sed -n \ - "s/'\''/'\''\\\\'\'''\''/g; - s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" + "s/'/'\\\\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" ;; #( *) + # 'set' quotes correctly as required by POSIX, so do not add quotes. sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | sort ) - echo +} - printf "%s\n" "## ----------------- ## +# Print debugging info to stdout. +ac_dump_debugging_info () +{ + echo + + printf '%s\n' "## ---------------- ## +## Cache variables. ## +## ---------------- ##" + echo + ac_cache_dump + echo + + printf '%s\n' "## ----------------- ## ## Output variables. ## ## ----------------- ##" + echo + for ac_var in $ac_subst_vars + do + eval ac_val=\$$ac_var + case $ac_val in + *\'*) ac_val=`printf '%s\n' "$ac_val" | sed "s/'/'\\\\\\\\''/g"`;; + esac + printf '%s\n' "$ac_var='$ac_val'" + done | sort + echo + + if test -n "$ac_subst_files"; then + printf '%s\n' "## ------------------- ## +## File substitutions. ## +## ------------------- ##" echo - for ac_var in $ac_subst_vars + for ac_var in $ac_subst_files do eval ac_val=\$$ac_var case $ac_val in - *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + *\'*) ac_val=`printf '%s\n' "$ac_val" | sed "s/'/'\\\\\\\\''/g"`;; esac - printf "%s\n" "$ac_var='\''$ac_val'\''" + printf '%s\n' "$ac_var='$ac_val'" done | sort echo + fi - if test -n "$ac_subst_files"; then - printf "%s\n" "## ------------------- ## -## File substitutions. ## -## ------------------- ##" - echo - for ac_var in $ac_subst_files - do - eval ac_val=\$$ac_var - case $ac_val in - *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; - esac - printf "%s\n" "$ac_var='\''$ac_val'\''" - done | sort - echo - fi - - if test -s confdefs.h; then - printf "%s\n" "## ----------- ## + if test -s confdefs.h; then + printf '%s\n' "## ----------- ## ## confdefs.h. ## ## ----------- ##" - echo - cat confdefs.h - echo - fi - test "$ac_signal" != 0 && - printf "%s\n" "$as_me: caught signal $ac_signal" - printf "%s\n" "$as_me: exit $exit_status" - } >&5 - rm -f core *.core core.conftest.* && + echo + cat confdefs.h + echo + fi + test "$ac_signal" != 0 && + printf '%s\n' "$as_me: caught signal $ac_signal" + printf '%s\n' "$as_me: exit $exit_status" +} + +# When interrupted or exit'd, cleanup temporary files, and complete +# config.log. +ac_exit_trap () +{ + exit_status= + # Sanitize IFS. + IFS=" "" $as_nl" + # Save into config.log some information that might help in debugging. + ac_dump_debugging_info >&5 + eval "rm -f $ac_clean_CONFIG_STATUS core *.core core.conftest.*" && rm -f -r conftest* confdefs* conf$$* $ac_clean_files && exit $exit_status -' 0 +} + +trap 'ac_exit_trap $?' 0 for ac_signal in 1 2 13 15; do trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal done @@ -1907,21 +1910,21 @@ ac_signal=0 # confdefs.h avoids OS command line length limits that DEFS can exceed. rm -f -r conftest* confdefs.h -printf "%s\n" "/* confdefs.h */" > confdefs.h +printf '%s\n' "/* confdefs.h */" > confdefs.h # Predefined preprocessor variables. -printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h +printf '%s\n' "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h +printf '%s\n' "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h +printf '%s\n' "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h +printf '%s\n' "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h +printf '%s\n' "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h +printf '%s\n' "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h # Let the site file select an alternate cache file if it wants to. @@ -1943,12 +1946,12 @@ do ac_site_file=./$ac_site_file ;; esac if test -f "$ac_site_file" && test -r "$ac_site_file"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 -printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 +printf '%s\n' "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ - || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + || { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "failed to load site script $ac_site_file See 'config.log' for more details" "$LINENO" 5; } fi @@ -1958,27 +1961,120 @@ if test -r "$cache_file"; then # Some versions of bash will fail to source /dev/null (special files # actually), so we avoid doing that. DJGPP emulates it as a regular file. if test /dev/null != "$cache_file" && test -f "$cache_file"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 -printf "%s\n" "$as_me: loading cache $cache_file" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 +printf '%s\n' "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 -printf "%s\n" "$as_me: creating cache $cache_file" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 +printf '%s\n' "$as_me: creating cache $cache_file" >&6;} >$cache_file fi +# Test code for whether the C compiler supports C23 (global declarations) +ac_c_conftest_c23_globals=' +/* Does the compiler advertise conformance to C17 or earlier? + Although GCC 14 does not do that, even with -std=gnu23, + it is close enough, and defines __STDC_VERSION == 202000L. */ +#if !defined __STDC_VERSION__ || __STDC_VERSION__ <= 201710L +# error "Compiler advertises conformance to C17 or earlier" +#endif + +// Check alignas. +char alignas (double) c23_aligned_as_double; +char alignas (0) c23_no_special_alignment; +extern char c23_aligned_as_int; +char alignas (0) alignas (int) c23_aligned_as_int; + +// Check alignof. +enum +{ + c23_int_alignment = alignof (int), + c23_int_array_alignment = alignof (int[100]), + c23_char_alignment = alignof (char) +}; +static_assert (0 < -alignof (int), "alignof is signed"); + +int function_with_unnamed_parameter (int) { return 0; } + +void c23_noreturn (); + +/* Test parsing of string and char UTF-8 literals (including hex escapes). + The parens pacify GCC 15. */ +bool use_u8 = (!sizeof u8"\xFF") == (!u8'\''x'\''); + +bool check_that_bool_works = true | false | !nullptr; +#if !true +# error "true does not work in #if" +#endif +#if false +#elifdef __STDC_VERSION__ +#else +# error "#elifdef does not work" +#endif + +#ifndef __has_c_attribute +# error "__has_c_attribute not defined" +#endif + +#ifndef __has_include +# error "__has_include not defined" +#endif + +#define LPAREN() ( +#define FORTY_TWO(x) 42 +#define VA_OPT_TEST(r, x, ...) __VA_OPT__ (FORTY_TWO r x)) +static_assert (VA_OPT_TEST (LPAREN (), 0, <:-) == 42); + +static_assert (0b101010 == 42); +static_assert (0B101010 == 42); +static_assert (0xDEAD'\''BEEF == 3'\''735'\''928'\''559); +static_assert (0.500'\''000'\''000 == 0.5); + +enum unsignedish : unsigned int { uione = 1 }; +static_assert (0 < -uione); + +#include +constexpr nullptr_t null_pointer = nullptr; + +static typeof (1 + 1L) two () { return 2; } +static long int three () { return 3; } +' + +# Test code for whether the C compiler supports C23 (body of main). +ac_c_conftest_c23_main=' + { + label_before_declaration: + int arr[10] = {}; + if (arr[0]) + goto label_before_declaration; + if (!arr[0]) + goto label_at_end_of_block; + label_at_end_of_block: + } + ok |= !null_pointer; + ok |= two != three; +' + +# Test code for whether the C compiler supports C23 (complete). +ac_c_conftest_c23_program="${ac_c_conftest_c23_globals} + +int +main (int, char **) +{ + int ok = 0; + ${ac_c_conftest_c23_main} + return ok; +} +" + # Test code for whether the C compiler supports C89 (global declarations) ac_c_conftest_c89_globals=' -/* Does the compiler advertise C89 conformance? - Do not test the value of __STDC__, because some compilers set it to 0 - while being otherwise adequately conformant. */ -#if !defined __STDC__ -# error "Compiler does not advertise C89 conformance" -#endif +/* Do not test the value of __STDC__, because some compilers define it to 0 + or do not define it, while otherwise adequately conforming. */ #include #include @@ -2058,7 +2154,8 @@ extern void free (void *); // Check varargs macros. These examples are taken from C99 6.10.3.5. // dprintf is used instead of fprintf to avoid needing to declare -// FILE and stderr. +// FILE and stderr, and "aND" is used instead of "and" to work around +// GCC bug 40564 which is irrelevant here. #define debug(...) dprintf (2, __VA_ARGS__) #define showlist(...) puts (#__VA_ARGS__) #define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__)) @@ -2069,7 +2166,7 @@ test_varargs_macros (void) int y = 5678; debug ("Flag"); debug ("X = %d\n", x); - showlist (The first, second, and third items.); + showlist (The first, second, aND third items.); report (x>y, "x is %d but y is %d", x, y); } @@ -2157,15 +2254,15 @@ ac_c_conftest_c99_main=' // Check restrict. if (test_restrict ("String literal") == 0) success = true; - char *restrict newvar = "Another string"; + const char *restrict newvar = "Another string"; // Check varargs. success &= test_varargs ("s, d'\'' f .", "string", 65, 34.234); test_varargs_macros (); // Check flexible array members. - struct incomplete_array *ia = - malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10)); + static struct incomplete_array *volatile incomplete_array_pointer; + struct incomplete_array *ia = incomplete_array_pointer; ia->datasize = 10; for (int i = 0; i < ia->datasize; ++i) ia->data[i] = i * 1.234; @@ -2181,13 +2278,12 @@ ac_c_conftest_c99_main=' ni.number = 58; - int dynamic_array[ni.number]; - dynamic_array[0] = argv[0][0]; - dynamic_array[ni.number - 1] = 543; + // Do not test for VLAs, as some otherwise-conforming compilers lack them. + // C code should instead use __STDC_NO_VLA__; see Autoconf manual. // work around unused variable warnings ok |= (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == '\''x'\'' - || dynamic_array[ni.number - 1] != 543); + || ni.number != 58); ' # Test code for whether the C compiler supports C11 (global declarations) @@ -2315,38 +2411,44 @@ for ac_var in $ac_precious_vars; do eval ac_new_val=\$ac_env_${ac_var}_value case $ac_old_set,$ac_new_set in set,) - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&5 -printf "%s\n" "$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&5 +printf '%s\n' "$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&2;} ac_cache_corrupted=: ;; ,set) - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run" >&5 -printf "%s\n" "$as_me: error: '$ac_var' was not set in the previous run" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run" >&5 +printf '%s\n' "$as_me: error: '$ac_var' was not set in the previous run" >&2;} ac_cache_corrupted=: ;; ,);; *) if test "x$ac_old_val" != "x$ac_new_val"; then # differences in whitespace do not lead to failure. - ac_old_val_w=`echo x $ac_old_val` - ac_new_val_w=`echo x $ac_new_val` + ac_old_val_w= + for ac_val in x $ac_old_val; do + ac_old_val_w="$ac_old_val_w $ac_val" + done + ac_new_val_w= + for ac_val in x $ac_new_val; do + ac_new_val_w="$ac_new_val_w $ac_val" + done if test "$ac_old_val_w" != "$ac_new_val_w"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:" >&5 -printf "%s\n" "$as_me: error: '$ac_var' has changed since the previous run:" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:" >&5 +printf '%s\n' "$as_me: error: '$ac_var' has changed since the previous run:" >&2;} ac_cache_corrupted=: else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&5 -printf "%s\n" "$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&5 +printf '%s\n' "$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&2;} eval $ac_var=\$ac_old_val fi - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: '$ac_old_val'" >&5 -printf "%s\n" "$as_me: former value: '$ac_old_val'" >&2;} - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: '$ac_new_val'" >&5 -printf "%s\n" "$as_me: current value: '$ac_new_val'" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: former value: '$ac_old_val'" >&5 +printf '%s\n' "$as_me: former value: '$ac_old_val'" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: current value: '$ac_new_val'" >&5 +printf '%s\n' "$as_me: current value: '$ac_new_val'" >&2;} fi;; esac # Pass precious variables to config.status. if test "$ac_new_set" = set; then case $ac_new_val in - *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=$ac_var=`printf '%s\n' "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; *) ac_arg=$ac_var=$ac_new_val ;; esac case " $ac_configure_args " in @@ -2356,10 +2458,10 @@ printf "%s\n" "$as_me: current value: '$ac_new_val'" >&2;} fi done if $ac_cache_corrupted; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 -printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 +printf '%s\n' "$as_me: error: changes in the environment can compromise the build" >&2;} as_fn_error $? "run '${MAKE-make} distclean' and/or 'rm $cache_file' and start over" "$LINENO" 5 fi @@ -2367,6 +2469,23 @@ fi ## Main body of script. ## ## -------------------- ## + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -2458,7 +2577,7 @@ if test ${enable_dynload+y} then : enableval=$enable_dynload; want_dynload="${enableval}" else case e in #( - e) want_dynload=auto ;; + e) want_dynload=yes ;; esac fi @@ -2513,6 +2632,9 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu + + + ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -2521,7 +2643,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args. set dummy ${ac_tool_prefix}gcc; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2542,7 +2664,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}gcc" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2554,11 +2676,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf "%s\n" "$CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf '%s\n' "$CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -2567,7 +2689,7 @@ if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC # Extract the first word of "gcc", so it can be a program name with args. set dummy gcc; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_ac_ct_CC+y} then : @@ -2588,7 +2710,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="gcc" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2600,11 +2722,11 @@ esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -printf "%s\n" "$ac_ct_CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf '%s\n' "$ac_ct_CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi if test "x$ac_ct_CC" = x; then @@ -2612,8 +2734,8 @@ fi else case $cross_compiling:$ac_tool_warned in yes:) -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf '%s\n' "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -2626,7 +2748,7 @@ if test -z "$CC"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args. set dummy ${ac_tool_prefix}cc; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2647,7 +2769,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}cc" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2659,11 +2781,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf "%s\n" "$CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf '%s\n' "$CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -2672,7 +2794,7 @@ fi if test -z "$CC"; then # Extract the first word of "cc", so it can be a program name with args. set dummy cc; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2698,7 +2820,7 @@ do continue fi ac_cv_prog_CC="cc" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2722,11 +2844,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf "%s\n" "$CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf '%s\n' "$CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -2737,7 +2859,7 @@ if test -z "$CC"; then do # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. set dummy $ac_tool_prefix$ac_prog; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2758,7 +2880,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="$ac_tool_prefix$ac_prog" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2770,11 +2892,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf "%s\n" "$CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf '%s\n' "$CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -2787,7 +2909,7 @@ if test -z "$CC"; then do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_ac_ct_CC+y} then : @@ -2808,7 +2930,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="$ac_prog" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2820,11 +2942,11 @@ esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -printf "%s\n" "$ac_ct_CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf '%s\n' "$ac_ct_CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -2836,8 +2958,8 @@ done else case $cross_compiling:$ac_tool_warned in yes:) -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf '%s\n' "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -2849,7 +2971,7 @@ if test -z "$CC"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}clang", so it can be a program name with args. set dummy ${ac_tool_prefix}clang; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2870,7 +2992,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}clang" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2882,11 +3004,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf "%s\n" "$CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf '%s\n' "$CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -2895,7 +3017,7 @@ if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC # Extract the first word of "clang", so it can be a program name with args. set dummy clang; ac_word=$2 -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_ac_ct_CC+y} then : @@ -2916,7 +3038,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="clang" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2928,11 +3050,11 @@ esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -printf "%s\n" "$ac_ct_CC" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf '%s\n' "$ac_ct_CC" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi if test "x$ac_ct_CC" = x; then @@ -2940,8 +3062,8 @@ fi else case $cross_compiling:$ac_tool_warned in yes:) -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf '%s\n' "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -2953,13 +3075,13 @@ fi fi -test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} +test -z "$CC" && { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "no acceptable C compiler found in \$PATH See 'config.log' for more details" "$LINENO" 5; } # Provide some information about the compiler. -printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 +printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 set X $ac_compile ac_compiler=$2 for ac_option in --version -v -V -qversion -version; do @@ -2969,7 +3091,7 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_compiler $ac_option >&5") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -2979,7 +3101,7 @@ printf "%s\n" "$ac_try_echo"; } >&5 cat conftest.er1 >&5 fi rm -f conftest.er1 conftest.err - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } done @@ -2999,9 +3121,9 @@ ac_clean_files="$ac_clean_files a.out a.out.dSYM a.exe b.out" # Try to create an executable without -o first, disregard a.out. # It will help us diagnose broken compilers, and finding out an intuition # of exeext. -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 printf %s "checking whether the C compiler works... " >&6; } -ac_link_default=`printf "%s\n" "$ac_link" | sed 's/ -o *conftest[^ ]*//'` +ac_link_default=`printf '%s\n' "$ac_link" | sed 's/ -o *conftest[^ ]*//'` # The possible output files: ac_files="a.out conftest.exe conftest a.exe a_out.exe b.out conftest.*" @@ -3022,10 +3144,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_link_default") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : # Autoconf-2.13 could set the ac_cv_exeext variable to 'no'. @@ -3066,29 +3188,29 @@ esac fi if test -z "$ac_file" then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } -printf "%s\n" "$as_me: failed program was:" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } +printf '%s\n' "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} +{ { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "C compiler cannot create executables See 'config.log' for more details" "$LINENO" 5; } else case e in #( - e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } ;; + e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 printf %s "checking for C compiler default output file name... " >&6; } -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 -printf "%s\n" "$ac_file" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 +printf '%s\n' "$ac_file" >&6; } ac_exeext=$ac_cv_exeext rm -f -r a.out a.out.dSYM a.exe conftest$ac_cv_exeext b.out ac_clean_files=$ac_clean_files_save -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 printf %s "checking for suffix of executables... " >&6; } if { { ac_try="$ac_link" case "(($ac_try" in @@ -3096,10 +3218,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : # If both 'conftest.exe' and 'conftest' are 'present' (well, observable) @@ -3116,15 +3238,15 @@ for ac_file in conftest.exe conftest conftest.*; do esac done else case e in #( - e) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + e) { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of executables: cannot compile and link See 'config.log' for more details" "$LINENO" 5; } ;; esac fi rm -f conftest conftest$ac_cv_exeext -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 -printf "%s\n" "$ac_cv_exeext" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 +printf '%s\n' "$ac_cv_exeext" >&6; } rm -f conftest.$ac_ext EXEEXT=$ac_cv_exeext @@ -3147,7 +3269,7 @@ _ACEOF ac_clean_files="$ac_clean_files conftest.out" # Check that the compiler produces executables we can run. If not, either # the compiler is broken, or we cross compile. -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 printf %s "checking whether we are cross compiling... " >&6; } if test "$cross_compiling" != yes; then { { ac_try="$ac_link" @@ -3156,10 +3278,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } if { ac_try='./conftest$ac_cv_exeext' { { case "(($ac_try" in @@ -3167,31 +3289,31 @@ printf "%s\n" "$ac_try_echo"; } >&5 *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; }; then cross_compiling=no else if test "$cross_compiling" = maybe; then cross_compiling=yes else - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot run C compiled programs. If you meant to cross compile, use '--host'. See 'config.log' for more details" "$LINENO" 5; } fi fi fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 -printf "%s\n" "$cross_compiling" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 +printf '%s\n' "$cross_compiling" >&6; } rm -f conftest.$ac_ext conftest$ac_cv_exeext \ conftest.o conftest.obj conftest.out ac_clean_files=$ac_clean_files_save -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 printf %s "checking for suffix of object files... " >&6; } if test ${ac_cv_objext+y} then : @@ -3215,10 +3337,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf "%s\n" "$ac_try_echo"; } >&5 +printf '%s\n' "$ac_try_echo"; } >&5 (eval "$ac_compile") 2>&5 ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : for ac_file in conftest.o conftest.obj conftest.*; do @@ -3230,11 +3352,11 @@ then : esac done else case e in #( - e) printf "%s\n" "$as_me: failed program was:" >&5 + e) printf '%s\n' "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} +{ { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of object files: cannot compile See 'config.log' for more details" "$LINENO" 5; } ;; esac @@ -3242,11 +3364,11 @@ fi rm -f conftest.$ac_cv_objext conftest.$ac_ext ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 -printf "%s\n" "$ac_cv_objext" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 +printf '%s\n' "$ac_cv_objext" >&6; } OBJEXT=$ac_cv_objext ac_objext=$OBJEXT -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5 printf %s "checking whether the compiler supports GNU C... " >&6; } if test ${ac_cv_c_compiler_gnu+y} then : @@ -3278,8 +3400,8 @@ ac_cv_c_compiler_gnu=$ac_compiler_gnu ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 -printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 +printf '%s\n' "$ac_cv_c_compiler_gnu" >&6; } ac_compiler_gnu=$ac_cv_c_compiler_gnu if test $ac_compiler_gnu = yes; then @@ -3289,7 +3411,7 @@ else fi ac_test_CFLAGS=${CFLAGS+y} ac_save_CFLAGS=$CFLAGS -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 printf %s "checking whether $CC accepts -g... " >&6; } if test ${ac_cv_prog_cc_g+y} then : @@ -3357,8 +3479,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_c_werror_flag=$ac_save_c_werror_flag ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 -printf "%s\n" "$ac_cv_prog_cc_g" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 +printf '%s\n' "$ac_cv_prog_cc_g" >&6; } if test $ac_test_CFLAGS; then CFLAGS=$ac_save_CFLAGS elif test $ac_cv_prog_cc_g = yes; then @@ -3377,7 +3499,56 @@ fi ac_prog_cc_stdc=no if test x$ac_prog_cc_stdc = xno then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C23 features" >&5 +printf %s "checking for $CC option to enable C23 features... " >&6; } +if test ${ac_cv_prog_cc_c23+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_cv_prog_cc_c23=no +ac_save_CC=$CC +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$ac_c_conftest_c23_program +_ACEOF +for ac_arg in '' -std=gnu23 +do + CC="$ac_save_CC $ac_arg" + if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_prog_cc_c23=$ac_arg +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam + test "x$ac_cv_prog_cc_c23" != "xno" && break +done +rm -f conftest.$ac_ext +CC=$ac_save_CC ;; +esac +fi + +if test "x$ac_cv_prog_cc_c23" = xno +then : + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf '%s\n' "unsupported" >&6; } +else case e in #( + e) if test "x$ac_cv_prog_cc_c23" = x +then : + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf '%s\n' "none needed" >&6; } +else case e in #( + e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c23" >&5 +printf '%s\n' "$ac_cv_prog_cc_c23" >&6; } + CC="$CC $ac_cv_prog_cc_c23" ;; +esac +fi + ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c23 + ac_prog_cc_stdc=c23 ;; +esac +fi +fi +if test x$ac_prog_cc_stdc = xno +then : + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5 printf %s "checking for $CC option to enable C11 features... " >&6; } if test ${ac_cv_prog_cc_c11+y} then : @@ -3389,7 +3560,7 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_c_conftest_c11_program _ACEOF -for ac_arg in '' -std=gnu11 +for ac_arg in '' -std=gnu11 -std:c11 do CC="$ac_save_CC $ac_arg" if ac_fn_c_try_compile "$LINENO" @@ -3406,16 +3577,16 @@ fi if test "x$ac_cv_prog_cc_c11" = xno then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf "%s\n" "unsupported" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf '%s\n' "unsupported" >&6; } else case e in #( e) if test "x$ac_cv_prog_cc_c11" = x then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf "%s\n" "none needed" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf '%s\n' "none needed" >&6; } else case e in #( - e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 -printf "%s\n" "$ac_cv_prog_cc_c11" >&6; } + e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 +printf '%s\n' "$ac_cv_prog_cc_c11" >&6; } CC="$CC $ac_cv_prog_cc_c11" ;; esac fi @@ -3426,7 +3597,7 @@ fi fi if test x$ac_prog_cc_stdc = xno then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5 printf %s "checking for $CC option to enable C99 features... " >&6; } if test ${ac_cv_prog_cc_c99+y} then : @@ -3455,16 +3626,16 @@ fi if test "x$ac_cv_prog_cc_c99" = xno then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf "%s\n" "unsupported" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf '%s\n' "unsupported" >&6; } else case e in #( e) if test "x$ac_cv_prog_cc_c99" = x then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf "%s\n" "none needed" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf '%s\n' "none needed" >&6; } else case e in #( - e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 -printf "%s\n" "$ac_cv_prog_cc_c99" >&6; } + e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 +printf '%s\n' "$ac_cv_prog_cc_c99" >&6; } CC="$CC $ac_cv_prog_cc_c99" ;; esac fi @@ -3475,7 +3646,7 @@ fi fi if test x$ac_prog_cc_stdc = xno then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5 printf %s "checking for $CC option to enable C89 features... " >&6; } if test ${ac_cv_prog_cc_c89+y} then : @@ -3504,16 +3675,16 @@ fi if test "x$ac_cv_prog_cc_c89" = xno then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf "%s\n" "unsupported" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf '%s\n' "unsupported" >&6; } else case e in #( e) if test "x$ac_cv_prog_cc_c89" = x then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf "%s\n" "none needed" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf '%s\n' "none needed" >&6; } else case e in #( - e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 -printf "%s\n" "$ac_cv_prog_cc_c89" >&6; } + e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +printf '%s\n' "$ac_cv_prog_cc_c89" >&6; } CC="$CC $ac_cv_prog_cc_c89" ;; esac fi @@ -3534,7 +3705,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu # Checks for header files. -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sys/wait.h that is POSIX.1 compatible" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for sys/wait.h that is POSIX.1 compatible" >&5 printf %s "checking for sys/wait.h that is POSIX.1 compatible... " >&6; } if test ${ac_cv_header_sys_wait_h+y} then : @@ -3571,11 +3742,11 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 -printf "%s\n" "$ac_cv_header_sys_wait_h" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 +printf '%s\n' "$ac_cv_header_sys_wait_h" >&6; } if test $ac_cv_header_sys_wait_h = yes; then -printf "%s\n" "#define HAVE_SYS_WAIT_H 1" >>confdefs.h +printf '%s\n' "#define HAVE_SYS_WAIT_H 1" >>confdefs.h fi @@ -3585,7 +3756,7 @@ do if test $ac_cache; then ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default" if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then - printf "%s\n" "#define $ac_item 1" >> confdefs.h + printf '%s\n' "#define $ac_item 1" >> confdefs.h fi ac_header= ac_cache= elif test $ac_header; then @@ -3605,31 +3776,31 @@ done if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes then : -printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h +printf '%s\n' "#define STDC_HEADERS 1" >>confdefs.h fi ac_fn_c_check_header_compile "$LINENO" "string.h" "ac_cv_header_string_h" "$ac_includes_default" if test "x$ac_cv_header_string_h" = xyes then : - printf "%s\n" "#define HAVE_STRING_H 1" >>confdefs.h + printf '%s\n' "#define HAVE_STRING_H 1" >>confdefs.h fi ac_fn_c_check_header_compile "$LINENO" "sys/time.h" "ac_cv_header_sys_time_h" "$ac_includes_default" if test "x$ac_cv_header_sys_time_h" = xyes then : - printf "%s\n" "#define HAVE_SYS_TIME_H 1" >>confdefs.h + printf '%s\n' "#define HAVE_SYS_TIME_H 1" >>confdefs.h fi ac_fn_c_check_header_compile "$LINENO" "unistd.h" "ac_cv_header_unistd_h" "$ac_includes_default" if test "x$ac_cv_header_unistd_h" = xyes then : - printf "%s\n" "#define HAVE_UNISTD_H 1" >>confdefs.h + printf '%s\n' "#define HAVE_UNISTD_H 1" >>confdefs.h fi # Checks for typedefs, structures, and compiler characteristics. -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for an ANSI C-conforming const" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for an ANSI C-conforming const" >&5 printf %s "checking for an ANSI C-conforming const... " >&6; } if test ${ac_cv_c_const+y} then : @@ -3670,15 +3841,12 @@ main (void) *t++ = 0; if (s) return 0; } - { /* Someone thinks the Sun supposedly-ANSI compiler will reject this. */ + { /* Derived from code rejected by Sun C 1.0 and similar vintage. */ int x[] = {25, 17}; - const int *foo = &x[0]; + typedef int const *iptr; + iptr foo = &x[0]; ++foo; - } - { /* Sun SC1.0 ANSI compiler rejects this -- but not the above. */ - typedef const int *iptr; - iptr p = 0; - ++p; + if (!*foo) return 0; } { /* IBM XL C 1.02.0.0 rejects this sort of thing, saying "k.c", line 2.27: 1506-025 (S) Operand must be a modifiable lvalue. */ @@ -3706,17 +3874,17 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_const" >&5 -printf "%s\n" "$ac_cv_c_const" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_const" >&5 +printf '%s\n' "$ac_cv_c_const" >&6; } if test $ac_cv_c_const = no; then -printf "%s\n" "#define const /**/" >>confdefs.h +printf '%s\n' "#define const /**/" >>confdefs.h fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports static inline..." >&5 -printf "%s\n" "$as_me: checking whether ${CC} supports static inline..." >&6;} +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports static inline..." >&5 +printf '%s\n' "$as_me: checking whether ${CC} supports static inline..." >&6;} can_inline=no cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -3734,16 +3902,16 @@ then : can_inline=yes fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${can_inline}" >&5 -printf "%s\n" "${can_inline}" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${can_inline}" >&5 +printf '%s\n' "${can_inline}" >&6; } if test "${can_inline}" = yes; then -printf "%s\n" "#define HAVE_STATIC_INLINE 1" >>confdefs.h +printf '%s\n' "#define HAVE_STATIC_INLINE 1" >>confdefs.h fi ### from R m4/R.m4 - needed to hack R 2.9.x -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether setjmp.h is POSIX.1 compatible" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether setjmp.h is POSIX.1 compatible" >&5 printf %s "checking whether setjmp.h is POSIX.1 compatible... " >&6; } if test ${r_cv_header_setjmp_posix+y} then : @@ -3772,10 +3940,10 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $r_cv_header_setjmp_posix" >&5 -printf "%s\n" "$r_cv_header_setjmp_posix" >&6; } -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC options needed to detect all undeclared functions" >&5 -printf %s "checking for $CC options needed to detect all undeclared functions... " >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $r_cv_header_setjmp_posix" >&5 +printf '%s\n' "$r_cv_header_setjmp_posix" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC options to detect undeclared functions" >&5 +printf %s "checking for $CC options to detect undeclared functions... " >&6; } if test ${ac_cv_c_undeclared_builtin_options+y} then : printf %s "(cached) " >&6 @@ -3843,12 +4011,12 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_undeclared_builtin_options" >&5 -printf "%s\n" "$ac_cv_c_undeclared_builtin_options" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_undeclared_builtin_options" >&5 +printf '%s\n' "$ac_cv_c_undeclared_builtin_options" >&6; } case $ac_cv_c_undeclared_builtin_options in #( 'cannot detect') : - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot make $CC report undeclared builtins See 'config.log' for more details" "$LINENO" 5; } ;; #( 'none needed') : @@ -3857,8 +4025,51 @@ See 'config.log' for more details" "$LINENO" 5; } ;; #( ac_c_undeclared_builtin_options=$ac_cv_c_undeclared_builtin_options ;; esac +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC options to ignore future-version functions" >&5 +printf %s "checking for $CC options to ignore future-version functions... " >&6; } +if test ${ac_cv_c_future_darwin_options+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) ac_compile_saved="$ac_compile" + ac_compile="$ac_compile -Werror=unguarded-availability-new" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#if ! (defined __APPLE__ && defined __MACH__) + #error "-Werror=unguarded-availability-new not needed here" + #endif + +int +main (void) +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_c_future_darwin_options='-Werror=unguarded-availability-new' +else case e in #( + e) ac_cv_c_future_darwin_options='none needed' ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ac_compile="$ac_compile_saved" + ;; +esac +fi +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_future_darwin_options" >&5 +printf '%s\n' "$ac_cv_c_future_darwin_options" >&6; } + case $ac_cv_c_future_darwin_options in #( + 'none needed') : + ac_c_future_darwin_options='' ;; #( + *) : + ac_c_future_darwin_options=$ac_cv_c_future_darwin_options ;; +esac + ac_fn_check_decl "$LINENO" "sigsetjmp" "ac_cv_have_decl_sigsetjmp" "#include -" "$ac_c_undeclared_builtin_options" "CFLAGS" +" "$ac_c_undeclared_builtin_options$ac_c_future_darwin_options" "CFLAGS" if test "x$ac_cv_have_decl_sigsetjmp" = xyes then : ac_have_decl=1 @@ -3866,9 +4077,9 @@ else case e in #( e) ac_have_decl=0 ;; esac fi -printf "%s\n" "#define HAVE_DECL_SIGSETJMP $ac_have_decl" >>confdefs.h +printf '%s\n' "#define HAVE_DECL_SIGSETJMP $ac_have_decl" >>confdefs.h ac_fn_check_decl "$LINENO" "siglongjmp" "ac_cv_have_decl_siglongjmp" "#include -" "$ac_c_undeclared_builtin_options" "CFLAGS" +" "$ac_c_undeclared_builtin_options$ac_c_future_darwin_options" "CFLAGS" if test "x$ac_cv_have_decl_siglongjmp" = xyes then : ac_have_decl=1 @@ -3876,7 +4087,7 @@ else case e in #( e) ac_have_decl=0 ;; esac fi -printf "%s\n" "#define HAVE_DECL_SIGLONGJMP $ac_have_decl" >>confdefs.h +printf '%s\n' "#define HAVE_DECL_SIGLONGJMP $ac_have_decl" >>confdefs.h if test "$ac_cv_have_decl_sigsetjmp" = no; then r_cv_header_setjmp_posix=no @@ -3886,11 +4097,11 @@ if test "$ac_cv_have_decl_siglongjmp" = no; then fi if test "${r_cv_header_setjmp_posix}" = yes; then -printf "%s\n" "#define HAVE_POSIX_SETJMP 1" >>confdefs.h +printf '%s\n' "#define HAVE_POSIX_SETJMP 1" >>confdefs.h fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking Java support in R" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking Java support in R" >&5 printf %s "checking Java support in R... " >&6; } R_JAVA_HOME=`"${RBIN}" CMD config JAVA_HOME` : ${JAVA_HOME="${R_JAVA_HOME}"} @@ -3912,14 +4123,14 @@ fi : ${JAVA_CPPFLAGS=`"${RBIN}" CMD config JAVA_CPPFLAGS|sed 's/ERROR:.*//'`} : ${JAVA_LIBS=`"${RBIN}" CMD config JAVA_LIBS|sed 's/ERROR:.*//'`} -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: present: +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: present: interpreter : '${JAVA}' archiver : '${JAR}' compiler : '${JAVAC}' header prep.: '${JAVAH}' cpp flags : '${JAVA_CPPFLAGS}' java libs : '${JAVA_LIBS}'" >&5 -printf "%s\n" "present: +printf '%s\n' "present: interpreter : '${JAVA}' archiver : '${JAR}' compiler : '${JAVAC}' @@ -3957,8 +4168,8 @@ if test `echo foo | sed -e 's:foo:bar:'` = bar; then JAVA_LIBS0=`echo ${JAVA_LIBS} | sed -e 's:$(JAVA_HOME):'${JAVA_HOME}':g'` JAVA_LD_LIBRARY_PATH0=`echo ${JAVA_LD_LIBRARY_PATH} | sed -e 's:$(JAVA_HOME):'${JAVA_HOME}':g'` else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: sed is not working properly - the configuration may fail" >&5 -printf "%s\n" "$as_me: WARNING: sed is not working properly - the configuration may fail" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: sed is not working properly - the configuration may fail" >&5 +printf '%s\n' "$as_me: WARNING: sed is not working properly - the configuration may fail" >&2;} JAVA_CPPFLAGS0="${JAVA_CPPFLAGS}" JAVA_LIBS0="${JAVA_LIBS}" JAVA_LD_LIBRARY_PATH0="${JAVA_LD_LIBRARY_PATH}" @@ -3975,37 +4186,37 @@ if test "x$OSNAME" = xDarwin; then export DYLD_FALLBACK_LIBRARY_PATH fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether Java run-time works" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether Java run-time works" >&5 printf %s "checking whether Java run-time works... " >&6; } if "$JAVA" -classpath tools getsp; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } as_fn_error $? "Java interpreter '$JAVA' does not work" "$LINENO" 5 fi has_xrs="$want_xrs" if test x"$has_xrs" = xauto; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether -Xrs is supported" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether -Xrs is supported" >&5 printf %s "checking whether -Xrs is supported... " >&6; } if "$JAVA" -Xrs -classpath tools getsp; then has_xrs=yes else has_xrs=no fi - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 -printf "%s\n" "${has_xrs}" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 +printf '%s\n' "${has_xrs}" >&6; } fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether -Xrs will be used" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether -Xrs will be used" >&5 printf %s "checking whether -Xrs will be used... " >&6; } -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 -printf "%s\n" "${has_xrs}" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 +printf '%s\n' "${has_xrs}" >&6; } if test x"$has_xrs" = xyes; then -printf "%s\n" "#define HAVE_XRS 1" >>confdefs.h +printf '%s\n' "#define HAVE_XRS 1" >>confdefs.h fi @@ -4020,10 +4231,10 @@ fi if test x"$use_dynload" != xyes; then use_dynload=no fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JVM will be loaded dynamically" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JVM will be loaded dynamically" >&5 printf %s "checking whether JVM will be loaded dynamically... " >&6; } -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${use_dynload}" >&5 -printf "%s\n" "${use_dynload}" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${use_dynload}" >&5 +printf '%s\n' "${use_dynload}" >&6; } if test "x${use_dynload}" = xyes; then USE_DYNLOAD_TRUE= USE_DYNLOAD_FALSE='#' @@ -4033,7 +4244,9 @@ else fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI programs can be compiled" >&5 +if test x${use_dynload} = xno; then + +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JNI programs can be compiled" >&5 printf %s "checking whether JNI programs can be compiled... " >&6; } cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4048,8 +4261,8 @@ int main(void) { _ACEOF if ac_fn_c_try_link "$LINENO" then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } else case e in #( e) as_fn_error $? "Cannot compile a simple JNI program. See config.log for details. @@ -4061,12 +4274,12 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI programs run" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JNI programs run" >&5 printf %s "checking whether JNI programs run... " >&6; } if test "$cross_compiling" = yes then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: don't know (cross-compiling)" >&5 -printf "%s\n" "don't know (cross-compiling)" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: don't know (cross-compiling)" >&5 +printf '%s\n' "don't know (cross-compiling)" >&6; } else case e in #( e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4081,8 +4294,8 @@ int main(void) { _ACEOF if ac_fn_c_try_run "$LINENO" then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } else case e in #( e) as_fn_error $? "Unable to run a simple JNI program. Make sure you have configured R with Java support (see R documentation) and check config.log for failure reason." "$LINENO" 5 ;; esac @@ -4093,12 +4306,12 @@ esac fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking JNI data types" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking JNI data types" >&5 printf %s "checking JNI data types... " >&6; } if test "$cross_compiling" = yes then : - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling See 'config.log' for more details" "$LINENO" 5; } else case e in #( @@ -4113,8 +4326,8 @@ int main(void) { _ACEOF if ac_fn_c_try_run "$LINENO" then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ok" >&5 -printf "%s\n" "ok" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ok" >&5 +printf '%s\n' "ok" >&6; } else case e in #( e) as_fn_error $? "One or more JNI types differ from the corresponding native type. You may need to use non-standard compiler flags or a different compiler in order to fix this." "$LINENO" 5 ;; esac @@ -4125,12 +4338,13 @@ esac fi +fi ## dynload = no if test "${want_jri}" = auto; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JRI should be compiled (autodetect)" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JRI should be compiled (autodetect)" >&5 printf %s "checking whether JRI should be compiled (autodetect)... " >&6; } - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${has_R_shlib}" >&5 -printf "%s\n" "${has_R_shlib}" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${has_R_shlib}" >&5 +printf '%s\n' "${has_R_shlib}" >&6; } want_jri=${has_R_shlib} fi if test "x${want_jri}" = xyes; then @@ -4142,31 +4356,31 @@ else fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether debugging output should be enabled" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether debugging output should be enabled" >&5 printf %s "checking whether debugging output should be enabled... " >&6; } if test "${want_debug}" = yes; then JAVA_CPPFLAGS="-g -DRJ_DEBUG ${JAVA_CPPFLAGS}" - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether memory profiling is desired" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether memory profiling is desired" >&5 printf %s "checking whether memory profiling is desired... " >&6; } if test "${want_memprof}" = debug; then want_memprof="${want_debug}" fi if test "${want_memprof}" = yes; then -printf "%s\n" "#define MEMPROF 1" >>confdefs.h +printf '%s\n' "#define MEMPROF 1" >>confdefs.h - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi @@ -4177,62 +4391,62 @@ fi use_threads=no -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether threads support is requested" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether threads support is requested" >&5 printf %s "checking whether threads support is requested... " >&6; } if test "${want_threads}" = yes; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether threads can be enabled" >&5 + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether threads can be enabled" >&5 printf %s "checking whether threads can be enabled... " >&6; } # check whether we can add THREADS support # we don't want to run full AC_CANONICAL_HOST, all we care about is OS X if test "x$OSNAME" = xDarwin; then use_threads=yes -printf "%s\n" "#define THREADS 1" >>confdefs.h +printf '%s\n' "#define THREADS 1" >>confdefs.h fi - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${use_threads}" >&5 -printf "%s\n" "${use_threads}" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${use_threads}" >&5 +printf '%s\n' "${use_threads}" >&6; } else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi ## enable callbacks if desired -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether callbacks support is requested" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether callbacks support is requested" >&5 printf %s "checking whether callbacks support is requested... " >&6; } if test "${want_callbacks}" = yes; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } if test "${want_jri}" != yes; then as_fn_error $? "Callbacks support can be only enabled if JRI is enabled as well." "$LINENO" 5 fi -printf "%s\n" "#define ENABLE_JRICB 1" >>confdefs.h +printf '%s\n' "#define ENABLE_JRICB 1" >>confdefs.h else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI cache support is requested" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JNI cache support is requested" >&5 printf %s "checking whether JNI cache support is requested... " >&6; } if test "${want_jni_cache}" = yes; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } if test "${use_threads}" = yes; then as_fn_error $? "Threads and JNI cache cannot be used at the same time, because JNI cache is by definition not thread-safe. Please disable either option." "$LINENO" 5 fi -printf "%s\n" "#define JNI_CACHE 1" >>confdefs.h +printf '%s\n' "#define JNI_CACHE 1" >>confdefs.h else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether headless init is enabled" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether headless init is enabled" >&5 printf %s "checking whether headless init is enabled... " >&6; } if test "${want_headless}" = auto; then want_headless=no @@ -4241,19 +4455,19 @@ if test "${want_headless}" = auto; then want_headless=yes fi fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${want_headless}" >&5 -printf "%s\n" "${want_headless}" >&6; } +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${want_headless}" >&5 +printf '%s\n' "${want_headless}" >&6; } if test "${want_headless}" = yes; then -printf "%s\n" "#define USE_HEADLESS_INIT 1" >>confdefs.h +printf '%s\n' "#define USE_HEADLESS_INIT 1" >>confdefs.h fi -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JRI is requested" >&5 +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JRI is requested" >&5 printf %s "checking whether JRI is requested... " >&6; } if test "${want_jri}" = yes; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf '%s\n' "yes" >&6; } export R_HOME export JAVA_HOME JAVA_CPPFLAGS JAVA_LIBS JAVA_LD_LIBRARY_PATH JAVA JAVAC JAVAH JAR CONFIGURED=1 @@ -4263,8 +4477,8 @@ printf "%s\n" "yes" >&6; } subdirs="$subdirs src/jri" else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf '%s\n' "no" >&6; } fi ac_config_files="$ac_config_files src/Makevars" @@ -4287,44 +4501,7 @@ cat >confcache <<\_ACEOF _ACEOF -# The following way of writing the cache mishandles newlines in values, -# but we know of no workaround that is simple, portable, and efficient. -# So, we kill variables containing newlines. -# Ultrix sh set writes to stderr and can't be redirected directly, -# and sets the high bit in the cache file unless we assign to the vars. -( - for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do - eval ac_val=\$$ac_var - case $ac_val in #( - *${as_nl}*) - case $ac_var in #( - *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; - esac - case $ac_var in #( - _ | IFS | as_nl) ;; #( - BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( - *) { eval $ac_var=; unset $ac_var;} ;; - esac ;; - esac - done - - (set) 2>&1 | - case $as_nl`(ac_space=' '; set) 2>&1` in #( - *${as_nl}ac_space=\ *) - # 'set' does not quote correctly, so add quotes: double-quote - # substitution turns \\\\ into \\, and sed turns \\ into \. - sed -n \ - "s/'/'\\\\''/g; - s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" - ;; #( - *) - # 'set' quotes correctly as required by POSIX, so do not add quotes. - sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" - ;; - esac | - sort -) | +ac_cache_dump | sed ' /^ac_cv_env_/b end t clear @@ -4336,8 +4513,8 @@ printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} if diff "$cache_file" confcache >/dev/null 2>&1; then :; else if test -w "$cache_file"; then if test "x$cache_file" != "x/dev/null"; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 -printf "%s\n" "$as_me: updating cache $cache_file" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 +printf '%s\n' "$as_me: updating cache $cache_file" >&6;} if test ! -f "$cache_file" || test -h "$cache_file"; then cat confcache >"$cache_file" else @@ -4351,8 +4528,8 @@ printf "%s\n" "$as_me: updating cache $cache_file" >&6;} fi fi else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 -printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 +printf '%s\n' "$as_me: not updating unwritable cache $cache_file" >&6;} fi fi rm -f confcache @@ -4369,7 +4546,7 @@ U= for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue # 1. Remove the extension, and $U if already installed. ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' - ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"` + ac_i=`printf '%s\n' "$ac_i" | sed "$ac_script"` # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR # will be set to the directory where LIBOBJS objects are built. as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" @@ -4390,13 +4567,21 @@ Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi : "${CONFIG_STATUS=./config.status}" +case $CONFIG_STATUS in #( + -*) : + CONFIG_STATUS=./$CONFIG_STATUS ;; #( + */*) : + ;; #( + *) : + CONFIG_STATUS=./$CONFIG_STATUS ;; +esac + ac_write_fail=0 -ac_clean_files_save=$ac_clean_files -ac_clean_files="$ac_clean_files $CONFIG_STATUS" -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 -printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;} +ac_clean_CONFIG_STATUS='"$CONFIG_STATUS"' +{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 +printf '%s\n' "$as_me: creating $CONFIG_STATUS" >&6;} as_write_fail=0 -cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 +cat >"$CONFIG_STATUS" <<_ASEOF || as_write_fail=1 #! $SHELL # Generated by $as_me. # Run this file to recreate the current configuration. @@ -4410,7 +4595,7 @@ ac_cs_silent=false SHELL=\${CONFIG_SHELL-$SHELL} export SHELL _ASEOF -cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ASEOF || as_write_fail=1 ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## @@ -4422,7 +4607,7 @@ then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which - # is contrary to our usage. Disable this feature. + # contradicts POSIX and common usage. Disable this. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else case e in #( @@ -4509,7 +4694,7 @@ if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then - printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + printf '%s\n' "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi @@ -4525,9 +4710,9 @@ as_fn_error () as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - printf "%s\n" "$as_me: error: $2" >&2 + printf '%s\n' "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -4622,7 +4807,7 @@ as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || -printf "%s\n" X/"$0" | +printf '%s\n' X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q @@ -4644,29 +4829,6 @@ as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits - -# Determine whether it's possible to make 'echo' print without a newline. -# These variables are no longer used directly by Autoconf, but are AC_SUBSTed -# for compatibility with existing Makefiles. -ECHO_C= ECHO_N= ECHO_T= -case `echo -n x` in #((((( --n*) - case `echo 'xy\c'` in - *c*) ECHO_T=' ';; # ECHO_T is single tab character. - xy) ECHO_C='\c';; - *) echo `echo ksh88 bug on AIX 6.1` > /dev/null - ECHO_T=' ';; - esac;; -*) - ECHO_N='-n';; -esac - -# For backward compatibility with old third-party macros, we provide -# the shell variables $as_echo and $as_echo_n. New code should use -# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. -as_echo='printf %s\n' -as_echo_n='printf %s' - rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -4708,7 +4870,7 @@ as_fn_mkdir_p () as_dirs= while :; do case $as_dir in #( - *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *\'*) as_qdir=`printf '%s\n' "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" @@ -4717,7 +4879,7 @@ $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -printf "%s\n" X"$as_dir" | +printf '%s\n' X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -4770,19 +4932,19 @@ as_tr_sh="eval sed '$as_sed_sh'" # deprecated exec 6>&1 -## ----------------------------------- ## -## Main body of $CONFIG_STATUS script. ## -## ----------------------------------- ## +## ------------------------------------- ## +## Main body of "$CONFIG_STATUS" script. ## +## ------------------------------------- ## _ASEOF -test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 +test $as_write_fail = 0 && chmod +x "$CONFIG_STATUS" || ac_write_fail=1 -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 # Save the log message, to keep $0 and so on meaningful, and to # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" This file was extended by rJava $as_me 0.8, which was -generated by GNU Autoconf 2.72. Invocation command line was +generated by GNU Autoconf 2.73. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS @@ -4804,14 +4966,14 @@ case $ac_config_headers in *" esac -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 # Files that config.status was made for. config_files="$ac_config_files" config_headers="$ac_config_headers" _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 ac_cs_usage="\ '$as_me' instantiates files and other configuration actions from templates according to the current configuration. Unless the files @@ -4840,25 +5002,29 @@ $config_headers Report bugs to ." _ACEOF -ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"` -ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +ac_cs_config=`printf '%s\n' "$ac_configure_args" | sed "$ac_safe_unquote"` +ac_cs_config_escaped=`printf '%s\n' "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ rJava config.status 0.8 -configured by $0, generated by GNU Autoconf 2.72, +configured by $0, generated by GNU Autoconf 2.73, with options \\"\$ac_cs_config\\" -Copyright (C) 2023 Free Software Foundation, Inc. +Copyright (C) 2026 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." ac_pwd='$ac_pwd' srcdir='$srcdir' -test -n "\$AWK" || AWK=awk +test -n "\$AWK" || { + awk '' >$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 # The default lists apply if the user does not specify any file. ac_need_defaults=: while test $# != 0 @@ -4886,15 +5052,15 @@ do -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) ac_cs_recheck=: ;; --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) - printf "%s\n" "$ac_cs_version"; exit ;; + printf '%s\n' "$ac_cs_version"; exit ;; --config | --confi | --conf | --con | --co | --c ) - printf "%s\n" "$ac_cs_config"; exit ;; + printf '%s\n' "$ac_cs_config"; exit ;; --debug | --debu | --deb | --de | --d | -d ) debug=: ;; --file | --fil | --fi | --f ) $ac_shift case $ac_optarg in - *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_optarg=`printf '%s\n' "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; '') as_fn_error $? "missing file argument" ;; esac as_fn_append CONFIG_FILES " '$ac_optarg'" @@ -4902,7 +5068,7 @@ do --header | --heade | --head | --hea ) $ac_shift case $ac_optarg in - *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_optarg=`printf '%s\n' "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append CONFIG_HEADERS " '$ac_optarg'" ac_need_defaults=false;; @@ -4911,7 +5077,7 @@ do as_fn_error $? "ambiguous option: '$1' Try '$0 --help' for more information.";; --help | --hel | -h ) - printf "%s\n" "$ac_cs_usage"; exit ;; + printf '%s\n' "$ac_cs_usage"; exit ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil | --si | --s) ac_cs_silent=: ;; @@ -4935,32 +5101,32 @@ if $ac_cs_silent; then fi _ACEOF -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 if \$ac_cs_recheck; then set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion shift - \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6 + \printf '%s\n' "running CONFIG_SHELL=$SHELL \$*" >&6 CONFIG_SHELL='$SHELL' export CONFIG_SHELL exec "\$@" fi _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 exec 5>>config.log { echo sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX ## Running $as_me. ## _ASBOX - printf "%s\n" "$ac_log" + printf '%s\n' "$ac_log" } >&5 _ACEOF -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 # Handling of arguments. for ac_config_target in $ac_config_targets @@ -5041,13 +5207,13 @@ _ACEOF echo "_ACEOF" } >conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 -ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` +ac_delim_num=`echo "$ac_subst_vars" | sed -n '$='` ac_delim='%!_!# ' for ac_last_try in false false false false false :; do . ./conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 - ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` + ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | sed -n '$='` if test $ac_delim_n = $ac_delim_num; then break elif $ac_last_try; then @@ -5058,7 +5224,7 @@ for ac_last_try in false false false false false :; do done rm -f conf$$subs.sh -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && _ACEOF sed -n ' @@ -5103,9 +5269,9 @@ t delim N s/\n// } -' >>$CONFIG_STATUS || ac_write_fail=1 +' >>"$CONFIG_STATUS" || ac_write_fail=1 rm -f conf$$subs.awk -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 _ACAWK cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && for (key in S) S_is_set[key] = 1 @@ -5134,7 +5300,7 @@ cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && _ACAWK _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" else @@ -5166,7 +5332,7 @@ s/^[^=]*=[ ]*$// }' fi -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 fi # test -n "$CONFIG_FILES" # Set up the scripts for CONFIG_HEADERS section. @@ -5237,9 +5403,9 @@ s/["\\]/\\&/g; s/^/"/; s/$/\\\\\\n"\\/p b cont ' >$CONFIG_STATUS || ac_write_fail=1 +"/g' >>"$CONFIG_STATUS" || ac_write_fail=1 -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 for (key in D) D_is_set[key] = 1 FS = "" } @@ -5257,8 +5423,12 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 macro = mac2[1] prefix = substr(line, 1, index(line, defundef) - 1) if (D_is_set[macro]) { + suffix = P[macro] D[macro] + while (suffix ~ /[\t ]$/) { + suffix = substr(suffix, 1, length(suffix) - 1) + } # Preserve the white space surrounding the "#". - print prefix "define", macro P[macro] D[macro] + print prefix "define", macro suffix next } else { # Replace #undef with comments. This is necessary, for example, @@ -5273,7 +5443,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 { print } _ACAWK _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 as_fn_error $? "could not setup config headers machinery" "$LINENO" 5 fi # test -n "$CONFIG_HEADERS" @@ -5317,7 +5487,7 @@ do esac || as_fn_error 1 "cannot find input file: '$ac_f'" "$LINENO" 5;; esac - case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac + case $ac_f in *\'*) ac_f=`printf '%s\n' "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" done @@ -5325,17 +5495,17 @@ do # use $as_me), people would be surprised to read: # /* config.h. Generated by config.status. */ configure_input='Generated from '` - printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' + printf '%s\n' "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' `' by configure.' if test x"$ac_file" != x-; then configure_input="$ac_file. $configure_input" - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 -printf "%s\n" "$as_me: creating $ac_file" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 +printf '%s\n' "$as_me: creating $ac_file" >&6;} fi # Neutralize special characters interpreted by sed in replacement strings. case $configure_input in #( *\&* | *\|* | *\\* ) - ac_sed_conf_input=`printf "%s\n" "$configure_input" | + ac_sed_conf_input=`printf '%s\n' "$configure_input" | sed 's/[\\\\&|]/\\\\&/g'`;; #( *) ac_sed_conf_input=$configure_input;; esac @@ -5352,7 +5522,7 @@ $as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$ac_file" : 'X\(//\)[^/]' \| \ X"$ac_file" : 'X\(//\)$' \| \ X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || -printf "%s\n" X"$ac_file" | +printf '%s\n' X"$ac_file" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -5376,9 +5546,9 @@ printf "%s\n" X"$ac_file" | case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf '%s\n' "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf '%s\n' "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -5414,7 +5584,7 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 # If the template does not know about datarootdir, expand it. # FIXME: This hack should be removed a few years after 2.60. ac_datarootdir_hack=; ac_datarootdir_seen= @@ -5431,10 +5601,10 @@ ac_sed_dataroot=' case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in *datarootdir*) ac_datarootdir_seen=yes;; *@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 -printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 +printf '%s\n' "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} _ACEOF -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 ac_datarootdir_hack=' s&@datadir@&$datadir&g s&@docdir@&$docdir&g @@ -5448,11 +5618,11 @@ _ACEOF # Neutralize VPATH when '$srcdir' = '.'. # Shell code in configure.ac might set extrasub. # FIXME: do we really want to maintain this feature? -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 ac_sed_extra="$ac_vpsub $extrasub _ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 +cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 :t /@[a-zA-Z_][a-zA-Z_0-9]*@/!b s|@configure_input@|$ac_sed_conf_input|;t t @@ -5474,9 +5644,9 @@ test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ "$ac_tmp/out"`; test -z "$ac_out"; } && - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir' + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&5 -printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir' +printf '%s\n' "$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$ac_tmp/stdin" @@ -5492,20 +5662,20 @@ which seems to be undefined. Please make sure it is defined" >&2;} # if test x"$ac_file" != x-; then { - printf "%s\n" "/* $configure_input */" >&1 \ + printf '%s\n' "/* $configure_input */" >&1 \ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" } >"$ac_tmp/config.h" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 if diff "$ac_file" "$ac_tmp/config.h" >/dev/null 2>&1; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 -printf "%s\n" "$as_me: $ac_file is unchanged" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 +printf '%s\n' "$as_me: $ac_file is unchanged" >&6;} else rm -f "$ac_file" mv "$ac_tmp/config.h" "$ac_file" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 fi else - printf "%s\n" "/* $configure_input */" >&1 \ + printf '%s\n' "/* $configure_input */" >&1 \ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" \ || as_fn_error $? "could not create -" "$LINENO" 5 fi @@ -5519,7 +5689,7 @@ done # for ac_tag as_fn_exit 0 _ACEOF -ac_clean_files=$ac_clean_files_save +ac_clean_CONFIG_STATUS= test $ac_write_fail = 0 || as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 @@ -5535,11 +5705,18 @@ test $ac_write_fail = 0 || # need to make the FD available again. if test "$no_create" != yes; then ac_cs_success=: + case $CONFIG_STATUS in #( + -*) : + ac_no_opts=-- ;; #( + *) : + ac_no_opts= ;; +esac ac_config_status_args= test "$silent" = yes && ac_config_status_args="$ac_config_status_args --quiet" exec 5>/dev/null - $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false + $SHELL $ac_no_opts "$CONFIG_STATUS" $ac_config_status_args || + ac_cs_success=false exec 5>>config.log # Use ||, not &&, to avoid exiting from the if with $? = 1, which # would make configure fail if this is the last instruction. @@ -5585,7 +5762,7 @@ if test "$no_recursion" != yes; then ;; *) case $ac_arg in - *\'*) ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append ac_sub_configure_args " '$ac_arg'" ;; esac @@ -5595,7 +5772,7 @@ if test "$no_recursion" != yes; then # in subdir configurations. ac_arg="--prefix=$prefix" case $ac_arg in - *\'*) ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac ac_sub_configure_args="'$ac_arg' $ac_sub_configure_args" @@ -5616,17 +5793,17 @@ if test "$no_recursion" != yes; then test -d "$srcdir/$ac_dir" || continue ac_msg="=== configuring in $ac_dir (`pwd`/$ac_dir)" - printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_msg" >&5 - printf "%s\n" "$ac_msg" >&6 + printf '%s\n' "$as_me:${as_lineno-$LINENO}: $ac_msg" >&5 + printf '%s\n' "$ac_msg" >&6 as_dir="$ac_dir"; as_fn_mkdir_p ac_builddir=. case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf '%s\n' "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf '%s\n' "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -5663,8 +5840,8 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix elif test -f "$ac_srcdir/configure"; then ac_sub_configure=$ac_srcdir/configure else - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: no configuration information is in $ac_dir" >&5 -printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: no configuration information is in $ac_dir" >&5 +printf '%s\n' "$as_me: WARNING: no configuration information is in $ac_dir" >&2;} ac_sub_configure= fi @@ -5677,8 +5854,8 @@ printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2; ac_sub_cache_file=$ac_top_build_prefix$cache_file ;; esac - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&5 -printf "%s\n" "$as_me: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&6;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&5 +printf '%s\n' "$as_me: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&6;} # The eval makes quoting arguments work. eval "\$SHELL \"\$ac_sub_configure\" $ac_sub_configure_args \ --cache-file=\"\$ac_sub_cache_file\" --srcdir=\"\$ac_srcdir\"" || @@ -5689,8 +5866,8 @@ printf "%s\n" "$as_me: running $SHELL $ac_sub_configure $ac_sub_configure_args - done fi if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 -printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} + { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 +printf '%s\n' "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi diff --git a/mkdist b/mkdist index 503a1be..4aaed03 100644 --- a/mkdist +++ b/mkdist @@ -178,7 +178,7 @@ VER=`sed -n -e 's/.*rJava v\([0-9.-]\{1,\}\).*/\1/p' src/rJava.h` echo "rJava version ${VER}" cat DESCRIPTION| sed "s/Version:.*/Version: ${VER}/" > d mv d DESCRIPTION -rm -rf tests +rm -rf tests tests-integration if test -n "${OPTIMIZED}"; then echo "Creating optimized sources ..." echo '#define ONEFILE 1' > src/all diff --git a/src/Rglue.c b/src/Rglue.c index a514ad9..db624ce 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1143,7 +1143,7 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { if (TYPEOF(sPath) != STRSXP || LENGTH(sPath) != 1) Rf_error("Invalid path to JVM library"); /* FIXME: worry about encoding? */ - path = CHAR(STRING_ELT(sPath, 0)); + path = Rf_translateCharUTF8(STRING_ELT(sPath, 0)); res = djni_load(path); if (silent) return Rf_ScalarInteger(res); @@ -1157,10 +1157,8 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { REPC SEXP RunloadJVM(void) { int res; - if (rJava_JVM_state == JVM_STATE_CREATED || - rJava_JVM_state == JVM_STATE_ATTACHED) { - destroyJVM(); - } + if (rJava_JVM_state != JVM_STATE_NONE || existingJVMs() > 0) + Rf_error("Cannot unload a JVM that has been started; use a new R process."); res = djni_unload(); return Rf_ScalarLogical((res < -1) ? 0 : 1); } diff --git a/src/djni.c b/src/djni.c index d9445ad..e88364d 100644 --- a/src/djni.c +++ b/src/djni.c @@ -1,171 +1,101 @@ +/* Dynamic JNI loader: no JVM dependency is resolved at namespace load. */ #ifdef _WIN32 +#define _WIN32_WINNT 0x0602 #include +#else +#include +#endif #include #include "djni.h" static JNI_GetDefaultJavaVMInitArgs_fnptr JNI_GetDefaultJavaVMInitArgs_fn; static JNI_CreateJavaVM_fnptr JNI_CreateJavaVM_fn; static JNI_GetCreatedJavaVMs_fnptr JNI_GetCreatedJavaVMs_fn; +#ifdef _WIN32 +static HMODULE jni_dl; +#else +static void *jni_dl; +#endif +static char last_error[1024]; -static HMODULE jni_hmodule = NULL; -static char last_error_buf[1024]; - -static int load_sym(const char *sym, void **ptr) { - void *v = (void*)GetProcAddress(jni_hmodule, sym); - if (!v) { - DWORD err = GetLastError(); - FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - last_error_buf, sizeof(last_error_buf), NULL); - FreeLibrary(jni_hmodule); - jni_hmodule = NULL; - return -1; - } - *ptr = v; - return 0; -} - -int djni_load(const char *path) { - last_error_buf[0] = 0; - if (jni_hmodule) return -1; - - // Use LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH to handle dependencies - // located in the same directory as jvm.dll (e.g. msvcr*.dll) - jni_hmodule = LoadLibraryExA(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); - - if (!jni_hmodule) { - DWORD err = GetLastError(); - FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - last_error_buf, sizeof(last_error_buf), NULL); - return -2; - } - - if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void**) &JNI_GetDefaultJavaVMInitArgs_fn) || - load_sym("JNI_CreateJavaVM", (void**) &JNI_CreateJavaVM_fn) || - load_sym("JNI_GetCreatedJavaVMs", (void**) &JNI_GetCreatedJavaVMs_fn)) { - JNI_GetDefaultJavaVMInitArgs_fn = 0; - JNI_CreateJavaVM_fn = 0; - JNI_GetCreatedJavaVMs_fn = 0; - FreeLibrary(jni_hmodule); - jni_hmodule = NULL; - return -3; - } - return 0; -} - -int djni_unload(void) { - if (jni_hmodule) { - if (!FreeLibrary(jni_hmodule)) - return -2; - jni_hmodule = NULL; - return 0; - } - return -1; -} - -int djni_loaded(void) { - return jni_hmodule ? 1 : 0; -} - -const char* djni_last_error(void) { - return last_error_buf; -} - -// Pass-through functions -jint JNI_GetDefaultJavaVMInitArgs(void *args) { - return JNI_GetDefaultJavaVMInitArgs_fn ? JNI_GetDefaultJavaVMInitArgs_fn(args) : -99; -} - -jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args) { - return JNI_CreateJavaVM_fn ? JNI_CreateJavaVM_fn(pvm, penv, args) : -99; -} - -jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { - return JNI_GetCreatedJavaVMs_fn ? JNI_GetCreatedJavaVMs_fn(vmBuf, bufLen, nVMs) : -99; +static void clear_symbols(void) { + JNI_GetDefaultJavaVMInitArgs_fn = 0; + JNI_CreateJavaVM_fn = 0; + JNI_GetCreatedJavaVMs_fn = 0; } - +static void save_error(void) { +#ifdef _WIN32 + DWORD code = GetLastError(); + snprintf(last_error, sizeof(last_error), "Windows loader error %lu", (unsigned long) code); #else -/* ... Existing POSIX code goes here (keep existing src/djni.c content in #else block) ... */ -#include - -#include "djni.h" - -static JNI_GetDefaultJavaVMInitArgs_fnptr JNI_GetDefaultJavaVMInitArgs_fn; -static JNI_CreateJavaVM_fnptr JNI_CreateJavaVM_fn; -static JNI_GetCreatedJavaVMs_fnptr JNI_GetCreatedJavaVMs_fn; - -static void *jni_dl = 0; - -static const char *last_error = 0; - + const char *error = dlerror(); + snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); +#endif +} static int load_sym(const char *sym, void **ptr) { - void *v = dlsym(jni_dl, sym); - if (!v) { - last_error = dlerror(); - dlclose(jni_dl); - jni_dl = 0; - return -1; - } - *ptr = v; - return 0; +#ifdef _WIN32 + *ptr = (void *) GetProcAddress(jni_dl, sym); +#else + *ptr = dlsym(jni_dl, sym); +#endif + if (!*ptr) { save_error(); return -1; } + return 0; } - -/* path: path to libjvm - returns: 0 = success, -1 = already loaded, -2 = dlopen error, -3 symbols not found */ int djni_load(const char *path) { - last_error = 0; + last_error[0] = 0; if (jni_dl) return -1; - jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW); - if (!jni_dl) { - last_error = dlerror(); - return -2; +#ifdef _WIN32 + wchar_t wide_path[32768], bin_path[32768], *slash; + DLL_DIRECTORY_COOKIE cookie = 0; + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide_path, 32768)) { + save_error(); return -2; + } + /* JDK 8 needs its bin directory as well as bin/server for dependent DLLs. */ + wcscpy(bin_path, wide_path); + for (slash = bin_path; *slash; slash++) if (*slash == L'/') *slash = L'\\'; + slash = wcsrchr(bin_path, L'\\'); + if (slash) { + *slash = 0; + slash = wcsrchr(bin_path, L'\\'); + if (slash) { *slash = 0; cookie = AddDllDirectory(bin_path); } } - if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void**) &JNI_GetDefaultJavaVMInitArgs_fn) || - load_sym("JNI_CreateJavaVM", (void**) &JNI_CreateJavaVM_fn) || - load_sym("JNI_GetCreatedJavaVMs", (void**) &JNI_GetCreatedJavaVMs_fn)) { - JNI_GetDefaultJavaVMInitArgs_fn = 0; - JNI_CreateJavaVM_fn = 0; - JNI_GetCreatedJavaVMs_fn = 0; - dlclose(jni_dl); - jni_dl = 0; - return -3; + jni_dl = LoadLibraryExW(wide_path, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!jni_dl) save_error(); + if (cookie) RemoveDllDirectory(cookie); +#else + jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW); + if (!jni_dl) save_error(); +#endif + if (!jni_dl) return -2; + if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void **) &JNI_GetDefaultJavaVMInitArgs_fn) || + load_sym("JNI_CreateJavaVM", (void **) &JNI_CreateJavaVM_fn) || + load_sym("JNI_GetCreatedJavaVMs", (void **) &JNI_GetCreatedJavaVMs_fn)) { + djni_unload(); + return -3; } return 0; } - -/* returns: 0 = success, -1 = noting loaded, -2 = unload failed */ int djni_unload(void) { - if (jni_dl) { - if (dlclose(jni_dl)) - return -2; - jni_dl = 0; - return 0; - } - return -1; -} - -int djni_loaded(void) { - return jni_dl ? 1 : 0; -} - -const char* djni_last_error(void) { - return last_error; + if (!jni_dl) return -1; +#ifdef _WIN32 + if (!FreeLibrary(jni_dl)) { save_error(); return -2; } +#else + if (dlclose(jni_dl)) { save_error(); return -2; } +#endif + jni_dl = 0; + clear_symbols(); + return 0; } - -/* The following are the "normal" JNI API calls which are routed to libjvm JNI API. - If no JNI was loaded, they return -99 to distinguish it from JNI error codes. */ - +int djni_loaded(void) { return jni_dl ? 1 : 0; } +const char *djni_last_error(void) { return last_error[0] ? last_error : 0; } jint JNI_GetDefaultJavaVMInitArgs(void *args) { - return JNI_GetDefaultJavaVMInitArgs_fn ? JNI_GetDefaultJavaVMInitArgs_fn(args) : -99; + return JNI_GetDefaultJavaVMInitArgs_fn ? JNI_GetDefaultJavaVMInitArgs_fn(args) : JNI_NO_DJNI; } - jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args) { - return JNI_CreateJavaVM_fn ? JNI_CreateJavaVM_fn(pvm, penv, args) : -99; + return JNI_CreateJavaVM_fn ? JNI_CreateJavaVM_fn(pvm, penv, args) : JNI_NO_DJNI; } - jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { - return JNI_GetCreatedJavaVMs_fn ? JNI_GetCreatedJavaVMs_fn(vmBuf, bufLen, nVMs) : -99; + return JNI_GetCreatedJavaVMs_fn ? JNI_GetCreatedJavaVMs_fn(vmBuf, bufLen, nVMs) : JNI_NO_DJNI; } /* define DJNI_MAIN_TEST to test the load + create + print version + destroy + unload cycle @@ -249,13 +179,13 @@ static int run_jvm_test(void) { int test_jvm(const char *path) { int e = djni_load(path); if (e) { - printf("Cannot load: %d (%s)\n", e, last_error ? last_error : "not a dl error"); + printf("Cannot load: %d (%s)\n", e, djni_last_error() ? djni_last_error() : "not a dl error"); return 1; } printf("JVM Loaded!\n"); if (!run_jvm_test()) { if (djni_unload()) - fprintf(stderr, "ERROR: cannot unload JNI (%s)\n", last_error ? last_error : "no information"); + fprintf(stderr, "ERROR: cannot unload JNI (%s)\n", djni_last_error() ? djni_last_error() : "no information"); else printf("JNI unloaded.\n"); } @@ -295,4 +225,3 @@ int main(int ac, char **av) { } #endif -#endif diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R new file mode 100644 index 0000000..ce4b1e7 --- /dev/null +++ b/tests-integration/deferred.R @@ -0,0 +1,46 @@ +# Each scenario must run in a fresh R process against the installed source build. +args <- commandArgs(TRUE) +scenario <- args[1] +home_a <- Sys.getenv("RJE_JAVA_HOME_A") +home_b <- Sys.getenv("RJE_JAVA_HOME_B") +stopifnot(dir.exists(home_a), dir.exists(home_b)) +Sys.setenv(JAVA_HOME = file.path(tempdir(), "missing-jdk")) +old_path <- Sys.getenv("PATH") +ns <- loadNamespace("rJava") +state <- function() get(".jvmState", ns)() +stopifnot(identical(state()$state, "not-loaded"), !state()$initialized, + identical(old_path, Sys.getenv("PATH"))) +lib <- function(home) { + pattern <- if (.Platform$OS.type == "windows") "^jvm[.]dll$" else "^libjvm[.](so|dylib)$" + paths <- list.files(home, pattern, recursive = TRUE, full.names = TRUE) + paths <- paths[grepl("/server/", paths, fixed = TRUE)] + stopifnot(length(paths) >= 1L) + normalizePath(paths[1], winslash = "/") +} +if (scenario == "retry") { + failed <- try(rJava::.jinit(), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") + # A native library with no JNI exports must fail cleanly and permit retry. + wrong <- getLoadedDLLs()[["rJava"]][["path"]] + failed <- try(get(".jloadJVM", ns)(wrong), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") +} else if (scenario == "unload") { + get(".jloadJVM", ns)(lib(home_a)) + stopifnot(state()$state == "none", !state()$initialized) + stopifnot(get(".junloadJVM", ns)(), state()$state == "not-loaded") + # Calls after unloading must not use stale function pointers. + stopifnot(state()$state == "not-loaded") +} else stopifnot(scenario == "select") +Sys.setenv(JAVA_HOME = home_b) +rJava::.jinit() +stopifnot(state()$initialized, state()$state == "created") +actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") +stopifnot(identical(normalizePath(actual), normalizePath(home_b))) +# Exercise JNI signatures, references, arrays and exceptions, beyond initialization. +obj <- rJava::.jnew("java/lang/String", "deferred") +stopifnot(rJava::.jcall(obj, "I", "length") == 8L, + identical(rJava::.jevalArray(rJava::.jarray(1:3)), 1:3)) +failed <- try(get(".junloadJVM", ns)(), silent = TRUE) +stopifnot(inherits(failed, "try-error"), state()$state == "created", + rJava::.jcall(obj, "I", "length") == 8L) +cat("PASS", scenario, rJava::.jcall("java/lang/System", "S", "getProperty", "java.version"), "\n") From 3a3ea3e5d1a046d9254160cd05b2873814448ddf Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 18:57:37 +0200 Subject: [PATCH 06/63] test: verify retry with a library without JNI exports --- .github/workflows/dynamic.yaml | 5 +++-- R/windows/FirstLib.R | 3 +-- tests-integration/deferred.R | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 7ab5ad5..ed05851 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -42,7 +42,8 @@ jobs: mkdir -p "$RUNNER_TEMP/rjava-lib" archive="$(ls ../rJava_*.tar.gz)" R CMD INSTALL --library="$RUNNER_TEMP/rjava-lib" "$archive" - echo "R_LIBS_USER=$RUNNER_TEMP/rjava-lib" >> "$GITHUB_ENV" + cp "$archive" "$RUNNER_TEMP/" + echo "R_LIBS=$RUNNER_TEMP/rjava-lib" >> "$GITHUB_ENV" - name: Check deferred loading in fresh processes shell: bash env: @@ -72,5 +73,5 @@ jobs: with: name: dynamic-${{ matrix.os }} path: | - ../rJava_*.tar.gz + ${{ runner.temp }}/rJava_*.tar.gz ${{ runner.temp }}/linkage.txt diff --git a/R/windows/FirstLib.R b/R/windows/FirstLib.R index b59f08f..77b1e27 100755 --- a/R/windows/FirstLib.R +++ b/R/windows/FirstLib.R @@ -10,8 +10,7 @@ } if (nzchar(javahome) && !dir.exists(javahome)) { - warning("Java home setting is INVALID, it will be ignored.") - javahome <- "" + stop("Java home setting is invalid: ", javahome) } # If not found, try Registry diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index ce4b1e7..1a0b55b 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -21,7 +21,8 @@ if (scenario == "retry") { failed <- try(rJava::.jinit(), silent = TRUE) stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") # A native library with no JNI exports must fail cleanly and permit retry. - wrong <- getLoadedDLLs()[["rJava"]][["path"]] + loadNamespace("stats") + wrong <- getLoadedDLLs()[["stats"]][["path"]] failed <- try(get(".jloadJVM", ns)(wrong), silent = TRUE) stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") } else if (scenario == "unload") { @@ -31,11 +32,12 @@ if (scenario == "retry") { # Calls after unloading must not use stale function pointers. stopifnot(state()$state == "not-loaded") } else stopifnot(scenario == "select") -Sys.setenv(JAVA_HOME = home_b) +selected <- if (scenario == "unload") home_a else home_b +Sys.setenv(JAVA_HOME = selected) rJava::.jinit() stopifnot(state()$initialized, state()$state == "created") actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") -stopifnot(identical(normalizePath(actual), normalizePath(home_b))) +stopifnot(identical(normalizePath(actual), normalizePath(selected))) # Exercise JNI signatures, references, arrays and exceptions, beyond initialization. obj <- rJava::.jnew("java/lang/String", "deferred") stopifnot(rJava::.jcall(obj, "I", "length") == 8L, From 9b6f625fa4bb5b6df68604526638d30a828d6619 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 18:58:12 +0200 Subject: [PATCH 07/63] ci: test dynamic rJava with installed downstream rJavaEnv consumers --- .github/workflows/dynamic.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index ed05851..3ea88a2 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -68,6 +68,24 @@ jobs: fi cat "$RUNNER_TEMP/linkage.txt" if grep -Ei '(libjvm|DLL Name: jvm.dll)' "$RUNNER_TEMP/linkage.txt"; then exit 1; fi + - uses: actions/checkout@v4 + with: + repository: e-kotov/rJavaEnv + ref: feature/downstream-java-integration + path: downstream-source + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + working-directory: downstream-source + dependencies: '"hard"' + extra-packages: any::callr + install-quarto: false + - name: Test installed rJavaEnv and downstream packages with dynamic rJava + working-directory: downstream-source + shell: Rscript {0} + env: + RJE_JAVA_HOME_A: ${{ steps.java17.outputs.path }} + RJE_JAVA_HOME_B: ${{ steps.java21.outputs.path }} + run: source("tools/test-downstream.R") - uses: actions/upload-artifact@v4 if: always() with: From dec19f37e0a2ceb0c8db27be9822025411a18689 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 18:59:34 +0200 Subject: [PATCH 08/63] docs: define deferred loading acceptance boundary and limits --- tests-integration/README.md | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests-integration/README.md diff --git a/tests-integration/README.md b/tests-integration/README.md new file mode 100644 index 0000000..f3f98b4 --- /dev/null +++ b/tests-integration/README.md @@ -0,0 +1,47 @@ +# Deferred JVM loading experiment + +This branch ports Simon Urbanek's `feature/dynamic` work onto upstream master +(`534ff33`) and includes Egor Kotov's earlier Windows loader work. It is an +experimental fork, not a released rJava API contract. + +The acceptance boundary is **before the first JVM library load**. Loading the +rJava namespace, including through another package's Imports, must not select, +load, or initialize Java. `.jinit()` discovers the selected JVM at call time. +An invalid explicit Java home fails at initialization, allowing correction and +retry while no library has loaded. On Windows, registry discovery is deferred +until initialization too. + +`dynamic.yaml` builds an actual source distribution with `sh mkdist`, installs +it in an isolated R library, and tests it on native Linux, macOS, and Windows +runners with Temurin 17 and 21. Every scenario uses a new R process. The native +library must have no direct JVM linkage. Tests cover selection after namespace +load, recovery after missing-library/missing-JNI-export errors, JNI calls, +and rejection of unloading a running JVM. They also install the rJavaEnv +feature branch and its two minimal downstream packages to test real Imports, +setup, startup, compatible guards, and child-process isolation. + +To run locally, build and install the source tarball in a temporary library, +then set `R_LIBS` to it and `RJE_JAVA_HOME_A`/`RJE_JAVA_HOME_B` to two real JDK +homes. Run `Rscript --vanilla tests-integration/deferred.R select`, `retry`, and +`unload` as separate commands. The last case reloads the **same** unstarted +runtime, not a different JDK. + +## Limits + +Namespace loading is different from JVM library loading, which is different +from JVM startup. Calling `.jloadJVM()` fixes the runtime selection boundary +even before `.jinit()`. A successful OS-level unload does not establish that +all runtime symbols and dependencies were removed. In a local macOS experiment, +loading Java 17, unloading before startup, then starting Java 27 failed with a +Java 17/27 symbol mismatch. Do not use `.junloadJVM()` to switch JDKs. + +A started JVM cannot be unloaded by this R API. Restarting a worker process +remains the supported way to run another JVM. A package that calls `.jinit()` +in `.onLoad()` still selects Java during package loading; this branch cannot +make that package's initialization lazy. + +The hosted matrix covers current 64-bit R and two Temurin versions. It does not +establish compatibility for 32-bit Windows, every vendor, JDK 8, Java embedding +R through JRI, or every downstream package. The source build includes JRI, but +embedding/attach behavior requires separate regression testing before upstream +release. From 42af82ac1a22dc85c636a9d8c282800ca5044540 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 19:00:00 +0200 Subject: [PATCH 09/63] fix: make deferred Windows discovery helper visible to package checks --- R/java-runtime.R | 41 +++++++++++++++++++++++++++++++++++++++++ R/windows/FirstLib.R | 43 ------------------------------------------- 2 files changed, 41 insertions(+), 43 deletions(-) create mode 100644 R/java-runtime.R diff --git a/R/java-runtime.R b/R/java-runtime.R new file mode 100644 index 0000000..2fb6b89 --- /dev/null +++ b/R/java-runtime.R @@ -0,0 +1,41 @@ +# Extract detection logic to a helper function +.find.java.runtime <- function() { + # Check options and env vars first + javahome <- if (!is.null(getOption("java.home"))) { + getOption("java.home") + } else { + Sys.getenv("JAVA_HOME") + } + + if (nzchar(javahome) && !dir.exists(javahome)) { + stop("Java home setting is invalid: ", javahome) + } + + # If not found, try Registry + if (!nzchar(javahome)) { + find.java <- function() { + for (root in c("HLM", "HCU")) { + for (key in c( + "Software\\JavaSoft\\JRE", + "Software\\JavaSoft\\JDK", + "Software\\JavaSoft\\Java Runtime Environment", + "Software\\JavaSoft\\Java Development Kit" + )) { + hive <- try( + utils::readRegistry(key, root, 2), + silent = TRUE + ) + if (!inherits(hive, "try-error")) return(hive) + } + } + hive + } + hive <- find.java() + if (!inherits(hive, "try-error") && length(hive$CurrentVersion)) { + this <- hive[[hive$CurrentVersion]] + javahome <- this$JavaHome + } + } + return(javahome) +} + diff --git a/R/windows/FirstLib.R b/R/windows/FirstLib.R index 77b1e27..9a08866 100755 --- a/R/windows/FirstLib.R +++ b/R/windows/FirstLib.R @@ -1,46 +1,3 @@ -.msg <- message - -# Extract detection logic to a helper function -.find.java.runtime <- function() { - # Check options and env vars first - javahome <- if (!is.null(getOption("java.home"))) { - getOption("java.home") - } else { - Sys.getenv("JAVA_HOME") - } - - if (nzchar(javahome) && !dir.exists(javahome)) { - stop("Java home setting is invalid: ", javahome) - } - - # If not found, try Registry - if (!nzchar(javahome)) { - find.java <- function() { - for (root in c("HLM", "HCU")) { - for (key in c( - "Software\\JavaSoft\\JRE", - "Software\\JavaSoft\\JDK", - "Software\\JavaSoft\\Java Runtime Environment", - "Software\\JavaSoft\\Java Development Kit" - )) { - hive <- try( - utils::readRegistry(key, root, 2), - silent = TRUE - ) - if (!inherits(hive, "try-error")) return(hive) - } - } - hive - } - hive <- find.java() - if (!inherits(hive, "try-error") && length(hive$CurrentVersion)) { - this <- hive[[hive$CurrentVersion]] - javahome <- this$JavaHome - } - } - return(javahome) -} - .onLoad <- function(libname, pkgname) { library.dynam("rJava", pkgname, libname) .jfirst(libname, pkgname) From 5fc891f5c3a5bc5f11280699840f2c92889e4e44 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 19:02:57 +0200 Subject: [PATCH 10/63] fix: retain JVM library selection until the R process exits --- src/Rglue.c | 10 +++++----- tests-integration/README.md | 8 ++++---- tests-integration/deferred.R | 7 ++++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Rglue.c b/src/Rglue.c index db624ce..38f6add 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1156,9 +1156,9 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { } REPC SEXP RunloadJVM(void) { - int res; - if (rJava_JVM_state != JVM_STATE_NONE || existingJVMs() > 0) - Rf_error("Cannot unload a JVM that has been started; use a new R process."); - res = djni_unload(); - return Rf_ScalarLogical((res < -1) ? 0 : 1); + /* dlclose/FreeLibrary success does not prove that the JVM and all its + dependencies were unmapped. In particular dyld retains TLS images. */ + if (djni_loaded()) + Rf_error("Cannot safely unload a selected JVM library; use a new R process."); + return Rf_ScalarLogical(1); } diff --git a/tests-integration/README.md b/tests-integration/README.md index f3f98b4..d485301 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -16,15 +16,15 @@ it in an isolated R library, and tests it on native Linux, macOS, and Windows runners with Temurin 17 and 21. Every scenario uses a new R process. The native library must have no direct JVM linkage. Tests cover selection after namespace load, recovery after missing-library/missing-JNI-export errors, JNI calls, -and rejection of unloading a running JVM. They also install the rJavaEnv +and rejection of unloading any selected JVM library. They also install the rJavaEnv feature branch and its two minimal downstream packages to test real Imports, setup, startup, compatible guards, and child-process isolation. To run locally, build and install the source tarball in a temporary library, then set `R_LIBS` to it and `RJE_JAVA_HOME_A`/`RJE_JAVA_HOME_B` to two real JDK homes. Run `Rscript --vanilla tests-integration/deferred.R select`, `retry`, and -`unload` as separate commands. The last case reloads the **same** unstarted -runtime, not a different JDK. +`unload` as separate commands. The last case checks that unloading and changing +a selected, unstarted runtime both fail cleanly, then starts that same runtime. ## Limits @@ -35,7 +35,7 @@ all runtime symbols and dependencies were removed. In a local macOS experiment, loading Java 17, unloading before startup, then starting Java 27 failed with a Java 17/27 symbol mismatch. Do not use `.junloadJVM()` to switch JDKs. -A started JVM cannot be unloaded by this R API. Restarting a worker process +A selected JVM library cannot be unloaded by this R API, even before startup. Restarting a worker process remains the supported way to run another JVM. A package that calls `.jinit()` in `.onLoad()` still selects Java during package loading; this branch cannot make that package's initialization lazy. diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 1a0b55b..a8e34d7 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -28,9 +28,10 @@ if (scenario == "retry") { } else if (scenario == "unload") { get(".jloadJVM", ns)(lib(home_a)) stopifnot(state()$state == "none", !state()$initialized) - stopifnot(get(".junloadJVM", ns)(), state()$state == "not-loaded") - # Calls after unloading must not use stale function pointers. - stopifnot(state()$state == "not-loaded") + failed <- try(get(".junloadJVM", ns)(), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$state == "none") + failed <- try(get(".jloadJVM", ns)(lib(home_b)), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$state == "none") } else stopifnot(scenario == "select") selected <- if (scenario == "unload") home_a else home_b Sys.setenv(JAVA_HOME = selected) From 880d1575e5078c66aeb6f5725e6fbf89e982121f Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 19:03:34 +0200 Subject: [PATCH 11/63] fix: reject another JVM already mapped outside the deferred loader --- .github/workflows/dynamic.yaml | 4 +-- src/Rglue.c | 1 + src/djni.c | 55 ++++++++++++++++++++++++++++++++-- tests-integration/deferred.R | 11 +++++-- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 3ea88a2..3128353 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -51,7 +51,7 @@ jobs: RJE_JAVA_HOME_B: ${{ steps.java21.outputs.path }} run: | set -euo pipefail - for scenario in select retry unload; do + for scenario in select retry unload preloaded; do Rscript --vanilla tests-integration/deferred.R "$scenario" done - name: Inspect native JVM linkage @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@v4 with: repository: e-kotov/rJavaEnv - ref: feature/downstream-java-integration + ref: 9dd898e path: downstream-source - uses: r-lib/actions/setup-r-dependencies@v2 with: diff --git a/src/Rglue.c b/src/Rglue.c index 38f6add..c340ae5 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1151,6 +1151,7 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { case -1: Rf_error("JVM run-time is already loaded"); case -2: Rf_error("Cannot load JVM run-time (%s)", djni_last_error() ? djni_last_error() : "unsuccessful"); case -3: Rf_error("JVM run-time does not have required entry points"); + case -4: Rf_error("%s", djni_last_error()); } return Rf_ScalarLogical(1); } diff --git a/src/djni.c b/src/djni.c index e88364d..f670005 100644 --- a/src/djni.c +++ b/src/djni.c @@ -1,11 +1,21 @@ /* Dynamic JNI loader: no JVM dependency is resolved at namespace load. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif #ifdef _WIN32 #define _WIN32_WINNT 0x0602 #include #else #include +#ifdef __APPLE__ +#include +#elif defined(__linux__) +#include +#endif #endif #include +#include +#include #include "djni.h" static JNI_GetDefaultJavaVMInitArgs_fnptr JNI_GetDefaultJavaVMInitArgs_fn; @@ -41,17 +51,49 @@ static int load_sym(const char *sym, void **ptr) { if (!*ptr) { save_error(); return -1; } return 0; } +#ifndef _WIN32 +static int different_jvm(const char *loaded, const char *requested) { + const char *base = strrchr(loaded, '/'); + char *a, *b; + int different; + base = base ? base + 1 : loaded; + if (strcmp(base, "libjvm.dylib") && strcmp(base, "libjvm.so")) return 0; + a = realpath(loaded, NULL); + b = realpath(requested, NULL); + different = !a || !b || strcmp(a, b); + if (different) + snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", loaded); + free(a); free(b); + return different; +} +#ifdef __linux__ +static int check_image(struct dl_phdr_info *info, size_t size, void *path) { + return different_jvm(info->dlpi_name, (const char *) path); +} +#endif +#endif + int djni_load(const char *path) { last_error[0] = 0; if (jni_dl) return -1; #ifdef _WIN32 - wchar_t wide_path[32768], bin_path[32768], *slash; + wchar_t wide_path[32768], bin_path[32768], full_path[32768], mapped_path[32768], *slash; + HMODULE mapped; DLL_DIRECTORY_COOKIE cookie = 0; if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide_path, 32768)) { save_error(); return -2; } + if (!GetFullPathNameW(wide_path, 32768, full_path, NULL)) { + save_error(); return -2; + } + for (slash = full_path; *slash; slash++) if (*slash == L'/') *slash = L'\\'; + mapped = GetModuleHandleW(L"jvm.dll"); + if (mapped && (!GetModuleFileNameW(mapped, mapped_path, 32768) || lstrcmpiW(full_path, mapped_path))) { + snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped. Use a new R process."); + return -4; + } /* JDK 8 needs its bin directory as well as bin/server for dependent DLLs. */ - wcscpy(bin_path, wide_path); + wcscpy(bin_path, full_path); for (slash = bin_path; *slash; slash++) if (*slash == L'/') *slash = L'\\'; slash = wcsrchr(bin_path, L'\\'); if (slash) { @@ -59,10 +101,17 @@ int djni_load(const char *path) { slash = wcsrchr(bin_path, L'\\'); if (slash) { *slash = 0; cookie = AddDllDirectory(bin_path); } } - jni_dl = LoadLibraryExW(wide_path, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + jni_dl = LoadLibraryExW(full_path, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); if (!jni_dl) save_error(); if (cookie) RemoveDllDirectory(cookie); #else +#ifdef __APPLE__ + uint32_t i; + for (i = 0; i < _dyld_image_count(); i++) + if (different_jvm(_dyld_get_image_name(i), path)) return -4; +#elif defined(__linux__) + if (dl_iterate_phdr(check_image, (void *) path)) return -4; +#endif jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW); if (!jni_dl) save_error(); #endif diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index a8e34d7..648d702 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -17,7 +17,14 @@ lib <- function(home) { stopifnot(length(paths) >= 1L) normalizePath(paths[1], winslash = "/") } -if (scenario == "retry") { +if (scenario == "preloaded") { + # Detect a JVM library loaded outside this loader, without starting a VM. + dyn.load(lib(home_a)) + Sys.setenv(JAVA_HOME = home_b) + failed <- try(rJava::.jinit(), silent = TRUE) + stopifnot(inherits(failed, "try-error"), grepl("already mapped", failed)) + stopifnot(!state()$initialized) +} else if (scenario == "retry") { failed <- try(rJava::.jinit(), silent = TRUE) stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") # A native library with no JNI exports must fail cleanly and permit retry. @@ -33,7 +40,7 @@ if (scenario == "retry") { failed <- try(get(".jloadJVM", ns)(lib(home_b)), silent = TRUE) stopifnot(inherits(failed, "try-error"), state()$state == "none") } else stopifnot(scenario == "select") -selected <- if (scenario == "unload") home_a else home_b +selected <- if (scenario %in% c("unload", "preloaded")) home_a else home_b Sys.setenv(JAVA_HOME = selected) rJava::.jinit() stopifnot(state()$initialized, state()$state == "created") From ef4fb30b918a30da92741ae84a61bacc203a2a35 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 19:06:20 +0200 Subject: [PATCH 12/63] ci: use full commit SHA for pinned rJavaEnv checkout --- .github/workflows/dynamic.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 3128353..377ff8b 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@v4 with: repository: e-kotov/rJavaEnv - ref: 9dd898e + ref: 9dd898e6a6d910ecae246c0771b7d5eb270031c4 path: downstream-source - uses: r-lib/actions/setup-r-dependencies@v2 with: From 9c9baf431c3cb09ae351284059f1475a7e7aaaa6 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 19:36:54 +0200 Subject: [PATCH 13/63] build: regenerate configure with upstream's Autoconf 2.72 The previous regeneration used Autoconf 2.73 and rewrote ~1300 lines. Regenerating with Autoconf 2.72 (the generator of upstream's configure, which it reproduces byte for byte) limits the difference from upstream to the dynload default and the skipped link-time JNI checks. --- configure | 1332 +++++++++++++++++++++++------------------------------ 1 file changed, 579 insertions(+), 753 deletions(-) diff --git a/configure b/configure index ef1f0a6..0ab3e79 100755 --- a/configure +++ b/configure @@ -1,11 +1,11 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.73 for rJava 0.8. +# Generated by GNU Autoconf 2.72 for rJava 0.8. # # Report bugs to . # # -# Copyright (C) 1992-1996, 1998-2017, 2020-2026 Free Software Foundation, +# Copyright (C) 1992-1996, 1998-2017, 2020-2023 Free Software Foundation, # Inc. # # @@ -22,7 +22,7 @@ then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which - # contradicts POSIX and common usage. Disable this. + # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else case e in #( @@ -109,7 +109,7 @@ if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then - printf '%s\n' "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi @@ -131,13 +131,10 @@ case $- in # (((( *x* ) as_opts=-x ;; * ) as_opts= ;; esac -case $# in # (( - 0) exec $CONFIG_SHELL $as_opts "$as_myself" ;; - *) exec $CONFIG_SHELL $as_opts "$as_myself" "$@" ;; -esac +exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed 'exec'. -printf '%s\n' "$0: could not re-execute with $CONFIG_SHELL" >&2 +printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi # We don't want this to propagate to other subprocesses. @@ -148,7 +145,7 @@ then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which - # contradicts POSIX and common usage. Disable this. + # is contrary to our usage. Disable this feature. alias -g '\${1+\"\$@\"}'='\"\$@\"' setopt NO_GLOB_SUBST else case e in #( @@ -257,25 +254,22 @@ case $- in # (((( *x* ) as_opts=-x ;; * ) as_opts= ;; esac -case $# in # (( - 0) exec $CONFIG_SHELL $as_opts "$as_myself" ;; - *) exec $CONFIG_SHELL $as_opts "$as_myself" "$@" ;; -esac +exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed 'exec'. -printf '%s\n' "$0: could not re-execute with $CONFIG_SHELL" >&2 +printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi if test x$as_have_required = xno then : - printf '%s\n' "$0: This script requires a shell more modern than all" - printf '%s\n' "$0: the shells that I found on your system." + printf "%s\n" "$0: This script requires a shell more modern than all" + printf "%s\n" "$0: the shells that I found on your system." if test ${ZSH_VERSION+y} ; then - printf '%s\n' "$0: In particular, zsh $ZSH_VERSION has bugs and should" - printf '%s\n' "$0: be upgraded to zsh 4.3.4 or later." + printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should" + printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later." else - printf '%s\n' "$0: Please tell bug-autoconf@gnu.org and + printf "%s\n" "$0: Please tell bug-autoconf@gnu.org and $0: Simon.Urbanek@r-project.org about your system, $0: including any error possibly output before this $0: message. Then install a modern shell, or manually run @@ -336,7 +330,7 @@ as_fn_mkdir_p () as_dirs= while :; do case $as_dir in #( - *\'*) as_qdir=`printf '%s\n' "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" @@ -345,7 +339,7 @@ $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -printf '%s\n' X"$as_dir" | +printf "%s\n" X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -428,9 +422,9 @@ as_fn_error () as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - printf '%s\n' "$as_me: error: $2" >&2 + printf "%s\n" "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -457,7 +451,7 @@ as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || -printf '%s\n' X/"$0" | +printf "%s\n" X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q @@ -503,7 +497,7 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits s/-\n.*// ' >$as_me.lineno && chmod +x "$as_me.lineno" || - { printf '%s\n' "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } + { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } # If we had to re-execute with $CONFIG_SHELL, we're ensured to have # already done that, so ensure we don't try to do so again and fall @@ -517,6 +511,29 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits exit } + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + +# For backward compatibility with old third-party macros, we provide +# the shell variables $as_echo and $as_echo_n. New code should use +# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. +as_echo='printf %s\n' +as_echo_n='printf %s' + rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -575,7 +592,6 @@ ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` # Initializations. # ac_default_prefix=/usr/local -ac_clean_CONFIG_STATUS= ac_clean_files= ac_config_libobj_dir=. LIBOBJS= @@ -647,13 +663,13 @@ LDFLAGS CFLAGS CC R_HOME -ECHO_T -ECHO_N -ECHO_C target_alias host_alias build_alias LIBS +ECHO_T +ECHO_N +ECHO_C DEFS mandir localedir @@ -817,7 +833,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" @@ -843,7 +859,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" @@ -1056,7 +1072,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" @@ -1072,7 +1088,7 @@ do expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: '$ac_useropt'" ac_useropt_orig=$ac_useropt - ac_useropt=`printf '%s\n' "$ac_useropt" | sed 's/[-+.]/_/g'` + ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" @@ -1116,9 +1132,9 @@ Try '$0 --help' for more information" *) # FIXME: should be removed in autoconf 3.0. - printf '%s\n' "$as_me: WARNING: you should use --build, --host, --target" >&2 + printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2 expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && - printf '%s\n' "$as_me: WARNING: invalid host type: $ac_option" >&2 + printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2 : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" ;; @@ -1126,7 +1142,7 @@ Try '$0 --help' for more information" done if test -n "$ac_prev"; then - ac_option=--`printf '%s\n' $ac_prev | sed 's/_/-/g'` + ac_option=--`echo $ac_prev | sed 's/_/-/g'` as_fn_error $? "missing argument to $ac_option" fi @@ -1134,7 +1150,7 @@ if test -n "$ac_unrecognized_opts"; then case $enable_option_checking in no) ;; fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; - *) printf '%s\n' "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; + *) printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; esac fi @@ -1198,7 +1214,7 @@ $as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_myself" : 'X\(//\)[^/]' \| \ X"$as_myself" : 'X\(//\)$' \| \ X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || -printf '%s\n' X"$as_myself" | +printf "%s\n" X"$as_myself" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -1378,9 +1394,9 @@ if test "$ac_init_help" = "recursive"; then case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`printf '%s\n' "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`printf '%s\n' "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -1417,7 +1433,7 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix echo && $SHELL "$ac_srcdir/configure" --help=recursive else - printf '%s\n' "$as_me: WARNING: no configuration information is in $ac_dir" >&2 + printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2 fi || ac_status=$? cd "$ac_pwd" || { ac_status=$?; break; } done @@ -1427,9 +1443,9 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF rJava configure 0.8 -generated by GNU Autoconf 2.73 +generated by GNU Autoconf 2.72 -Copyright (C) 2026 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. _ACEOF @@ -1453,7 +1469,7 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compile") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -1461,7 +1477,7 @@ printf '%s\n' "$ac_try_echo"; } >&5 cat conftest.er1 >&5 mv -f conftest.er1 conftest.err fi - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { test -z "$ac_c_werror_flag" || test ! -s conftest.err @@ -1469,7 +1485,7 @@ printf '%s\n' "$ac_try_echo"; } >&5 then : ac_retval=0 else case e in #( - e) printf '%s\n' "$as_me: failed program was:" >&5 + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=1 ;; @@ -1487,7 +1503,7 @@ fi ac_fn_c_check_header_compile () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5 printf %s "checking for $2... " >&6; } if eval test \${$3+y} then : @@ -1509,8 +1525,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi eval ac_res=\$$3 - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -printf '%s\n' "$ac_res" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_c_check_header_compile @@ -1523,7 +1539,7 @@ ac_fn_check_decl () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack as_decl_name=`echo $2|sed 's/ *(.*//'` - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether $as_decl_name is declared" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $as_decl_name is declared" >&5 printf %s "checking whether $as_decl_name is declared... " >&6; } if eval test \${$3+y} then : @@ -1563,8 +1579,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext esac fi eval ac_res=\$$3 - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 -printf '%s\n' "$ac_res" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_check_decl @@ -1582,7 +1598,7 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -1590,7 +1606,7 @@ printf '%s\n' "$ac_try_echo"; } >&5 cat conftest.er1 >&5 mv -f conftest.er1 conftest.err fi - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { test -z "$ac_c_werror_flag" || test ! -s conftest.err @@ -1601,7 +1617,7 @@ printf '%s\n' "$ac_try_echo"; } >&5 then : ac_retval=0 else case e in #( - e) printf '%s\n' "$as_me: failed program was:" >&5 + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=1 ;; @@ -1630,26 +1646,26 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { ac_try='./conftest$ac_exeext' { { case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; } then : ac_retval=0 else case e in #( - e) printf '%s\n' "$as_me: program exited with status $ac_status" >&5 - printf '%s\n' "$as_me: failed program was:" >&5 + e) printf "%s\n" "$as_me: program exited with status $ac_status" >&5 + printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=$ac_status ;; @@ -1665,7 +1681,7 @@ for ac_arg do case $ac_arg in *\'*) - ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append ac_configure_args_raw " '$ac_arg'" done @@ -1677,7 +1693,7 @@ case $ac_configure_args_raw in ac_unsafe_z='|&;<>()$`\\"*?[ '' ' # This string ends in space, tab. ac_unsafe_a="$ac_unsafe_z#~" ac_safe_unquote="s/ '\\([^$ac_unsafe_a][^$ac_unsafe_z]*\\)'/ \\1/g" - ac_configure_args_raw=` printf '%s\n' "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;; + ac_configure_args_raw=` printf "%s\n" "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;; esac cat >config.log <<_ACEOF @@ -1685,7 +1701,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. It was created by rJava $as_me 0.8, which was -generated by GNU Autoconf 2.73. Invocation command line was +generated by GNU Autoconf 2.72. Invocation command line was $ $0$ac_configure_args_raw @@ -1725,7 +1741,7 @@ do */) ;; *) as_dir=$as_dir/ ;; esac - printf '%s\n' "PATH: $as_dir" + printf "%s\n" "PATH: $as_dir" done IFS=$as_save_IFS @@ -1760,7 +1776,7 @@ do | -silent | --silent | --silen | --sile | --sil) continue ;; *\'*) - ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac case $ac_pass in 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; @@ -1789,22 +1805,31 @@ done { ac_configure_args0=; unset ac_configure_args0;} { ac_configure_args1=; unset ac_configure_args1;} -# Dump the cache to stdout. It can be in a pipe (this is a requirement). -ac_cache_dump () -{ - # The following way of writing the cache mishandles newlines in values, -# but we know of no workaround that is simple, portable, and efficient. -# So, we kill variables containing newlines. -# Ultrix sh set writes to stderr and can't be redirected directly, -# and sets the high bit in the cache file unless we assign to the vars. +# When interrupted or exit'd, cleanup temporary files, and complete +# config.log. We remove comments because anyway the quotes in there +# would cause problems or look ugly. +# WARNING: Use '\'' to represent an apostrophe within the trap. +# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. +trap 'exit_status=$? + # Sanitize IFS. + IFS=" "" $as_nl" + # Save into config.log some information that might help in debugging. + { + echo + + printf "%s\n" "## ---------------- ## +## Cache variables. ## +## ---------------- ##" + echo + # The following way of writing the cache mishandles newlines in values, ( - for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do + for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do eval ac_val=\$$ac_var case $ac_val in #( *${as_nl}*) case $ac_var in #( - *_cv_*) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -printf '%s\n' "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( @@ -1813,95 +1838,67 @@ printf '%s\n' "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} esac ;; esac done - (set) 2>&1 | - case $as_nl`(ac_space=' '; set) 2>&1` in #( + case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( *${as_nl}ac_space=\ *) - # 'set' does not quote correctly, so add quotes: double-quote - # substitution turns \\\\ into \\, and sed turns \\ into \. sed -n \ - "s/'/'\\\\''/g; - s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" + "s/'\''/'\''\\\\'\'''\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" ;; #( *) - # 'set' quotes correctly as required by POSIX, so do not add quotes. sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | sort ) -} - -# Print debugging info to stdout. -ac_dump_debugging_info () -{ - echo - - printf '%s\n' "## ---------------- ## -## Cache variables. ## -## ---------------- ##" - echo - ac_cache_dump - echo + echo - printf '%s\n' "## ----------------- ## + printf "%s\n" "## ----------------- ## ## Output variables. ## ## ----------------- ##" - echo - for ac_var in $ac_subst_vars - do - eval ac_val=\$$ac_var - case $ac_val in - *\'*) ac_val=`printf '%s\n' "$ac_val" | sed "s/'/'\\\\\\\\''/g"`;; - esac - printf '%s\n' "$ac_var='$ac_val'" - done | sort - echo - - if test -n "$ac_subst_files"; then - printf '%s\n' "## ------------------- ## -## File substitutions. ## -## ------------------- ##" echo - for ac_var in $ac_subst_files + for ac_var in $ac_subst_vars do eval ac_val=\$$ac_var case $ac_val in - *\'*) ac_val=`printf '%s\n' "$ac_val" | sed "s/'/'\\\\\\\\''/g"`;; + *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac - printf '%s\n' "$ac_var='$ac_val'" + printf "%s\n" "$ac_var='\''$ac_val'\''" done | sort echo - fi - if test -s confdefs.h; then - printf '%s\n' "## ----------- ## + if test -n "$ac_subst_files"; then + printf "%s\n" "## ------------------- ## +## File substitutions. ## +## ------------------- ##" + echo + for ac_var in $ac_subst_files + do + eval ac_val=\$$ac_var + case $ac_val in + *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; + esac + printf "%s\n" "$ac_var='\''$ac_val'\''" + done | sort + echo + fi + + if test -s confdefs.h; then + printf "%s\n" "## ----------- ## ## confdefs.h. ## ## ----------- ##" - echo - cat confdefs.h - echo - fi - test "$ac_signal" != 0 && - printf '%s\n' "$as_me: caught signal $ac_signal" - printf '%s\n' "$as_me: exit $exit_status" -} - -# When interrupted or exit'd, cleanup temporary files, and complete -# config.log. -ac_exit_trap () -{ - exit_status= - # Sanitize IFS. - IFS=" "" $as_nl" - # Save into config.log some information that might help in debugging. - ac_dump_debugging_info >&5 - eval "rm -f $ac_clean_CONFIG_STATUS core *.core core.conftest.*" && + echo + cat confdefs.h + echo + fi + test "$ac_signal" != 0 && + printf "%s\n" "$as_me: caught signal $ac_signal" + printf "%s\n" "$as_me: exit $exit_status" + } >&5 + rm -f core *.core core.conftest.* && rm -f -r conftest* confdefs* conf$$* $ac_clean_files && exit $exit_status -} - -trap 'ac_exit_trap $?' 0 +' 0 for ac_signal in 1 2 13 15; do trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal done @@ -1910,21 +1907,21 @@ ac_signal=0 # confdefs.h avoids OS command line length limits that DEFS can exceed. rm -f -r conftest* confdefs.h -printf '%s\n' "/* confdefs.h */" > confdefs.h +printf "%s\n" "/* confdefs.h */" > confdefs.h # Predefined preprocessor variables. -printf '%s\n' "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h -printf '%s\n' "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h -printf '%s\n' "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h -printf '%s\n' "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h -printf '%s\n' "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h -printf '%s\n' "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h # Let the site file select an alternate cache file if it wants to. @@ -1946,12 +1943,12 @@ do ac_site_file=./$ac_site_file ;; esac if test -f "$ac_site_file" && test -r "$ac_site_file"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 -printf '%s\n' "$as_me: loading site script $ac_site_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 +printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ - || { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} + || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "failed to load site script $ac_site_file See 'config.log' for more details" "$LINENO" 5; } fi @@ -1961,120 +1958,27 @@ if test -r "$cache_file"; then # Some versions of bash will fail to source /dev/null (special files # actually), so we avoid doing that. DJGPP emulates it as a regular file. if test /dev/null != "$cache_file" && test -f "$cache_file"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 -printf '%s\n' "$as_me: loading cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 +printf "%s\n" "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 -printf '%s\n' "$as_me: creating cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 +printf "%s\n" "$as_me: creating cache $cache_file" >&6;} >$cache_file fi -# Test code for whether the C compiler supports C23 (global declarations) -ac_c_conftest_c23_globals=' -/* Does the compiler advertise conformance to C17 or earlier? - Although GCC 14 does not do that, even with -std=gnu23, - it is close enough, and defines __STDC_VERSION == 202000L. */ -#if !defined __STDC_VERSION__ || __STDC_VERSION__ <= 201710L -# error "Compiler advertises conformance to C17 or earlier" -#endif - -// Check alignas. -char alignas (double) c23_aligned_as_double; -char alignas (0) c23_no_special_alignment; -extern char c23_aligned_as_int; -char alignas (0) alignas (int) c23_aligned_as_int; - -// Check alignof. -enum -{ - c23_int_alignment = alignof (int), - c23_int_array_alignment = alignof (int[100]), - c23_char_alignment = alignof (char) -}; -static_assert (0 < -alignof (int), "alignof is signed"); - -int function_with_unnamed_parameter (int) { return 0; } - -void c23_noreturn (); - -/* Test parsing of string and char UTF-8 literals (including hex escapes). - The parens pacify GCC 15. */ -bool use_u8 = (!sizeof u8"\xFF") == (!u8'\''x'\''); - -bool check_that_bool_works = true | false | !nullptr; -#if !true -# error "true does not work in #if" -#endif -#if false -#elifdef __STDC_VERSION__ -#else -# error "#elifdef does not work" -#endif - -#ifndef __has_c_attribute -# error "__has_c_attribute not defined" -#endif - -#ifndef __has_include -# error "__has_include not defined" -#endif - -#define LPAREN() ( -#define FORTY_TWO(x) 42 -#define VA_OPT_TEST(r, x, ...) __VA_OPT__ (FORTY_TWO r x)) -static_assert (VA_OPT_TEST (LPAREN (), 0, <:-) == 42); - -static_assert (0b101010 == 42); -static_assert (0B101010 == 42); -static_assert (0xDEAD'\''BEEF == 3'\''735'\''928'\''559); -static_assert (0.500'\''000'\''000 == 0.5); - -enum unsignedish : unsigned int { uione = 1 }; -static_assert (0 < -uione); - -#include -constexpr nullptr_t null_pointer = nullptr; - -static typeof (1 + 1L) two () { return 2; } -static long int three () { return 3; } -' - -# Test code for whether the C compiler supports C23 (body of main). -ac_c_conftest_c23_main=' - { - label_before_declaration: - int arr[10] = {}; - if (arr[0]) - goto label_before_declaration; - if (!arr[0]) - goto label_at_end_of_block; - label_at_end_of_block: - } - ok |= !null_pointer; - ok |= two != three; -' - -# Test code for whether the C compiler supports C23 (complete). -ac_c_conftest_c23_program="${ac_c_conftest_c23_globals} - -int -main (int, char **) -{ - int ok = 0; - ${ac_c_conftest_c23_main} - return ok; -} -" - # Test code for whether the C compiler supports C89 (global declarations) ac_c_conftest_c89_globals=' -/* Do not test the value of __STDC__, because some compilers define it to 0 - or do not define it, while otherwise adequately conforming. */ +/* Does the compiler advertise C89 conformance? + Do not test the value of __STDC__, because some compilers set it to 0 + while being otherwise adequately conformant. */ +#if !defined __STDC__ +# error "Compiler does not advertise C89 conformance" +#endif #include #include @@ -2154,8 +2058,7 @@ extern void free (void *); // Check varargs macros. These examples are taken from C99 6.10.3.5. // dprintf is used instead of fprintf to avoid needing to declare -// FILE and stderr, and "aND" is used instead of "and" to work around -// GCC bug 40564 which is irrelevant here. +// FILE and stderr. #define debug(...) dprintf (2, __VA_ARGS__) #define showlist(...) puts (#__VA_ARGS__) #define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__)) @@ -2166,7 +2069,7 @@ test_varargs_macros (void) int y = 5678; debug ("Flag"); debug ("X = %d\n", x); - showlist (The first, second, aND third items.); + showlist (The first, second, and third items.); report (x>y, "x is %d but y is %d", x, y); } @@ -2254,15 +2157,15 @@ ac_c_conftest_c99_main=' // Check restrict. if (test_restrict ("String literal") == 0) success = true; - const char *restrict newvar = "Another string"; + char *restrict newvar = "Another string"; // Check varargs. success &= test_varargs ("s, d'\'' f .", "string", 65, 34.234); test_varargs_macros (); // Check flexible array members. - static struct incomplete_array *volatile incomplete_array_pointer; - struct incomplete_array *ia = incomplete_array_pointer; + struct incomplete_array *ia = + malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10)); ia->datasize = 10; for (int i = 0; i < ia->datasize; ++i) ia->data[i] = i * 1.234; @@ -2278,12 +2181,13 @@ ac_c_conftest_c99_main=' ni.number = 58; - // Do not test for VLAs, as some otherwise-conforming compilers lack them. - // C code should instead use __STDC_NO_VLA__; see Autoconf manual. + int dynamic_array[ni.number]; + dynamic_array[0] = argv[0][0]; + dynamic_array[ni.number - 1] = 543; // work around unused variable warnings ok |= (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == '\''x'\'' - || ni.number != 58); + || dynamic_array[ni.number - 1] != 543); ' # Test code for whether the C compiler supports C11 (global declarations) @@ -2411,44 +2315,38 @@ for ac_var in $ac_precious_vars; do eval ac_new_val=\$ac_env_${ac_var}_value case $ac_old_set,$ac_new_set in set,) - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&5 -printf '%s\n' "$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&5 +printf "%s\n" "$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run" >&2;} ac_cache_corrupted=: ;; ,set) - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run" >&5 -printf '%s\n' "$as_me: error: '$ac_var' was not set in the previous run" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run" >&5 +printf "%s\n" "$as_me: error: '$ac_var' was not set in the previous run" >&2;} ac_cache_corrupted=: ;; ,);; *) if test "x$ac_old_val" != "x$ac_new_val"; then # differences in whitespace do not lead to failure. - ac_old_val_w= - for ac_val in x $ac_old_val; do - ac_old_val_w="$ac_old_val_w $ac_val" - done - ac_new_val_w= - for ac_val in x $ac_new_val; do - ac_new_val_w="$ac_new_val_w $ac_val" - done + ac_old_val_w=`echo x $ac_old_val` + ac_new_val_w=`echo x $ac_new_val` if test "$ac_old_val_w" != "$ac_new_val_w"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:" >&5 -printf '%s\n' "$as_me: error: '$ac_var' has changed since the previous run:" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:" >&5 +printf "%s\n" "$as_me: error: '$ac_var' has changed since the previous run:" >&2;} ac_cache_corrupted=: else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&5 -printf '%s\n' "$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&5 +printf "%s\n" "$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:" >&2;} eval $ac_var=\$ac_old_val fi - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: former value: '$ac_old_val'" >&5 -printf '%s\n' "$as_me: former value: '$ac_old_val'" >&2;} - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: current value: '$ac_new_val'" >&5 -printf '%s\n' "$as_me: current value: '$ac_new_val'" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: '$ac_old_val'" >&5 +printf "%s\n" "$as_me: former value: '$ac_old_val'" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: '$ac_new_val'" >&5 +printf "%s\n" "$as_me: current value: '$ac_new_val'" >&2;} fi;; esac # Pass precious variables to config.status. if test "$ac_new_set" = set; then case $ac_new_val in - *\'*) ac_arg=$ac_var=`printf '%s\n' "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; *) ac_arg=$ac_var=$ac_new_val ;; esac case " $ac_configure_args " in @@ -2458,10 +2356,10 @@ printf '%s\n' "$as_me: current value: '$ac_new_val'" >&2;} fi done if $ac_cache_corrupted; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 -printf '%s\n' "$as_me: error: changes in the environment can compromise the build" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 +printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;} as_fn_error $? "run '${MAKE-make} distclean' and/or 'rm $cache_file' and start over" "$LINENO" 5 fi @@ -2469,23 +2367,6 @@ fi ## Main body of script. ## ## -------------------- ## - -# Determine whether it's possible to make 'echo' print without a newline. -# These variables are no longer used directly by Autoconf, but are AC_SUBSTed -# for compatibility with existing Makefiles. -ECHO_C= ECHO_N= ECHO_T= -case `echo -n x` in #((((( --n*) - case `echo 'xy\c'` in - *c*) ECHO_T=' ';; # ECHO_T is single tab character. - xy) ECHO_C='\c';; - *) echo `echo ksh88 bug on AIX 6.1` > /dev/null - ECHO_T=' ';; - esac;; -*) - ECHO_N='-n';; -esac - ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -2632,9 +2513,6 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu - - - ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -2643,7 +2521,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args. set dummy ${ac_tool_prefix}gcc; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2664,7 +2542,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}gcc" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2676,11 +2554,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf '%s\n' "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2689,7 +2567,7 @@ if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC # Extract the first word of "gcc", so it can be a program name with args. set dummy gcc; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_ac_ct_CC+y} then : @@ -2710,7 +2588,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="gcc" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2722,11 +2600,11 @@ esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -printf '%s\n' "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi if test "x$ac_ct_CC" = x; then @@ -2734,8 +2612,8 @@ fi else case $cross_compiling:$ac_tool_warned in yes:) -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -printf '%s\n' "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -2748,7 +2626,7 @@ if test -z "$CC"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args. set dummy ${ac_tool_prefix}cc; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2769,7 +2647,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}cc" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2781,11 +2659,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf '%s\n' "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2794,7 +2672,7 @@ fi if test -z "$CC"; then # Extract the first word of "cc", so it can be a program name with args. set dummy cc; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2820,7 +2698,7 @@ do continue fi ac_cv_prog_CC="cc" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2844,11 +2722,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf '%s\n' "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2859,7 +2737,7 @@ if test -z "$CC"; then do # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. set dummy $ac_tool_prefix$ac_prog; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2880,7 +2758,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="$ac_tool_prefix$ac_prog" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2892,11 +2770,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf '%s\n' "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2909,7 +2787,7 @@ if test -z "$CC"; then do # Extract the first word of "$ac_prog", so it can be a program name with args. set dummy $ac_prog; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_ac_ct_CC+y} then : @@ -2930,7 +2808,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="$ac_prog" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -2942,11 +2820,11 @@ esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -printf '%s\n' "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -2958,8 +2836,8 @@ done else case $cross_compiling:$ac_tool_warned in yes:) -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -printf '%s\n' "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -2971,7 +2849,7 @@ if test -z "$CC"; then if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}clang", so it can be a program name with args. set dummy ${ac_tool_prefix}clang; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_CC+y} then : @@ -2992,7 +2870,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_CC="${ac_tool_prefix}clang" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -3004,11 +2882,11 @@ esac fi CC=$ac_cv_prog_CC if test -n "$CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 -printf '%s\n' "$CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +printf "%s\n" "$CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -3017,7 +2895,7 @@ if test -z "$ac_cv_prog_CC"; then ac_ct_CC=$CC # Extract the first word of "clang", so it can be a program name with args. set dummy clang; ac_word=$2 -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_prog_ac_ct_CC+y} then : @@ -3038,7 +2916,7 @@ do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_prog_ac_ct_CC="clang" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done @@ -3050,11 +2928,11 @@ esac fi ac_ct_CC=$ac_cv_prog_ac_ct_CC if test -n "$ac_ct_CC"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 -printf '%s\n' "$ac_ct_CC" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +printf "%s\n" "$ac_ct_CC" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi if test "x$ac_ct_CC" = x; then @@ -3062,8 +2940,8 @@ fi else case $cross_compiling:$ac_tool_warned in yes:) -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -printf '%s\n' "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac CC=$ac_ct_CC @@ -3075,13 +2953,13 @@ fi fi -test -z "$CC" && { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} +test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "no acceptable C compiler found in \$PATH See 'config.log' for more details" "$LINENO" 5; } # Provide some information about the compiler. -printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 +printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 set X $ac_compile ac_compiler=$2 for ac_option in --version -v -V -qversion -version; do @@ -3091,7 +2969,7 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compiler $ac_option >&5") 2>conftest.err ac_status=$? if test -s conftest.err; then @@ -3101,7 +2979,7 @@ printf '%s\n' "$ac_try_echo"; } >&5 cat conftest.er1 >&5 fi rm -f conftest.er1 conftest.err - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } done @@ -3121,9 +2999,9 @@ ac_clean_files="$ac_clean_files a.out a.out.dSYM a.exe b.out" # Try to create an executable without -o first, disregard a.out. # It will help us diagnose broken compilers, and finding out an intuition # of exeext. -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5 printf %s "checking whether the C compiler works... " >&6; } -ac_link_default=`printf '%s\n' "$ac_link" | sed 's/ -o *conftest[^ ]*//'` +ac_link_default=`printf "%s\n" "$ac_link" | sed 's/ -o *conftest[^ ]*//'` # The possible output files: ac_files="a.out conftest.exe conftest a.exe a_out.exe b.out conftest.*" @@ -3144,10 +3022,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link_default") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : # Autoconf-2.13 could set the ac_cv_exeext variable to 'no'. @@ -3188,29 +3066,29 @@ esac fi if test -z "$ac_file" then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } -printf '%s\n' "$as_me: failed program was:" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } +printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} +{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "C compiler cannot create executables See 'config.log' for more details" "$LINENO" 5; } else case e in #( - e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } ;; + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5 printf %s "checking for C compiler default output file name... " >&6; } -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 -printf '%s\n' "$ac_file" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5 +printf "%s\n" "$ac_file" >&6; } ac_exeext=$ac_cv_exeext rm -f -r a.out a.out.dSYM a.exe conftest$ac_cv_exeext b.out ac_clean_files=$ac_clean_files_save -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5 printf %s "checking for suffix of executables... " >&6; } if { { ac_try="$ac_link" case "(($ac_try" in @@ -3218,10 +3096,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : # If both 'conftest.exe' and 'conftest' are 'present' (well, observable) @@ -3238,15 +3116,15 @@ for ac_file in conftest.exe conftest conftest.*; do esac done else case e in #( - e) { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} + e) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of executables: cannot compile and link See 'config.log' for more details" "$LINENO" 5; } ;; esac fi rm -f conftest conftest$ac_cv_exeext -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 -printf '%s\n' "$ac_cv_exeext" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5 +printf "%s\n" "$ac_cv_exeext" >&6; } rm -f conftest.$ac_ext EXEEXT=$ac_cv_exeext @@ -3269,7 +3147,7 @@ _ACEOF ac_clean_files="$ac_clean_files conftest.out" # Check that the compiler produces executables we can run. If not, either # the compiler is broken, or we cross compile. -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5 printf %s "checking whether we are cross compiling... " >&6; } if test "$cross_compiling" != yes; then { { ac_try="$ac_link" @@ -3278,10 +3156,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } if { ac_try='./conftest$ac_cv_exeext' { { case "(($ac_try" in @@ -3289,31 +3167,31 @@ printf '%s\n' "$ac_try_echo"; } >&5 *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; }; then cross_compiling=no else if test "$cross_compiling" = maybe; then cross_compiling=yes else - { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error 77 "cannot run C compiled programs. If you meant to cross compile, use '--host'. See 'config.log' for more details" "$LINENO" 5; } fi fi fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 -printf '%s\n' "$cross_compiling" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5 +printf "%s\n" "$cross_compiling" >&6; } rm -f conftest.$ac_ext conftest$ac_cv_exeext \ conftest.o conftest.obj conftest.out ac_clean_files=$ac_clean_files_save -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5 printf %s "checking for suffix of object files... " >&6; } if test ${ac_cv_objext+y} then : @@ -3337,10 +3215,10 @@ case "(($ac_try" in *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -printf '%s\n' "$ac_try_echo"; } >&5 +printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_compile") 2>&5 ac_status=$? - printf '%s\n' "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } then : for ac_file in conftest.o conftest.obj conftest.*; do @@ -3352,11 +3230,11 @@ then : esac done else case e in #( - e) printf '%s\n' "$as_me: failed program was:" >&5 + e) printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 -{ { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} +{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot compute suffix of object files: cannot compile See 'config.log' for more details" "$LINENO" 5; } ;; esac @@ -3364,11 +3242,11 @@ fi rm -f conftest.$ac_cv_objext conftest.$ac_ext ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 -printf '%s\n' "$ac_cv_objext" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5 +printf "%s\n" "$ac_cv_objext" >&6; } OBJEXT=$ac_cv_objext ac_objext=$OBJEXT -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5 printf %s "checking whether the compiler supports GNU C... " >&6; } if test ${ac_cv_c_compiler_gnu+y} then : @@ -3400,8 +3278,8 @@ ac_cv_c_compiler_gnu=$ac_compiler_gnu ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 -printf '%s\n' "$ac_cv_c_compiler_gnu" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 +printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; } ac_compiler_gnu=$ac_cv_c_compiler_gnu if test $ac_compiler_gnu = yes; then @@ -3411,7 +3289,7 @@ else fi ac_test_CFLAGS=${CFLAGS+y} ac_save_CFLAGS=$CFLAGS -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 printf %s "checking whether $CC accepts -g... " >&6; } if test ${ac_cv_prog_cc_g+y} then : @@ -3479,8 +3357,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ac_c_werror_flag=$ac_save_c_werror_flag ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 -printf '%s\n' "$ac_cv_prog_cc_g" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 +printf "%s\n" "$ac_cv_prog_cc_g" >&6; } if test $ac_test_CFLAGS; then CFLAGS=$ac_save_CFLAGS elif test $ac_cv_prog_cc_g = yes; then @@ -3499,56 +3377,7 @@ fi ac_prog_cc_stdc=no if test x$ac_prog_cc_stdc = xno then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C23 features" >&5 -printf %s "checking for $CC option to enable C23 features... " >&6; } -if test ${ac_cv_prog_cc_c23+y} -then : - printf %s "(cached) " >&6 -else case e in #( - e) ac_cv_prog_cc_c23=no -ac_save_CC=$CC -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -$ac_c_conftest_c23_program -_ACEOF -for ac_arg in '' -std=gnu23 -do - CC="$ac_save_CC $ac_arg" - if ac_fn_c_try_compile "$LINENO" -then : - ac_cv_prog_cc_c23=$ac_arg -fi -rm -f core conftest.err conftest.$ac_objext conftest.beam - test "x$ac_cv_prog_cc_c23" != "xno" && break -done -rm -f conftest.$ac_ext -CC=$ac_save_CC ;; -esac -fi - -if test "x$ac_cv_prog_cc_c23" = xno -then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf '%s\n' "unsupported" >&6; } -else case e in #( - e) if test "x$ac_cv_prog_cc_c23" = x -then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf '%s\n' "none needed" >&6; } -else case e in #( - e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c23" >&5 -printf '%s\n' "$ac_cv_prog_cc_c23" >&6; } - CC="$CC $ac_cv_prog_cc_c23" ;; -esac -fi - ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c23 - ac_prog_cc_stdc=c23 ;; -esac -fi -fi -if test x$ac_prog_cc_stdc = xno -then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5 printf %s "checking for $CC option to enable C11 features... " >&6; } if test ${ac_cv_prog_cc_c11+y} then : @@ -3560,7 +3389,7 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ $ac_c_conftest_c11_program _ACEOF -for ac_arg in '' -std=gnu11 -std:c11 +for ac_arg in '' -std=gnu11 do CC="$ac_save_CC $ac_arg" if ac_fn_c_try_compile "$LINENO" @@ -3577,16 +3406,16 @@ fi if test "x$ac_cv_prog_cc_c11" = xno then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf '%s\n' "unsupported" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } else case e in #( e) if test "x$ac_cv_prog_cc_c11" = x then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf '%s\n' "none needed" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } else case e in #( - e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 -printf '%s\n' "$ac_cv_prog_cc_c11" >&6; } + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5 +printf "%s\n" "$ac_cv_prog_cc_c11" >&6; } CC="$CC $ac_cv_prog_cc_c11" ;; esac fi @@ -3597,7 +3426,7 @@ fi fi if test x$ac_prog_cc_stdc = xno then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5 printf %s "checking for $CC option to enable C99 features... " >&6; } if test ${ac_cv_prog_cc_c99+y} then : @@ -3626,16 +3455,16 @@ fi if test "x$ac_cv_prog_cc_c99" = xno then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf '%s\n' "unsupported" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } else case e in #( e) if test "x$ac_cv_prog_cc_c99" = x then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf '%s\n' "none needed" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } else case e in #( - e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 -printf '%s\n' "$ac_cv_prog_cc_c99" >&6; } + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 +printf "%s\n" "$ac_cv_prog_cc_c99" >&6; } CC="$CC $ac_cv_prog_cc_c99" ;; esac fi @@ -3646,7 +3475,7 @@ fi fi if test x$ac_prog_cc_stdc = xno then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5 printf %s "checking for $CC option to enable C89 features... " >&6; } if test ${ac_cv_prog_cc_c89+y} then : @@ -3675,16 +3504,16 @@ fi if test "x$ac_cv_prog_cc_c89" = xno then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -printf '%s\n' "unsupported" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +printf "%s\n" "unsupported" >&6; } else case e in #( e) if test "x$ac_cv_prog_cc_c89" = x then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -printf '%s\n' "none needed" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +printf "%s\n" "none needed" >&6; } else case e in #( - e) { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 -printf '%s\n' "$ac_cv_prog_cc_c89" >&6; } + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +printf "%s\n" "$ac_cv_prog_cc_c89" >&6; } CC="$CC $ac_cv_prog_cc_c89" ;; esac fi @@ -3705,7 +3534,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu # Checks for header files. -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for sys/wait.h that is POSIX.1 compatible" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sys/wait.h that is POSIX.1 compatible" >&5 printf %s "checking for sys/wait.h that is POSIX.1 compatible... " >&6; } if test ${ac_cv_header_sys_wait_h+y} then : @@ -3742,11 +3571,11 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 -printf '%s\n' "$ac_cv_header_sys_wait_h" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h" >&5 +printf "%s\n" "$ac_cv_header_sys_wait_h" >&6; } if test $ac_cv_header_sys_wait_h = yes; then -printf '%s\n' "#define HAVE_SYS_WAIT_H 1" >>confdefs.h +printf "%s\n" "#define HAVE_SYS_WAIT_H 1" >>confdefs.h fi @@ -3756,7 +3585,7 @@ do if test $ac_cache; then ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default" if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then - printf '%s\n' "#define $ac_item 1" >> confdefs.h + printf "%s\n" "#define $ac_item 1" >> confdefs.h fi ac_header= ac_cache= elif test $ac_header; then @@ -3776,31 +3605,31 @@ done if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes then : -printf '%s\n' "#define STDC_HEADERS 1" >>confdefs.h +printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h fi ac_fn_c_check_header_compile "$LINENO" "string.h" "ac_cv_header_string_h" "$ac_includes_default" if test "x$ac_cv_header_string_h" = xyes then : - printf '%s\n' "#define HAVE_STRING_H 1" >>confdefs.h + printf "%s\n" "#define HAVE_STRING_H 1" >>confdefs.h fi ac_fn_c_check_header_compile "$LINENO" "sys/time.h" "ac_cv_header_sys_time_h" "$ac_includes_default" if test "x$ac_cv_header_sys_time_h" = xyes then : - printf '%s\n' "#define HAVE_SYS_TIME_H 1" >>confdefs.h + printf "%s\n" "#define HAVE_SYS_TIME_H 1" >>confdefs.h fi ac_fn_c_check_header_compile "$LINENO" "unistd.h" "ac_cv_header_unistd_h" "$ac_includes_default" if test "x$ac_cv_header_unistd_h" = xyes then : - printf '%s\n' "#define HAVE_UNISTD_H 1" >>confdefs.h + printf "%s\n" "#define HAVE_UNISTD_H 1" >>confdefs.h fi # Checks for typedefs, structures, and compiler characteristics. -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for an ANSI C-conforming const" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for an ANSI C-conforming const" >&5 printf %s "checking for an ANSI C-conforming const... " >&6; } if test ${ac_cv_c_const+y} then : @@ -3841,12 +3670,15 @@ main (void) *t++ = 0; if (s) return 0; } - { /* Derived from code rejected by Sun C 1.0 and similar vintage. */ + { /* Someone thinks the Sun supposedly-ANSI compiler will reject this. */ int x[] = {25, 17}; - typedef int const *iptr; - iptr foo = &x[0]; + const int *foo = &x[0]; ++foo; - if (!*foo) return 0; + } + { /* Sun SC1.0 ANSI compiler rejects this -- but not the above. */ + typedef const int *iptr; + iptr p = 0; + ++p; } { /* IBM XL C 1.02.0.0 rejects this sort of thing, saying "k.c", line 2.27: 1506-025 (S) Operand must be a modifiable lvalue. */ @@ -3874,17 +3706,17 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_const" >&5 -printf '%s\n' "$ac_cv_c_const" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_const" >&5 +printf "%s\n" "$ac_cv_c_const" >&6; } if test $ac_cv_c_const = no; then -printf '%s\n' "#define const /**/" >>confdefs.h +printf "%s\n" "#define const /**/" >>confdefs.h fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports static inline..." >&5 -printf '%s\n' "$as_me: checking whether ${CC} supports static inline..." >&6;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports static inline..." >&5 +printf "%s\n" "$as_me: checking whether ${CC} supports static inline..." >&6;} can_inline=no cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -3902,16 +3734,16 @@ then : can_inline=yes fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${can_inline}" >&5 -printf '%s\n' "${can_inline}" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${can_inline}" >&5 +printf "%s\n" "${can_inline}" >&6; } if test "${can_inline}" = yes; then -printf '%s\n' "#define HAVE_STATIC_INLINE 1" >>confdefs.h +printf "%s\n" "#define HAVE_STATIC_INLINE 1" >>confdefs.h fi ### from R m4/R.m4 - needed to hack R 2.9.x -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether setjmp.h is POSIX.1 compatible" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether setjmp.h is POSIX.1 compatible" >&5 printf %s "checking whether setjmp.h is POSIX.1 compatible... " >&6; } if test ${r_cv_header_setjmp_posix+y} then : @@ -3940,10 +3772,10 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $r_cv_header_setjmp_posix" >&5 -printf '%s\n' "$r_cv_header_setjmp_posix" >&6; } -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC options to detect undeclared functions" >&5 -printf %s "checking for $CC options to detect undeclared functions... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $r_cv_header_setjmp_posix" >&5 +printf "%s\n" "$r_cv_header_setjmp_posix" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC options needed to detect all undeclared functions" >&5 +printf %s "checking for $CC options needed to detect all undeclared functions... " >&6; } if test ${ac_cv_c_undeclared_builtin_options+y} then : printf %s "(cached) " >&6 @@ -4011,12 +3843,12 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_undeclared_builtin_options" >&5 -printf '%s\n' "$ac_cv_c_undeclared_builtin_options" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_undeclared_builtin_options" >&5 +printf "%s\n" "$ac_cv_c_undeclared_builtin_options" >&6; } case $ac_cv_c_undeclared_builtin_options in #( 'cannot detect') : - { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot make $CC report undeclared builtins See 'config.log' for more details" "$LINENO" 5; } ;; #( 'none needed') : @@ -4025,51 +3857,8 @@ See 'config.log' for more details" "$LINENO" 5; } ;; #( ac_c_undeclared_builtin_options=$ac_cv_c_undeclared_builtin_options ;; esac -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking for $CC options to ignore future-version functions" >&5 -printf %s "checking for $CC options to ignore future-version functions... " >&6; } -if test ${ac_cv_c_future_darwin_options+y} -then : - printf %s "(cached) " >&6 -else case e in #( - e) ac_compile_saved="$ac_compile" - ac_compile="$ac_compile -Werror=unguarded-availability-new" - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#if ! (defined __APPLE__ && defined __MACH__) - #error "-Werror=unguarded-availability-new not needed here" - #endif - -int -main (void) -{ - - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO" -then : - ac_cv_c_future_darwin_options='-Werror=unguarded-availability-new' -else case e in #( - e) ac_cv_c_future_darwin_options='none needed' ;; -esac -fi -rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext - ac_compile="$ac_compile_saved" - ;; -esac -fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_future_darwin_options" >&5 -printf '%s\n' "$ac_cv_c_future_darwin_options" >&6; } - case $ac_cv_c_future_darwin_options in #( - 'none needed') : - ac_c_future_darwin_options='' ;; #( - *) : - ac_c_future_darwin_options=$ac_cv_c_future_darwin_options ;; -esac - ac_fn_check_decl "$LINENO" "sigsetjmp" "ac_cv_have_decl_sigsetjmp" "#include -" "$ac_c_undeclared_builtin_options$ac_c_future_darwin_options" "CFLAGS" +" "$ac_c_undeclared_builtin_options" "CFLAGS" if test "x$ac_cv_have_decl_sigsetjmp" = xyes then : ac_have_decl=1 @@ -4077,9 +3866,9 @@ else case e in #( e) ac_have_decl=0 ;; esac fi -printf '%s\n' "#define HAVE_DECL_SIGSETJMP $ac_have_decl" >>confdefs.h +printf "%s\n" "#define HAVE_DECL_SIGSETJMP $ac_have_decl" >>confdefs.h ac_fn_check_decl "$LINENO" "siglongjmp" "ac_cv_have_decl_siglongjmp" "#include -" "$ac_c_undeclared_builtin_options$ac_c_future_darwin_options" "CFLAGS" +" "$ac_c_undeclared_builtin_options" "CFLAGS" if test "x$ac_cv_have_decl_siglongjmp" = xyes then : ac_have_decl=1 @@ -4087,7 +3876,7 @@ else case e in #( e) ac_have_decl=0 ;; esac fi -printf '%s\n' "#define HAVE_DECL_SIGLONGJMP $ac_have_decl" >>confdefs.h +printf "%s\n" "#define HAVE_DECL_SIGLONGJMP $ac_have_decl" >>confdefs.h if test "$ac_cv_have_decl_sigsetjmp" = no; then r_cv_header_setjmp_posix=no @@ -4097,11 +3886,11 @@ if test "$ac_cv_have_decl_siglongjmp" = no; then fi if test "${r_cv_header_setjmp_posix}" = yes; then -printf '%s\n' "#define HAVE_POSIX_SETJMP 1" >>confdefs.h +printf "%s\n" "#define HAVE_POSIX_SETJMP 1" >>confdefs.h fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking Java support in R" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking Java support in R" >&5 printf %s "checking Java support in R... " >&6; } R_JAVA_HOME=`"${RBIN}" CMD config JAVA_HOME` : ${JAVA_HOME="${R_JAVA_HOME}"} @@ -4123,14 +3912,14 @@ fi : ${JAVA_CPPFLAGS=`"${RBIN}" CMD config JAVA_CPPFLAGS|sed 's/ERROR:.*//'`} : ${JAVA_LIBS=`"${RBIN}" CMD config JAVA_LIBS|sed 's/ERROR:.*//'`} -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: present: +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: present: interpreter : '${JAVA}' archiver : '${JAR}' compiler : '${JAVAC}' header prep.: '${JAVAH}' cpp flags : '${JAVA_CPPFLAGS}' java libs : '${JAVA_LIBS}'" >&5 -printf '%s\n' "present: +printf "%s\n" "present: interpreter : '${JAVA}' archiver : '${JAR}' compiler : '${JAVAC}' @@ -4168,8 +3957,8 @@ if test `echo foo | sed -e 's:foo:bar:'` = bar; then JAVA_LIBS0=`echo ${JAVA_LIBS} | sed -e 's:$(JAVA_HOME):'${JAVA_HOME}':g'` JAVA_LD_LIBRARY_PATH0=`echo ${JAVA_LD_LIBRARY_PATH} | sed -e 's:$(JAVA_HOME):'${JAVA_HOME}':g'` else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: sed is not working properly - the configuration may fail" >&5 -printf '%s\n' "$as_me: WARNING: sed is not working properly - the configuration may fail" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: sed is not working properly - the configuration may fail" >&5 +printf "%s\n" "$as_me: WARNING: sed is not working properly - the configuration may fail" >&2;} JAVA_CPPFLAGS0="${JAVA_CPPFLAGS}" JAVA_LIBS0="${JAVA_LIBS}" JAVA_LD_LIBRARY_PATH0="${JAVA_LD_LIBRARY_PATH}" @@ -4186,37 +3975,37 @@ if test "x$OSNAME" = xDarwin; then export DYLD_FALLBACK_LIBRARY_PATH fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether Java run-time works" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether Java run-time works" >&5 printf %s "checking whether Java run-time works... " >&6; } if "$JAVA" -classpath tools getsp; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } as_fn_error $? "Java interpreter '$JAVA' does not work" "$LINENO" 5 fi has_xrs="$want_xrs" if test x"$has_xrs" = xauto; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether -Xrs is supported" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether -Xrs is supported" >&5 printf %s "checking whether -Xrs is supported... " >&6; } if "$JAVA" -Xrs -classpath tools getsp; then has_xrs=yes else has_xrs=no fi - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 -printf '%s\n' "${has_xrs}" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 +printf "%s\n" "${has_xrs}" >&6; } fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether -Xrs will be used" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether -Xrs will be used" >&5 printf %s "checking whether -Xrs will be used... " >&6; } -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 -printf '%s\n' "${has_xrs}" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${has_xrs}" >&5 +printf "%s\n" "${has_xrs}" >&6; } if test x"$has_xrs" = xyes; then -printf '%s\n' "#define HAVE_XRS 1" >>confdefs.h +printf "%s\n" "#define HAVE_XRS 1" >>confdefs.h fi @@ -4231,10 +4020,10 @@ fi if test x"$use_dynload" != xyes; then use_dynload=no fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JVM will be loaded dynamically" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JVM will be loaded dynamically" >&5 printf %s "checking whether JVM will be loaded dynamically... " >&6; } -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${use_dynload}" >&5 -printf '%s\n' "${use_dynload}" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${use_dynload}" >&5 +printf "%s\n" "${use_dynload}" >&6; } if test "x${use_dynload}" = xyes; then USE_DYNLOAD_TRUE= USE_DYNLOAD_FALSE='#' @@ -4246,7 +4035,7 @@ fi if test x${use_dynload} = xno; then -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JNI programs can be compiled" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI programs can be compiled" >&5 printf %s "checking whether JNI programs can be compiled... " >&6; } cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4261,8 +4050,8 @@ int main(void) { _ACEOF if ac_fn_c_try_link "$LINENO" then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else case e in #( e) as_fn_error $? "Cannot compile a simple JNI program. See config.log for details. @@ -4274,12 +4063,12 @@ fi rm -f core conftest.err conftest.$ac_objext conftest.beam \ conftest$ac_exeext conftest.$ac_ext -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JNI programs run" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI programs run" >&5 printf %s "checking whether JNI programs run... " >&6; } if test "$cross_compiling" = yes then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: don't know (cross-compiling)" >&5 -printf '%s\n' "don't know (cross-compiling)" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: don't know (cross-compiling)" >&5 +printf "%s\n" "don't know (cross-compiling)" >&6; } else case e in #( e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4294,8 +4083,8 @@ int main(void) { _ACEOF if ac_fn_c_try_run "$LINENO" then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else case e in #( e) as_fn_error $? "Unable to run a simple JNI program. Make sure you have configured R with Java support (see R documentation) and check config.log for failure reason." "$LINENO" 5 ;; esac @@ -4306,12 +4095,12 @@ esac fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking JNI data types" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking JNI data types" >&5 printf %s "checking JNI data types... " >&6; } if test "$cross_compiling" = yes then : - { { printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf '%s\n' "$as_me: error: in '$ac_pwd':" >&2;} + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling See 'config.log' for more details" "$LINENO" 5; } else case e in #( @@ -4326,8 +4115,8 @@ int main(void) { _ACEOF if ac_fn_c_try_run "$LINENO" then : - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ok" >&5 -printf '%s\n' "ok" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ok" >&5 +printf "%s\n" "ok" >&6; } else case e in #( e) as_fn_error $? "One or more JNI types differ from the corresponding native type. You may need to use non-standard compiler flags or a different compiler in order to fix this." "$LINENO" 5 ;; esac @@ -4341,10 +4130,10 @@ fi fi ## dynload = no if test "${want_jri}" = auto; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JRI should be compiled (autodetect)" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JRI should be compiled (autodetect)" >&5 printf %s "checking whether JRI should be compiled (autodetect)... " >&6; } - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${has_R_shlib}" >&5 -printf '%s\n' "${has_R_shlib}" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${has_R_shlib}" >&5 +printf "%s\n" "${has_R_shlib}" >&6; } want_jri=${has_R_shlib} fi if test "x${want_jri}" = xyes; then @@ -4356,31 +4145,31 @@ else fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether debugging output should be enabled" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether debugging output should be enabled" >&5 printf %s "checking whether debugging output should be enabled... " >&6; } if test "${want_debug}" = yes; then JAVA_CPPFLAGS="-g -DRJ_DEBUG ${JAVA_CPPFLAGS}" - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether memory profiling is desired" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether memory profiling is desired" >&5 printf %s "checking whether memory profiling is desired... " >&6; } if test "${want_memprof}" = debug; then want_memprof="${want_debug}" fi if test "${want_memprof}" = yes; then -printf '%s\n' "#define MEMPROF 1" >>confdefs.h +printf "%s\n" "#define MEMPROF 1" >>confdefs.h - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi @@ -4391,62 +4180,62 @@ fi use_threads=no -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether threads support is requested" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether threads support is requested" >&5 printf %s "checking whether threads support is requested... " >&6; } if test "${want_threads}" = yes; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether threads can be enabled" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether threads can be enabled" >&5 printf %s "checking whether threads can be enabled... " >&6; } # check whether we can add THREADS support # we don't want to run full AC_CANONICAL_HOST, all we care about is OS X if test "x$OSNAME" = xDarwin; then use_threads=yes -printf '%s\n' "#define THREADS 1" >>confdefs.h +printf "%s\n" "#define THREADS 1" >>confdefs.h fi - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${use_threads}" >&5 -printf '%s\n' "${use_threads}" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${use_threads}" >&5 +printf "%s\n" "${use_threads}" >&6; } else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi ## enable callbacks if desired -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether callbacks support is requested" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether callbacks support is requested" >&5 printf %s "checking whether callbacks support is requested... " >&6; } if test "${want_callbacks}" = yes; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } if test "${want_jri}" != yes; then as_fn_error $? "Callbacks support can be only enabled if JRI is enabled as well." "$LINENO" 5 fi -printf '%s\n' "#define ENABLE_JRICB 1" >>confdefs.h +printf "%s\n" "#define ENABLE_JRICB 1" >>confdefs.h else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JNI cache support is requested" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI cache support is requested" >&5 printf %s "checking whether JNI cache support is requested... " >&6; } if test "${want_jni_cache}" = yes; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } if test "${use_threads}" = yes; then as_fn_error $? "Threads and JNI cache cannot be used at the same time, because JNI cache is by definition not thread-safe. Please disable either option." "$LINENO" 5 fi -printf '%s\n' "#define JNI_CACHE 1" >>confdefs.h +printf "%s\n" "#define JNI_CACHE 1" >>confdefs.h else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether headless init is enabled" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether headless init is enabled" >&5 printf %s "checking whether headless init is enabled... " >&6; } if test "${want_headless}" = auto; then want_headless=no @@ -4455,19 +4244,19 @@ if test "${want_headless}" = auto; then want_headless=yes fi fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: ${want_headless}" >&5 -printf '%s\n' "${want_headless}" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${want_headless}" >&5 +printf "%s\n" "${want_headless}" >&6; } if test "${want_headless}" = yes; then -printf '%s\n' "#define USE_HEADLESS_INIT 1" >>confdefs.h +printf "%s\n" "#define USE_HEADLESS_INIT 1" >>confdefs.h fi -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: checking whether JRI is requested" >&5 +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JRI is requested" >&5 printf %s "checking whether JRI is requested... " >&6; } if test "${want_jri}" = yes; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf '%s\n' "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } export R_HOME export JAVA_HOME JAVA_CPPFLAGS JAVA_LIBS JAVA_LD_LIBRARY_PATH JAVA JAVAC JAVAH JAR CONFIGURED=1 @@ -4477,8 +4266,8 @@ printf '%s\n' "yes" >&6; } subdirs="$subdirs src/jri" else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf '%s\n' "no" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } fi ac_config_files="$ac_config_files src/Makevars" @@ -4501,7 +4290,44 @@ cat >confcache <<\_ACEOF _ACEOF -ac_cache_dump | +# The following way of writing the cache mishandles newlines in values, +# but we know of no workaround that is simple, portable, and efficient. +# So, we kill variables containing newlines. +# Ultrix sh set writes to stderr and can't be redirected directly, +# and sets the high bit in the cache file unless we assign to the vars. +( + for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do + eval ac_val=\$$ac_var + case $ac_val in #( + *${as_nl}*) + case $ac_var in #( + *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 +printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; + esac + case $ac_var in #( + _ | IFS | as_nl) ;; #( + BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( + *) { eval $ac_var=; unset $ac_var;} ;; + esac ;; + esac + done + + (set) 2>&1 | + case $as_nl`(ac_space=' '; set) 2>&1` in #( + *${as_nl}ac_space=\ *) + # 'set' does not quote correctly, so add quotes: double-quote + # substitution turns \\\\ into \\, and sed turns \\ into \. + sed -n \ + "s/'/'\\\\''/g; + s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" + ;; #( + *) + # 'set' quotes correctly as required by POSIX, so do not add quotes. + sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" + ;; + esac | + sort +) | sed ' /^ac_cv_env_/b end t clear @@ -4513,8 +4339,8 @@ ac_cache_dump | if diff "$cache_file" confcache >/dev/null 2>&1; then :; else if test -w "$cache_file"; then if test "x$cache_file" != "x/dev/null"; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 -printf '%s\n' "$as_me: updating cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 +printf "%s\n" "$as_me: updating cache $cache_file" >&6;} if test ! -f "$cache_file" || test -h "$cache_file"; then cat confcache >"$cache_file" else @@ -4528,8 +4354,8 @@ printf '%s\n' "$as_me: updating cache $cache_file" >&6;} fi fi else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 -printf '%s\n' "$as_me: not updating unwritable cache $cache_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 +printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;} fi fi rm -f confcache @@ -4546,7 +4372,7 @@ U= for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue # 1. Remove the extension, and $U if already installed. ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' - ac_i=`printf '%s\n' "$ac_i" | sed "$ac_script"` + ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"` # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR # will be set to the directory where LIBOBJS objects are built. as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" @@ -4567,21 +4393,13 @@ Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi : "${CONFIG_STATUS=./config.status}" -case $CONFIG_STATUS in #( - -*) : - CONFIG_STATUS=./$CONFIG_STATUS ;; #( - */*) : - ;; #( - *) : - CONFIG_STATUS=./$CONFIG_STATUS ;; -esac - ac_write_fail=0 -ac_clean_CONFIG_STATUS='"$CONFIG_STATUS"' -{ printf '%s\n' "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 -printf '%s\n' "$as_me: creating $CONFIG_STATUS" >&6;} +ac_clean_files_save=$ac_clean_files +ac_clean_files="$ac_clean_files $CONFIG_STATUS" +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 +printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;} as_write_fail=0 -cat >"$CONFIG_STATUS" <<_ASEOF || as_write_fail=1 +cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 #! $SHELL # Generated by $as_me. # Run this file to recreate the current configuration. @@ -4595,7 +4413,7 @@ ac_cs_silent=false SHELL=\${CONFIG_SHELL-$SHELL} export SHELL _ASEOF -cat >>"$CONFIG_STATUS" <<\_ASEOF || as_write_fail=1 +cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## @@ -4607,7 +4425,7 @@ then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which - # contradicts POSIX and common usage. Disable this. + # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else case e in #( @@ -4694,7 +4512,7 @@ if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then - printf '%s\n' "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 + printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi @@ -4710,9 +4528,9 @@ as_fn_error () as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - printf '%s\n' "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi - printf '%s\n' "$as_me: error: $2" >&2 + printf "%s\n" "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error @@ -4807,7 +4625,7 @@ as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || -printf '%s\n' X/"$0" | +printf "%s\n" X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q @@ -4829,6 +4647,29 @@ as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits + +# Determine whether it's possible to make 'echo' print without a newline. +# These variables are no longer used directly by Autoconf, but are AC_SUBSTed +# for compatibility with existing Makefiles. +ECHO_C= ECHO_N= ECHO_T= +case `echo -n x` in #((((( +-n*) + case `echo 'xy\c'` in + *c*) ECHO_T=' ';; # ECHO_T is single tab character. + xy) ECHO_C='\c';; + *) echo `echo ksh88 bug on AIX 6.1` > /dev/null + ECHO_T=' ';; + esac;; +*) + ECHO_N='-n';; +esac + +# For backward compatibility with old third-party macros, we provide +# the shell variables $as_echo and $as_echo_n. New code should use +# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. +as_echo='printf %s\n' +as_echo_n='printf %s' + rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file @@ -4870,7 +4711,7 @@ as_fn_mkdir_p () as_dirs= while :; do case $as_dir in #( - *\'*) as_qdir=`printf '%s\n' "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( + *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" @@ -4879,7 +4720,7 @@ $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -printf '%s\n' X"$as_dir" | +printf "%s\n" X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -4932,19 +4773,19 @@ as_tr_sh="eval sed '$as_sed_sh'" # deprecated exec 6>&1 -## ------------------------------------- ## -## Main body of "$CONFIG_STATUS" script. ## -## ------------------------------------- ## +## ----------------------------------- ## +## Main body of $CONFIG_STATUS script. ## +## ----------------------------------- ## _ASEOF -test $as_write_fail = 0 && chmod +x "$CONFIG_STATUS" || ac_write_fail=1 +test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # Save the log message, to keep $0 and so on meaningful, and to # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" This file was extended by rJava $as_me 0.8, which was -generated by GNU Autoconf 2.73. Invocation command line was +generated by GNU Autoconf 2.72. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS @@ -4966,14 +4807,14 @@ case $ac_config_headers in *" esac -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 # Files that config.status was made for. config_files="$ac_config_files" config_headers="$ac_config_headers" _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 ac_cs_usage="\ '$as_me' instantiates files and other configuration actions from templates according to the current configuration. Unless the files @@ -5002,29 +4843,25 @@ $config_headers Report bugs to ." _ACEOF -ac_cs_config=`printf '%s\n' "$ac_configure_args" | sed "$ac_safe_unquote"` -ac_cs_config_escaped=`printf '%s\n' "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"` +ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ rJava config.status 0.8 -configured by $0, generated by GNU Autoconf 2.73, +configured by $0, generated by GNU Autoconf 2.72, with options \\"\$ac_cs_config\\" -Copyright (C) 2026 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." ac_pwd='$ac_pwd' srcdir='$srcdir' -test -n "\$AWK" || { - awk '' >"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # The default lists apply if the user does not specify any file. ac_need_defaults=: while test $# != 0 @@ -5052,15 +4889,15 @@ do -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) ac_cs_recheck=: ;; --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) - printf '%s\n' "$ac_cs_version"; exit ;; + printf "%s\n" "$ac_cs_version"; exit ;; --config | --confi | --conf | --con | --co | --c ) - printf '%s\n' "$ac_cs_config"; exit ;; + printf "%s\n" "$ac_cs_config"; exit ;; --debug | --debu | --deb | --de | --d | -d ) debug=: ;; --file | --fil | --fi | --f ) $ac_shift case $ac_optarg in - *\'*) ac_optarg=`printf '%s\n' "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; '') as_fn_error $? "missing file argument" ;; esac as_fn_append CONFIG_FILES " '$ac_optarg'" @@ -5068,7 +4905,7 @@ do --header | --heade | --head | --hea ) $ac_shift case $ac_optarg in - *\'*) ac_optarg=`printf '%s\n' "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append CONFIG_HEADERS " '$ac_optarg'" ac_need_defaults=false;; @@ -5077,7 +4914,7 @@ do as_fn_error $? "ambiguous option: '$1' Try '$0 --help' for more information.";; --help | --hel | -h ) - printf '%s\n' "$ac_cs_usage"; exit ;; + printf "%s\n" "$ac_cs_usage"; exit ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil | --si | --s) ac_cs_silent=: ;; @@ -5101,32 +4938,32 @@ if $ac_cs_silent; then fi _ACEOF -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 if \$ac_cs_recheck; then set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion shift - \printf '%s\n' "running CONFIG_SHELL=$SHELL \$*" >&6 + \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6 CONFIG_SHELL='$SHELL' export CONFIG_SHELL exec "\$@" fi _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 exec 5>>config.log { echo sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX ## Running $as_me. ## _ASBOX - printf '%s\n' "$ac_log" + printf "%s\n" "$ac_log" } >&5 _ACEOF -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # Handling of arguments. for ac_config_target in $ac_config_targets @@ -5207,13 +5044,13 @@ _ACEOF echo "_ACEOF" } >conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 -ac_delim_num=`echo "$ac_subst_vars" | sed -n '$='` +ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` ac_delim='%!_!# ' for ac_last_try in false false false false false :; do . ./conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 - ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | sed -n '$='` + ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` if test $ac_delim_n = $ac_delim_num; then break elif $ac_last_try; then @@ -5224,7 +5061,7 @@ for ac_last_try in false false false false false :; do done rm -f conf$$subs.sh -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && _ACEOF sed -n ' @@ -5269,9 +5106,9 @@ t delim N s/\n// } -' >>"$CONFIG_STATUS" || ac_write_fail=1 +' >>$CONFIG_STATUS || ac_write_fail=1 rm -f conf$$subs.awk -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 _ACAWK cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && for (key in S) S_is_set[key] = 1 @@ -5300,7 +5137,7 @@ cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && _ACAWK _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" else @@ -5332,7 +5169,7 @@ s/^[^=]*=[ ]*$// }' fi -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 fi # test -n "$CONFIG_FILES" # Set up the scripts for CONFIG_HEADERS section. @@ -5403,9 +5240,9 @@ s/["\\]/\\&/g; s/^/"/; s/$/\\\\\\n"\\/p b cont ' >"$CONFIG_STATUS" || ac_write_fail=1 +"/g' >>$CONFIG_STATUS || ac_write_fail=1 -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 for (key in D) D_is_set[key] = 1 FS = "" } @@ -5423,12 +5260,8 @@ cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 macro = mac2[1] prefix = substr(line, 1, index(line, defundef) - 1) if (D_is_set[macro]) { - suffix = P[macro] D[macro] - while (suffix ~ /[\t ]$/) { - suffix = substr(suffix, 1, length(suffix) - 1) - } # Preserve the white space surrounding the "#". - print prefix "define", macro suffix + print prefix "define", macro P[macro] D[macro] next } else { # Replace #undef with comments. This is necessary, for example, @@ -5443,7 +5276,7 @@ cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 { print } _ACAWK _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 as_fn_error $? "could not setup config headers machinery" "$LINENO" 5 fi # test -n "$CONFIG_HEADERS" @@ -5487,7 +5320,7 @@ do esac || as_fn_error 1 "cannot find input file: '$ac_f'" "$LINENO" 5;; esac - case $ac_f in *\'*) ac_f=`printf '%s\n' "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac + case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" done @@ -5495,17 +5328,17 @@ do # use $as_me), people would be surprised to read: # /* config.h. Generated by config.status. */ configure_input='Generated from '` - printf '%s\n' "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' + printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' `' by configure.' if test x"$ac_file" != x-; then configure_input="$ac_file. $configure_input" - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 -printf '%s\n' "$as_me: creating $ac_file" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 +printf "%s\n" "$as_me: creating $ac_file" >&6;} fi # Neutralize special characters interpreted by sed in replacement strings. case $configure_input in #( *\&* | *\|* | *\\* ) - ac_sed_conf_input=`printf '%s\n' "$configure_input" | + ac_sed_conf_input=`printf "%s\n" "$configure_input" | sed 's/[\\\\&|]/\\\\&/g'`;; #( *) ac_sed_conf_input=$configure_input;; esac @@ -5522,7 +5355,7 @@ $as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$ac_file" : 'X\(//\)[^/]' \| \ X"$ac_file" : 'X\(//\)$' \| \ X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || -printf '%s\n' X"$ac_file" | +printf "%s\n" X"$ac_file" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q @@ -5546,9 +5379,9 @@ printf '%s\n' X"$ac_file" | case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`printf '%s\n' "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`printf '%s\n' "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -5584,7 +5417,7 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # If the template does not know about datarootdir, expand it. # FIXME: This hack should be removed a few years after 2.60. ac_datarootdir_hack=; ac_datarootdir_seen= @@ -5601,10 +5434,10 @@ ac_sed_dataroot=' case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in *datarootdir*) ac_datarootdir_seen=yes;; *@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 -printf '%s\n' "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 +printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} _ACEOF -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_datarootdir_hack=' s&@datadir@&$datadir&g s&@docdir@&$docdir&g @@ -5618,11 +5451,11 @@ _ACEOF # Neutralize VPATH when '$srcdir' = '.'. # Shell code in configure.ac might set extrasub. # FIXME: do we really want to maintain this feature? -cat >>"$CONFIG_STATUS" <<_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_sed_extra="$ac_vpsub $extrasub _ACEOF -cat >>"$CONFIG_STATUS" <<\_ACEOF || ac_write_fail=1 +cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 :t /@[a-zA-Z_][a-zA-Z_0-9]*@/!b s|@configure_input@|$ac_sed_conf_input|;t t @@ -5644,9 +5477,9 @@ test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ "$ac_tmp/out"`; test -z "$ac_out"; } && - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir' + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&5 -printf '%s\n' "$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir' +printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir' which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$ac_tmp/stdin" @@ -5662,20 +5495,20 @@ which seems to be undefined. Please make sure it is defined" >&2;} # if test x"$ac_file" != x-; then { - printf '%s\n' "/* $configure_input */" >&1 \ + printf "%s\n" "/* $configure_input */" >&1 \ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" } >"$ac_tmp/config.h" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 if diff "$ac_file" "$ac_tmp/config.h" >/dev/null 2>&1; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 -printf '%s\n' "$as_me: $ac_file is unchanged" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5 +printf "%s\n" "$as_me: $ac_file is unchanged" >&6;} else rm -f "$ac_file" mv "$ac_tmp/config.h" "$ac_file" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 fi else - printf '%s\n' "/* $configure_input */" >&1 \ + printf "%s\n" "/* $configure_input */" >&1 \ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" \ || as_fn_error $? "could not create -" "$LINENO" 5 fi @@ -5689,7 +5522,7 @@ done # for ac_tag as_fn_exit 0 _ACEOF -ac_clean_CONFIG_STATUS= +ac_clean_files=$ac_clean_files_save test $ac_write_fail = 0 || as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 @@ -5705,18 +5538,11 @@ test $ac_write_fail = 0 || # need to make the FD available again. if test "$no_create" != yes; then ac_cs_success=: - case $CONFIG_STATUS in #( - -*) : - ac_no_opts=-- ;; #( - *) : - ac_no_opts= ;; -esac ac_config_status_args= test "$silent" = yes && ac_config_status_args="$ac_config_status_args --quiet" exec 5>/dev/null - $SHELL $ac_no_opts "$CONFIG_STATUS" $ac_config_status_args || - ac_cs_success=false + $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false exec 5>>config.log # Use ||, not &&, to avoid exiting from the if with $? = 1, which # would make configure fail if this is the last instruction. @@ -5762,7 +5588,7 @@ if test "$no_recursion" != yes; then ;; *) case $ac_arg in - *\'*) ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append ac_sub_configure_args " '$ac_arg'" ;; esac @@ -5772,7 +5598,7 @@ if test "$no_recursion" != yes; then # in subdir configurations. ac_arg="--prefix=$prefix" case $ac_arg in - *\'*) ac_arg=`printf '%s\n' "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; + *\'*) ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac ac_sub_configure_args="'$ac_arg' $ac_sub_configure_args" @@ -5793,17 +5619,17 @@ if test "$no_recursion" != yes; then test -d "$srcdir/$ac_dir" || continue ac_msg="=== configuring in $ac_dir (`pwd`/$ac_dir)" - printf '%s\n' "$as_me:${as_lineno-$LINENO}: $ac_msg" >&5 - printf '%s\n' "$ac_msg" >&6 + printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_msg" >&5 + printf "%s\n" "$ac_msg" >&6 as_dir="$ac_dir"; as_fn_mkdir_p ac_builddir=. case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) - ac_dir_suffix=/`printf '%s\n' "$ac_dir" | sed 's|^\.[\\/]||'` + ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`printf '%s\n' "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` + ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; @@ -5840,8 +5666,8 @@ ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix elif test -f "$ac_srcdir/configure"; then ac_sub_configure=$ac_srcdir/configure else - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: no configuration information is in $ac_dir" >&5 -printf '%s\n' "$as_me: WARNING: no configuration information is in $ac_dir" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: no configuration information is in $ac_dir" >&5 +printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2;} ac_sub_configure= fi @@ -5854,8 +5680,8 @@ printf '%s\n' "$as_me: WARNING: no configuration information is in $ac_dir" >&2; ac_sub_cache_file=$ac_top_build_prefix$cache_file ;; esac - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&5 -printf '%s\n' "$as_me: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&6;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&5 +printf "%s\n" "$as_me: running $SHELL $ac_sub_configure $ac_sub_configure_args --cache-file=$ac_sub_cache_file --srcdir=$ac_srcdir" >&6;} # The eval makes quoting arguments work. eval "\$SHELL \"\$ac_sub_configure\" $ac_sub_configure_args \ --cache-file=\"\$ac_sub_cache_file\" --srcdir=\"\$ac_srcdir\"" || @@ -5866,8 +5692,8 @@ printf '%s\n' "$as_me: running $SHELL $ac_sub_configure $ac_sub_configure_args - done fi if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then - { printf '%s\n' "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 -printf '%s\n' "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 +printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi From 123838eaf55316a9ac958db4f1e572a78e60e8a9 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:02:25 +0200 Subject: [PATCH 14/63] fix: identify mapped JVM libraries by file identity and harden Windows paths Windows: replace four fixed 32768-wchar stack buffers with length-queried heap allocations. GetFullPathNameW returns the required size when the buffer is too small, and GetModuleFileNameW returns the buffer size when it truncates; both were previously accepted as success. A jvm.dll already mapped is now compared by volume serial and file ID (FileIdInfo, falling back to BY_HANDLE_FILE_INFORMATION), so 8.3 names, junctions, symbolic links and other spellings of the same file are accepted, while different files and unverifiable identities are still rejected. When the identity matches, djni takes a reference to the mapped module by address instead of loading the alias, so a second copy of jvm.dll cannot be mapped. Errors include the path and the system message. POSIX: compare st_dev/st_ino instead of realpath() strings, and dlopen a mapped JVM by its own name. Pass native-encoded paths to dlopen. Add djni_mapped()/RgetMappedJVM: reports a JVM library already mapped into the process and its number of created VMs (RTLD_NOLOAD or an unchanged module reference), without loading or initializing anything. Add compile-time checks of the JNI type sizes, which configure no longer tests because src/jni.h replaces the JDK headers. Reject empty paths. --- R/jfirst.R | 2 +- src/Rglue.c | 31 ++++- src/djni.c | 364 ++++++++++++++++++++++++++++++++++++++++++---------- src/djni.h | 3 + 4 files changed, 328 insertions(+), 72 deletions(-) diff --git a/R/jfirst.R b/R/jfirst.R index a32fada..7c20d37 100644 --- a/R/jfirst.R +++ b/R/jfirst.R @@ -22,7 +22,7 @@ "RgetShortArrayCont", "RgetStringArrayCont", "RidenticalRef", "RgetSimpleClassNames", "RisAssignableFrom", "RpollException", "RsetField", "RthrowException", "javaObjectCache", "initRJavaTools", "newRJavaLookupTable", "useDynamicSymbols", - "RgetJVMstate", "RloadJVM", "RunloadJVM", + "RgetJVMstate", "RloadJVM", "RunloadJVM", "RgetMappedJVM", # .External "RcreateObject", "RgetStringValue", "RinitJVM", "RtoString", "RcallMethod", # .C diff --git a/src/Rglue.c b/src/Rglue.c index c340ae5..fd6a1f6 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1142,8 +1142,13 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { int res, silent = Rf_asInteger(sSilent); if (TYPEOF(sPath) != STRSXP || LENGTH(sPath) != 1) Rf_error("Invalid path to JVM library"); - /* FIXME: worry about encoding? */ - path = Rf_translateCharUTF8(STRING_ELT(sPath, 0)); +#ifdef _WIN32 + path = Rf_translateCharUTF8(STRING_ELT(sPath, 0)); /* djni converts to UTF-16 */ +#else + path = Rf_translateChar(STRING_ELT(sPath, 0)); /* dlopen uses native bytes */ +#endif + if (!*path) + Rf_error("Invalid path to JVM library"); res = djni_load(path); if (silent) return Rf_ScalarInteger(res); @@ -1163,3 +1168,25 @@ REPC SEXP RunloadJVM(void) { Rf_error("Cannot safely unload a selected JVM library; use a new R process."); return Rf_ScalarLogical(1); } + +/* list(path, vms) for a JVM library already mapped into the process, e.g. by + a Java application embedding R, or NULL. Nothing is loaded or initialized. */ +REPC SEXP RgetMappedJVM(void) { + int vms; + const char *path = djni_mapped(&vms); + SEXP res, names; + if (!path) return R_NilValue; + res = PROTECT(Rf_allocVector(VECSXP, 2)); + names = PROTECT(Rf_allocVector(STRSXP, 2)); +#ifdef _WIN32 + SET_VECTOR_ELT(res, 0, Rf_ScalarString(Rf_mkCharCE(path, CE_UTF8))); +#else + SET_VECTOR_ELT(res, 0, Rf_mkString(path)); +#endif + SET_VECTOR_ELT(res, 1, Rf_ScalarInteger(vms)); + SET_STRING_ELT(names, 0, Rf_mkChar("path")); + SET_STRING_ELT(names, 1, Rf_mkChar("vms")); + Rf_setAttrib(res, R_NamesSymbol, names); + UNPROTECT(2); + return res; +} diff --git a/src/djni.c b/src/djni.c index f670005..8d0f232 100644 --- a/src/djni.c +++ b/src/djni.c @@ -7,6 +7,7 @@ #include #else #include +#include #ifdef __APPLE__ #include #elif defined(__linux__) @@ -18,6 +19,18 @@ #include #include "djni.h" +/* jni.h replaces the JDK headers, so configure no longer compares JNI types + with native ones. Check the fixed sizes all supported JDKs use instead. */ +typedef char djni_jbyte_size[sizeof(jbyte) == 1 ? 1 : -1]; +typedef char djni_jboolean_size[sizeof(jboolean) == 1 ? 1 : -1]; +typedef char djni_jchar_size[sizeof(jchar) == 2 ? 1 : -1]; +typedef char djni_jshort_size[sizeof(jshort) == 2 ? 1 : -1]; +typedef char djni_jint_size[sizeof(jint) == 4 ? 1 : -1]; +typedef char djni_jlong_size[sizeof(jlong) == 8 ? 1 : -1]; +typedef char djni_jfloat_size[sizeof(jfloat) == 4 ? 1 : -1]; +typedef char djni_jdouble_size[sizeof(jdouble) == 8 ? 1 : -1]; +typedef char djni_jvalue_size[sizeof(jvalue) == 8 ? 1 : -1]; + static JNI_GetDefaultJavaVMInitArgs_fnptr JNI_GetDefaultJavaVMInitArgs_fn; static JNI_CreateJavaVM_fnptr JNI_CreateJavaVM_fn; static JNI_GetCreatedJavaVMs_fnptr JNI_GetCreatedJavaVMs_fn; @@ -27,95 +40,303 @@ static HMODULE jni_dl; static void *jni_dl; #endif static char last_error[1024]; +static char *mapped_name; static void clear_symbols(void) { JNI_GetDefaultJavaVMInitArgs_fn = 0; JNI_CreateJavaVM_fn = 0; JNI_GetCreatedJavaVMs_fn = 0; } -static void save_error(void) { + #ifdef _WIN32 - DWORD code = GetLastError(); - snprintf(last_error, sizeof(last_error), "Windows loader error %lu", (unsigned long) code); -#else - const char *error = dlerror(); - snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); -#endif +static wchar_t *to_wide(const char *s) { + int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, -1, NULL, 0); + wchar_t *w = (n > 0) ? malloc(sizeof(wchar_t) * n) : NULL; + if (n > 0 && !w) SetLastError(ERROR_NOT_ENOUGH_MEMORY); + if (w && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, -1, w, n) != n) { + free(w); + w = NULL; + } + return w; } -static int load_sym(const char *sym, void **ptr) { -#ifdef _WIN32 - *ptr = (void *) GetProcAddress(jni_dl, sym); -#else - *ptr = dlsym(jni_dl, sym); -#endif - if (!*ptr) { save_error(); return -1; } - return 0; +static char *to_utf8(const wchar_t *w) { + int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL); + char *s = (n > 0) ? malloc(n) : NULL; + if (s && WideCharToMultiByte(CP_UTF8, 0, w, -1, s, n, NULL, NULL) != n) { + free(s); + s = NULL; + } + return s; } -#ifndef _WIN32 -static int different_jvm(const char *loaded, const char *requested) { - const char *base = strrchr(loaded, '/'); - char *a, *b; - int different; - base = base ? base + 1 : loaded; - if (strcmp(base, "libjvm.dylib") && strcmp(base, "libjvm.so")) return 0; - a = realpath(loaded, NULL); - b = realpath(requested, NULL); - different = !a || !b || strcmp(a, b); - if (different) - snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", loaded); - free(a); free(b); - return different; +static void save_win_error(const char *what, const char *path, DWORD code) { + wchar_t *msg = NULL; + char *text = NULL; + size_t n; + if (FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code, 0, (LPWSTR) &msg, 0, NULL) && msg) + text = to_utf8(msg); + if (msg) LocalFree(msg); + n = text ? strlen(text) : 0; + while (n && (text[n - 1] == '\n' || text[n - 1] == '\r' || text[n - 1] == ' ')) text[--n] = 0; + snprintf(last_error, sizeof(last_error), "%s %s: %s (Windows error %lu)", what, path, + n ? text : "unknown error", (unsigned long) code); + free(text); } -#ifdef __linux__ -static int check_image(struct dl_phdr_info *info, size_t size, void *path) { - return different_jvm(info->dlpi_name, (const char *) path); +/* A return value of at least the buffer size is the size required, not the + length of a complete path, so retry instead of using a truncated path. */ +static wchar_t *full_path(const wchar_t *path) { + DWORD size = GetFullPathNameW(path, 0, NULL, NULL), n; + int tries; + for (tries = 0; size && tries < 3; tries++) { + wchar_t *buf = malloc(sizeof(wchar_t) * size); + if (!buf) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); return NULL; } + n = GetFullPathNameW(path, size, buf, NULL); + if (n && n < size) return buf; + free(buf); + size = n; + } + if (size) SetLastError(ERROR_INSUFFICIENT_BUFFER); + return NULL; } -#endif -#endif - -int djni_load(const char *path) { - last_error[0] = 0; - if (jni_dl) return -1; -#ifdef _WIN32 - wchar_t wide_path[32768], bin_path[32768], full_path[32768], mapped_path[32768], *slash; +/* GetModuleFileNameW returns the buffer size when it truncates. */ +static wchar_t *module_path(HMODULE module) { + DWORD size = MAX_PATH, n; + for (;;) { + wchar_t *buf = malloc(sizeof(wchar_t) * size); + if (!buf) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); return NULL; } + n = GetModuleFileNameW(module, buf, size); + if (n && n < size) return buf; + free(buf); + if (!n) return NULL; + if (size >= 65536) { SetLastError(ERROR_INSUFFICIENT_BUFFER); return NULL; } + size *= 2; + } +} +static HANDLE open_file(const wchar_t *path) { + return CreateFileW(path, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); +} +/* Compare file identities rather than names: 8.3 aliases, junctions, + symbolic links and spelling variants can all name the same file. + Returns 1 for the same file, 0 for different files, -1 if unknown. */ +static int same_file(const wchar_t *a, const wchar_t *b) { + HANDLE ha = open_file(a), hb = open_file(b); + FILE_ID_INFO ia, ib; + BY_HANDLE_FILE_INFORMATION fa, fb; + int res = -1; + if (ha != INVALID_HANDLE_VALUE && hb != INVALID_HANDLE_VALUE) { + if (GetFileInformationByHandleEx(ha, FileIdInfo, &ia, sizeof(ia)) && + GetFileInformationByHandleEx(hb, FileIdInfo, &ib, sizeof(ib))) + res = ia.VolumeSerialNumber == ib.VolumeSerialNumber && + !memcmp(&ia.FileId, &ib.FileId, sizeof(ia.FileId)); + else if (GetFileInformationByHandle(ha, &fa) && GetFileInformationByHandle(hb, &fb)) + res = fa.dwVolumeSerialNumber == fb.dwVolumeSerialNumber && + fa.nFileIndexHigh == fb.nFileIndexHigh && fa.nFileIndexLow == fb.nFileIndexLow; + } else if (hb != INVALID_HANDLE_VALUE) + res = 0; /* the requested file does not exist, but a JVM is mapped */ + if (ha != INVALID_HANDLE_VALUE) CloseHandle(ha); + if (hb != INVALID_HANDLE_VALUE) CloseHandle(hb); + return res; +} +static int load_library(const char *path) { + wchar_t *wide = NULL, *full = NULL, *mapped_path = NULL, *bin = NULL, *slash; HMODULE mapped; - DLL_DIRECTORY_COOKIE cookie = 0; - if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide_path, 32768)) { - save_error(); return -2; + DLL_DIRECTORY_COOKIE cookie = NULL; + int res = -2; + if (!(wide = to_wide(path))) { + save_win_error("Cannot convert JVM library path to UTF-16", path, GetLastError()); + goto done; } - if (!GetFullPathNameW(wide_path, 32768, full_path, NULL)) { - save_error(); return -2; + if (!(full = full_path(wide))) { + save_win_error("Cannot resolve JVM library path", path, GetLastError()); + goto done; } - for (slash = full_path; *slash; slash++) if (*slash == L'/') *slash = L'\\'; - mapped = GetModuleHandleW(L"jvm.dll"); - if (mapped && (!GetModuleFileNameW(mapped, mapped_path, 32768) || lstrcmpiW(full_path, mapped_path))) { - snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped. Use a new R process."); - return -4; + for (slash = full; *slash; slash++) if (*slash == L'/') *slash = L'\\'; + if ((mapped = GetModuleHandleW(L"jvm.dll"))) { + int same = (mapped_path = module_path(mapped)) ? same_file(full, mapped_path) : -1; + char *name = mapped_path ? to_utf8(mapped_path) : NULL; + if (same == 1) { + /* Reference the mapped module itself: loading an alias of its path + must never map a second copy of jvm.dll. */ + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR) mapped, &jni_dl)) + res = 0; + else { + jni_dl = NULL; + save_win_error("Cannot reference the mapped JVM library", name ? name : path, GetLastError()); + } + } else { + if (same) + snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s. Use a new R process.", + path, name ? name : "unknown path"); + else + snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", + name ? name : "unknown path"); + res = -4; + } + free(name); + goto done; } - /* JDK 8 needs its bin directory as well as bin/server for dependent DLLs. */ - wcscpy(bin_path, full_path); - for (slash = bin_path; *slash; slash++) if (*slash == L'/') *slash = L'\\'; - slash = wcsrchr(bin_path, L'\\'); - if (slash) { - *slash = 0; - slash = wcsrchr(bin_path, L'\\'); - if (slash) { *slash = 0; cookie = AddDllDirectory(bin_path); } + /* JDK 8 keeps MSVC runtime DLLs in bin, the parent of bin\server. The + directory is searched only during this load and removed again after + success or failure, leaving the process DLL search state unchanged. */ + if ((bin = malloc(sizeof(wchar_t) * (wcslen(full) + 1)))) { + wcscpy(bin, full); + if ((slash = wcsrchr(bin, L'\\'))) { + *slash = 0; + if ((slash = wcsrchr(bin, L'\\'))) { + *slash = 0; + cookie = AddDllDirectory(bin); + } + } } - jni_dl = LoadLibraryExW(full_path, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); - if (!jni_dl) save_error(); + jni_dl = LoadLibraryExW(full, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (jni_dl) res = 0; + else save_win_error("Cannot load JVM library", path, GetLastError()); if (cookie) RemoveDllDirectory(cookie); +done: + free(wide); free(full); free(mapped_path); free(bin); + return res; +} +const char *djni_mapped(int *vms) { + HMODULE module = NULL; + wchar_t *path; + JNI_GetCreatedJavaVMs_fnptr created; + *vms = -1; + free(mapped_name); + mapped_name = NULL; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, L"jvm.dll", &module)) + return NULL; + if ((created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"))) { + JavaVM *vm; + jsize n = 0; + if (created(&vm, 1, &n) == JNI_OK) *vms = (int) n; + } + if ((path = module_path(module))) { + mapped_name = to_utf8(path); + free(path); + } + return mapped_name; +} #else +static int same_file(const char *a, const char *b) { + struct stat sa, sb; + if (stat(a, &sa) || stat(b, &sb)) return -1; + return sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; +} +static int is_jvm_image(const char *name) { + const char *base = strrchr(name, '/'); + base = base ? base + 1 : name; + return !strcmp(base, "libjvm.dylib") || !strcmp(base, "libjvm.so"); +} +typedef int (*image_fn)(const char *name, void *data); +#ifdef __linux__ +struct phdr_data { image_fn fn; void *data; }; +static int phdr_image(struct dl_phdr_info *info, size_t size, void *data) { + struct phdr_data *d = (struct phdr_data *) data; + return info->dlpi_name ? d->fn(info->dlpi_name, d->data) : 0; +} +#endif +/* Calls fn for each image mapped into the process until it returns nonzero. */ +static int each_image(image_fn fn, void *data) { #ifdef __APPLE__ - uint32_t i; - for (i = 0; i < _dyld_image_count(); i++) - if (different_jvm(_dyld_get_image_name(i), path)) return -4; + uint32_t i, n = _dyld_image_count(); + for (i = 0; i < n; i++) { + const char *name = _dyld_get_image_name(i); + if (name && fn(name, data)) return 1; + } + return 0; #elif defined(__linux__) - if (dl_iterate_phdr(check_image, (void *) path)) return -4; + struct phdr_data d = { fn, data }; + return dl_iterate_phdr(phdr_image, &d); +#else + return 0; +#endif +} +struct jvm_match { const char *requested; char *mapped; }; +/* Device and inode identify the file across symbolic links, hard links and + equivalent spellings; a requested file that cannot be found differs. */ +static int check_mapped(const char *name, void *data) { + struct jvm_match *m = (struct jvm_match *) data; + if (!is_jvm_image(name)) return 0; + if (same_file(name, m->requested) == 1) { + if (!m->mapped) m->mapped = strdup(name); + return 0; + } + snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", name); + return 1; +} +static int find_jvm(const char *name, void *data) { + if (!is_jvm_image(name)) return 0; + *(char **) data = strdup(name); + return 1; +} +static int load_library(const char *path) { + struct jvm_match m = { path, NULL }; + if (each_image(check_mapped, &m)) { + free(m.mapped); + return -4; + } + /* Open a mapped JVM by its own name, so an alias cannot map another copy. */ + jni_dl = dlopen(m.mapped ? m.mapped : path, RTLD_LOCAL | RTLD_NOW); + free(m.mapped); + if (!jni_dl) { + const char *error = dlerror(); + snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); + return -2; + } + return 0; +} +const char *djni_mapped(int *vms) { + char *found = NULL; + *vms = -1; + free(mapped_name); + mapped_name = NULL; + each_image(find_jvm, &found); + if (!found) return NULL; +#ifdef RTLD_NOLOAD + { + /* RTLD_NOLOAD returns a reference only if the image is already mapped. */ + void *handle = dlopen(found, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + if (handle) { + JNI_GetCreatedJavaVMs_fnptr created; + *(void **) &created = dlsym(handle, "JNI_GetCreatedJavaVMs"); + if (created) { + JavaVM *vm; + jsize n = 0; + if (created(&vm, 1, &n) == JNI_OK) *vms = (int) n; + } + dlclose(handle); + } + } #endif - jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW); - if (!jni_dl) save_error(); + mapped_name = found; + return found; +} +#endif + +static int load_sym(const char *sym, void **ptr) { +#ifdef _WIN32 + *ptr = (void *) GetProcAddress(jni_dl, sym); + if (!*ptr) { save_win_error("Cannot find JNI entry point", sym, GetLastError()); return -1; } +#else + const char *error; + *ptr = dlsym(jni_dl, sym); + if (!*ptr) { + error = dlerror(); + snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); + return -1; + } #endif - if (!jni_dl) return -2; + return 0; +} + +/* returns: 0 = success, -1 = already loaded, -2 = load error, -3 = symbols + not found, -4 = another JVM library is already mapped */ +int djni_load(const char *path) { + int res; + last_error[0] = 0; + if (jni_dl) return -1; + if ((res = load_library(path))) return res; if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void **) &JNI_GetDefaultJavaVMInitArgs_fn) || load_sym("JNI_CreateJavaVM", (void **) &JNI_CreateJavaVM_fn) || load_sym("JNI_GetCreatedJavaVMs", (void **) &JNI_GetCreatedJavaVMs_fn)) { @@ -127,9 +348,13 @@ int djni_load(const char *path) { int djni_unload(void) { if (!jni_dl) return -1; #ifdef _WIN32 - if (!FreeLibrary(jni_dl)) { save_error(); return -2; } + if (!FreeLibrary(jni_dl)) { save_win_error("Cannot unload JVM library", "jvm.dll", GetLastError()); return -2; } #else - if (dlclose(jni_dl)) { save_error(); return -2; } + if (dlclose(jni_dl)) { + const char *error = dlerror(); + snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); + return -2; + } #endif jni_dl = 0; clear_symbols(); @@ -147,6 +372,7 @@ jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { return JNI_GetCreatedJavaVMs_fn ? JNI_GetCreatedJavaVMs_fn(vmBuf, bufLen, nVMs) : JNI_NO_DJNI; } + /* define DJNI_MAIN_TEST to test the load + create + print version + destroy + unload cycle it will create an executable - just compile djni.c alone for this (not used in rJava) */ #ifdef DJNI_MAIN_TEST diff --git a/src/djni.h b/src/djni.h index 8abfa7d..70335ad 100644 --- a/src/djni.h +++ b/src/djni.h @@ -16,6 +16,9 @@ int djni_load(const char *path); int djni_unload(void); const char* djni_last_error(void); int djni_loaded(void); +/* path of a JVM library already mapped into the process (or NULL), without + loading one; *vms is the number of created VMs, -1 if unknown */ +const char* djni_mapped(int *vms); jint JNI_GetDefaultJavaVMInitArgs(void *args); jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); From 0375881094f1294cfbb5e9485db89cd22c411cd3 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:02:25 +0200 Subject: [PATCH 15/63] feat: define deferred JVM selection order and attach to an embedding JVM Restore upstream behavior for an invalid explicit Java home: as rJava did on Windows (java.home option or JAVA_HOME) and macOS (JAVA_HOME), a value that is not an existing directory is ignored with the upstream warning and discovery falls back to the registry (Windows), /usr/libexec/java_home (macOS), then java on PATH. Selection still happens only at the first .jinit()/.jloadJVM(); an existing home without a JVM library is an error that leaves the process able to retry. Find the JVM library in known layouts first (lib/server, jre/lib// server for JDK 8 on Linux, jre/lib/server for JDK 8 on macOS, bin/server and jre/bin/server on Windows), then search recursively. If a JVM has already been created in the process, e.g. by a Java application embedding R through JRI, attach to it whatever Java home is configured. That is the only usable runtime, and the behavior of rJava linked against the JVM library. A library mapped without a created VM still rejects a different runtime. --- R/java-runtime.R | 97 ++++++++++++++++++++++++++++++++---------------- R/jinit.R | 23 +++++------- man/jinit.Rd | 19 +++++++++- 3 files changed, 92 insertions(+), 47 deletions(-) diff --git a/R/java-runtime.R b/R/java-runtime.R index 2fb6b89..0111d0a 100644 --- a/R/java-runtime.R +++ b/R/java-runtime.R @@ -1,41 +1,74 @@ -# Extract detection logic to a helper function +## Deferred JVM discovery. This runs at the first .jinit() or .jloadJVM(), +## never at namespace load. The Java home is the first of: +## 1. getOption("java.home") (Windows only, as before) or JAVA_HOME; a value +## that is not an existing directory is ignored with a warning, as rJava +## did on Windows and macOS before deferred loading +## 2. on Windows, the CurrentVersion of the JavaSoft registry keys +## 3. on macOS, /usr/libexec/java_home +## 4. the java.home property reported by `java` on PATH +## An existing Java home without a JVM library is an error, not a fallback. .find.java.runtime <- function() { - # Check options and env vars first - javahome <- if (!is.null(getOption("java.home"))) { - getOption("java.home") - } else { - Sys.getenv("JAVA_HOME") - } - + windows <- .Platform$OS.type == "windows" + javahome <- if (windows && !is.null(getOption("java.home"))) getOption("java.home") else Sys.getenv("JAVA_HOME") + if (!is.character(javahome) || length(javahome) != 1L || is.na(javahome)) javahome <- "" if (nzchar(javahome) && !dir.exists(javahome)) { - stop("Java home setting is invalid: ", javahome) + if (windows) message("java.home option: ", getOption("java.home")) + message("JAVA_HOME environment variable: ", Sys.getenv("JAVA_HOME")) + warning("Java home setting is INVALID, it will be ignored.\nPlease do NOT set it unless you want to override system settings.") + javahome <- "" } + if (!nzchar(javahome) && windows) javahome <- .find.java.registry() + if (!nzchar(javahome) && file.exists("/usr/libexec/java_home")) + javahome <- .java.output("/usr/libexec/java_home") + if (!nzchar(javahome)) + javahome <- .java.output("java", c("-cp", shQuote(system.file("java", package="rJava")), "getsp", "java.home")) + if (nzchar(javahome) && dir.exists(javahome)) javahome else "" +} + +## first line of a command's standard output, or "" if it fails +.java.output <- function(command, args=character()) { + out <- tryCatch(suppressWarnings(system2(command, args, stdout=TRUE, stderr=FALSE)), + error=function(...) character()) + if (!length(out) || !is.null(attr(out, "status")) || is.na(out[1L])) "" else out[1L] +} - # If not found, try Registry - if (!nzchar(javahome)) { - find.java <- function() { - for (root in c("HLM", "HCU")) { - for (key in c( - "Software\\JavaSoft\\JRE", - "Software\\JavaSoft\\JDK", - "Software\\JavaSoft\\Java Runtime Environment", - "Software\\JavaSoft\\Java Development Kit" - )) { - hive <- try( - utils::readRegistry(key, root, 2), - silent = TRUE - ) - if (!inherits(hive, "try-error")) return(hive) - } +.find.java.registry <- function() { + for (root in c("HLM", "HCU")) { + for (key in c( + "Software\\JavaSoft\\JRE", + "Software\\JavaSoft\\JDK", + "Software\\JavaSoft\\Java Runtime Environment", + "Software\\JavaSoft\\Java Development Kit" + )) { + hive <- try(utils::readRegistry(key, root, 2), silent = TRUE) + if (!inherits(hive, "try-error") && length(hive$CurrentVersion)) { + home <- hive[[hive$CurrentVersion]]$JavaHome + if (is.character(home) && length(home) == 1L && nzchar(home)) return(home) } - hive - } - hive <- find.java() - if (!inherits(hive, "try-error") && length(hive$CurrentVersion)) { - this <- hive[[hive$CurrentVersion]] - javahome <- this$JavaHome } } - return(javahome) + "" } +## JVM libraries in a Java home, server VM first. Known layouts are checked +## before a recursive search: lib/server (JDK 9+ on Linux and macOS), +## jre/lib//server (JDK 8 on Linux), jre/lib/server (JDK 8 on macOS), +## bin/server and jre/bin/server (Windows). +.find.jvm.library <- function(home) { + if (.Platform$OS.type == "windows") { + lib <- "jvm.dll" + base <- file.path(home, c("bin", "jre/bin")) + } else { + lib <- if (length(grep("^darwin", R.version$os))) "libjvm.dylib" else "libjvm.so" + base <- file.path(home, c("lib", "jre/lib")) + base <- c(base, list.dirs(base, recursive=FALSE)) + } + found <- c(file.path(base, "server", lib), file.path(base, "client", lib)) + found <- found[file.exists(found)] + if (!length(found)) { + found <- list.files(home, paste0("^", gsub(".", "[.]", lib, fixed=TRUE), "$"), + recursive=TRUE, full.names=TRUE) + found <- c(found[grepl("/server/", found, fixed=TRUE)], found[!grepl("/server/", found, fixed=TRUE)]) + } + found +} diff --git a/R/jinit.R b/R/jinit.R index bdfa79e..9ce9932 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -16,22 +16,17 @@ .Call(RunloadJVM) .jfindAndLoadJVM <- function() { - home <- if (.Platform$OS.type == "windows") .find.java.runtime() else Sys.getenv("JAVA_HOME") - if (!length(home) || !nzchar(home)) { - if (file.exists("/usr/libexec/java_home")) - home <- tryCatch(system("/usr/libexec/java_home", intern=TRUE, ignore.stderr=TRUE), error=function(...) "") - if (!length(home) || !nzchar(home)) - home <- tryCatch(system2("java", c("-cp", shQuote(system.file("java", package="rJava")), "getsp", "java.home"), - stdout=TRUE, stderr=FALSE), error=function(...) "") - } - if (length(home) != 1L || is.na(home) || !nzchar(home) || !dir.exists(home)) + ## A Java application embedding R (e.g. JRI) has already created a JVM. It + ## is the only runtime this process can use, so attach to it whatever Java + ## home is configured, as rJava linked against the JVM library did. + mapped <- .Call(RgetMappedJVM) + if (!is.null(mapped) && isTRUE(mapped$vms > 0L)) + return(.jloadJVM(mapped$path)) + home <- .find.java.runtime() + if (!nzchar(home)) stop("Cannot find Java. Set JAVA_HOME to an existing Java home before initializing the JVM.") - pattern <- if (.Platform$OS.type == "windows") "^jvm[.]dll$" else "^libjvm[.](so|dylib)$" - libjvm <- list.files(home, pattern, recursive=TRUE, full.names=TRUE) + libjvm <- .find.jvm.library(home) if (!length(libjvm)) stop("Cannot find JVM library in ", home) - ## Prefer the server VM when an older JDK also contains a client VM. - server <- grepl("/server/", libjvm, fixed=TRUE) - libjvm <- c(libjvm[server], libjvm[!server]) .jloadJVM(normalizePath(libjvm[1], winslash="/", mustWork=TRUE)) } diff --git a/man/jinit.Rd b/man/jinit.Rd index 4f9d88d..0ccdef4 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -47,7 +47,8 @@ runing JVM, \code{FALSE} otherwise.} \item{state}{string representing the current state of the JVM. One of the following values: - \code{"none"} if there is no JVM, \code{"created"} if the current + \code{"not-loaded"} if no JVM run-time library has been selected yet, + \code{"none"} if a run-time library is loaded, but there is no JVM, \code{"created"} if the current JVM has been created by rJava, \code{"attached"} if rJava attached into an existing JVM (typically when R is embedded into a running JVM via JRI), \code{"detached"} if there is a JVM (such as embedded @@ -93,6 +94,22 @@ than \code{.jinit} and \code{.jvmState} is undefined unless \code{.jvmState()$initialized} is \code{TRUE}. + Loading the rJava namespace does not select or load a JVM run-time. + The first call to \code{.jinit} selects one, in this order: + a JVM already created in the process (e.g. when R is embedded in Java + via JRI), to which rJava attaches regardless of other settings; + otherwise the \code{java.home} option (Windows only) or the + \code{JAVA_HOME} environment variable; then, on Windows, the + \code{CurrentVersion} of the JavaSoft registry keys; on macOS, + \code{/usr/libexec/java_home}; and finally the \code{java} executable + found on \code{PATH}. A Java home setting that is not an existing + directory is ignored with a warning. If no run-time can be found or + loaded, \code{.jinit} fails and can be called again after correcting + the setting. Once a run-time library is loaded, it remains selected + for the life of the R process: a different JVM requires a new R + process. If another JVM library is already mapped into the process, + but no JVM was created in it, only that same library can be selected. + NOTE: \code{.jinit()} can be called implicitly by other functions such as \code{\link{.jpackage}} or \code{\link{J}}. } From 48ef04ef0437c1eba887bdc2d3a801a55c2a7ea2 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:02:25 +0200 Subject: [PATCH 16/63] test: remove obsolete manual Windows smoke script test_rjava_win.R was a manual script requiring an edited JAVA_HOME, and it was shipped at the top level of the source tarball built by mkdist. The native Windows CI scenarios in tests-integration/ supersede it. --- test_rjava_win.R | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 test_rjava_win.R diff --git a/test_rjava_win.R b/test_rjava_win.R deleted file mode 100644 index 9e8944f..0000000 --- a/test_rjava_win.R +++ /dev/null @@ -1,37 +0,0 @@ -# test_rjava_win.R - -# 1. Load the package. -# Expected behavior: Should NOT fail even if JAVA_HOME is unset or invalid initially, -# because we removed the hard link to jvm.dll. -library(rJava) -message("rJava loaded.") - -# 2. Check JVM state -# Expected: "not-loaded" or "none" -print(rJava::.jvmState()) - -# 3. Simulate user setting JAVA_HOME after loading -# REPLACE THIS PATH with a valid JDK path on your Windows machine for testing! -# e.g. "C:/Program Files/Java/jdk-11" -my_java_home <- Sys.getenv("JAVA_HOME") -if (my_java_home == "") { - warning("Please set a valid JAVA_HOME for this test script to run fully.") -} else { - Sys.setenv(JAVA_HOME = my_java_home) - message("JAVA_HOME set to: ", my_java_home) -} - -# 4. Initialize -# Expected: Should find jvm.dll inside the JAVA_HOME set above and load it via LoadLibrary -.jinit() - -# 5. Verify -print(rJava::.jvmState()) -v <- .jcall("java/lang/System", "S", "getProperty", "java.version") -message("Java Version Loaded: ", v) - -if (nzchar(v)) { - message("SUCCESS: JVM loaded dynamically on Windows.") -} else { - stop("FAILURE: JVM did not load.") -} From 24d9a4b0b4028415a8487497c3bf2e37326d8bb7 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:02:53 +0200 Subject: [PATCH 17/63] ci: run upstream's check workflow on this branch again The guard skipped upstream's R CMD check matrix (Zulu 8 and 11 on five runner images including Intel macOS). It adds older-JDK and vendor coverage under upstream's own build path, so keep it running. --- .github/workflows/check.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 6c1f66c..350d708 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -4,7 +4,6 @@ name: check jobs: check: - if: github.ref != 'refs/heads/feature/dynamic-cross-platform' runs-on: ${{ matrix.os }} env: NOAWT: 1 From 2adfce00988807b161ee866cb1e8d6fb6972ad06 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:02:53 +0200 Subject: [PATCH 18/63] test: cover JDK 8-25, vendors, path aliases, fallback, JRI and DLL search Run a bounded matrix per platform: legacy layouts (Temurin 8/11; Zulu 8 on arm64 macOS), current LTS (Temurin 17/21, plus the downstream rJavaEnv packages), and other vendors (Corretto 11, Zulu 25). The build JDK is always Temurin 17. Every scenario starts a fresh R process with PATH reduced to system and R directories and JAVA_HOME set explicitly, so the runner's Java setup cannot mask discovery or dependency handling. New scenarios: bad and over-long paths; same-runtime selection of a preloaded library through symbolic links/junctions, 8.3 names (Windows) and alternative spellings; non-ASCII paths with spaces; the fallback order for an invalid Java home, with the expected home registered under HKCU on Windows; later native loads (zip, net, management, security, mscapi, AWT imaging) with loaded Windows modules logged; JRI started from R; and a Java host embedding R via JRI, where rJava must attach to the host JVM although JAVA_HOME names the other JDK. Compare src/jni.h with each JDK's jni.h (type sizes, invocation struct fields and every function table offset). On Windows, a native harness checks that the bin directory added for jvm.dll's dependencies is no longer searched after failed and successful loads, with a positive control. --- .github/workflows/dynamic.yaml | 111 +++++++++--- tests-integration/JriAttach.java | 38 +++++ tests-integration/deferred.R | 282 ++++++++++++++++++++++++++----- tests-integration/dll-search.c | 62 +++++++ tests-integration/jni-abi.R | 51 ++++++ tests-integration/jri.R | 30 ++++ 6 files changed, 508 insertions(+), 66 deletions(-) create mode 100644 tests-integration/JriAttach.java create mode 100644 tests-integration/dll-search.c create mode 100644 tests-integration/jni-abi.R create mode 100644 tests-integration/jri.R diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 377ff8b..12de70b 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -5,11 +5,23 @@ on: workflow_dispatch: jobs: dynamic: + # Three runtime pairs per platform instead of every OS x vendor x version: + # legacy layouts (JDK 8/11), current LTS (17/21, with downstream packages), + # and other vendors spanning 11 to 25. The build JDK is always Temurin 17. + name: ${{ matrix.os }} ${{ matrix.set }} (${{ matrix.a-dist }} ${{ matrix.a-version }}, ${{ matrix.b-dist }} ${{ matrix.b-version }}) strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] + set: [legacy, current, vendor] + include: + - { set: legacy, a-dist: temurin, a-version: '8', b-dist: temurin, b-version: '11' } + # Temurin has no JDK 8 for macOS on arm64. + - { set: legacy, os: macos-latest, a-dist: zulu } + - { set: current, a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21', downstream: true } + - { set: vendor, a-dist: corretto, a-version: '11', b-dist: zulu, b-version: '25' } runs-on: ${{ matrix.os }} + timeout-minutes: 45 env: NOAWT: '1' steps: @@ -21,19 +33,53 @@ jobs: r-version: release rtools-version: '45' - uses: actions/setup-java@v5 - id: java17 + id: java-a with: - distribution: temurin - java-version: '17' + distribution: ${{ matrix.a-dist }} + java-version: ${{ matrix.a-version }} + - uses: actions/setup-java@v5 + id: java-b + with: + distribution: ${{ matrix.b-dist }} + java-version: ${{ matrix.b-version }} - uses: actions/setup-java@v5 - id: java21 + id: java-build with: distribution: temurin - java-version: '21' + java-version: '17' + - name: Record runtimes + shell: bash + env: + JAVA_A: ${{ steps.java-a.outputs.path }} + JAVA_B: ${{ steps.java-b.outputs.path }} + run: | + set -euo pipefail + for home in "$JAVA_A" "$JAVA_B"; do + echo "== $home"; "$home/bin/java" -version 2>&1 + find "$home" \( -name 'libjvm.*' -o -name jvm.dll \) -print + done + echo "RJE_JAVA_HOME_A=$JAVA_A" >> "$GITHUB_ENV" + echo "RJE_JAVA_HOME_B=$JAVA_B" >> "$GITHUB_ENV" + echo "RJE_FALLBACK_HOME=$JAVA_B" >> "$GITHUB_ENV" + - name: Prepare Windows discovery and path fixtures + if: runner.os == 'Windows' + shell: pwsh + run: | + # Generate 8.3 names for directories created from now on. + fsutil 8dot3name set C: 0 + # Machine-wide JavaSoft keys would take precedence over this user key. + reg query 'HKLM\Software\JavaSoft' /s 2>$null + if ($LASTEXITCODE -eq 0) { Write-Output 'HKLM JavaSoft keys are present' } else { Write-Output 'No HKLM JavaSoft keys' } + reg add 'HKCU\Software\JavaSoft\JDK' /v CurrentVersion /d rjava-test /f + reg add 'HKCU\Software\JavaSoft\JDK\rjava-test' /v JavaHome /d "$env:RJE_JAVA_HOME_B" /f - name: Configure build Java if: runner.os != 'Windows' shell: bash - run: sudo R CMD javareconf + run: | + echo "export PATH=$PATH" > reconf.sh + echo "export JAVA_HOME=$JAVA_HOME" >> reconf.sh + echo "R CMD javareconf" >> reconf.sh + sudo bash reconf.sh - name: Generate distribution and install in isolated library shell: bash run: | @@ -44,16 +90,6 @@ jobs: R CMD INSTALL --library="$RUNNER_TEMP/rjava-lib" "$archive" cp "$archive" "$RUNNER_TEMP/" echo "R_LIBS=$RUNNER_TEMP/rjava-lib" >> "$GITHUB_ENV" - - name: Check deferred loading in fresh processes - shell: bash - env: - RJE_JAVA_HOME_A: ${{ steps.java17.outputs.path }} - RJE_JAVA_HOME_B: ${{ steps.java21.outputs.path }} - run: | - set -euo pipefail - for scenario in select retry unload preloaded; do - Rscript --vanilla tests-integration/deferred.R "$scenario" - done - name: Inspect native JVM linkage shell: bash run: | @@ -68,28 +104,63 @@ jobs: fi cat "$RUNNER_TEMP/linkage.txt" if grep -Ei '(libjvm|DLL Name: jvm.dll)' "$RUNNER_TEMP/linkage.txt"; then exit 1; fi + - name: Compare src/jni.h with the JDK headers + shell: bash + run: Rscript --vanilla tests-integration/jni-abi.R "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" + - name: Check deferred loading in fresh processes + shell: bash + run: | + set -euo pipefail + run() { echo "::group::$*"; Rscript --vanilla tests-integration/deferred.R "$@"; echo "::endgroup::"; } + for scenario in select retry badinput unload preloaded nonascii fallback laterload jengine; do + run "$scenario" + done + run alias link + run alias spelling + if [[ "$RUNNER_OS" == Windows ]]; then run alias short; fi + - name: Embed R in a Java host through JRI + shell: bash + timeout-minutes: 5 + run: Rscript --vanilla tests-integration/jri.R + - name: Check Windows DLL search state after loads + if: runner.os == 'Windows' + shell: bash + run: | + set -euo pipefail + work="$RUNNER_TEMP/dll-search" + server="$(dirname "$(Rscript --vanilla -e 'cat(rJava:::.find.jvm.library(Sys.getenv("RJE_JAVA_HOME_A"))[1])')")" + mkdir -p "$work/missing/bin/server" "$work/copy/bin/server" + cp "$server/jvm.dll" "$work/copy/bin/server/" + cp "$(dirname "$server")"/*.dll "$work/copy/bin/" + marker="$(dirname "$server")/java.dll" + cp "$marker" "$work/missing/bin/rjava_marker.dll" + cp "$marker" "$work/copy/bin/rjava_marker.dll" + gcc -Isrc -o "$work/dll-search.exe" tests-integration/dll-search.c + w() { cygpath -w "$1"; } + PATH="/c/Windows/System32:/c/Windows" "$work/dll-search.exe" \ + "$(w "$work/missing/bin/server/jvm.dll")" "$(w "$work/copy/bin/server/jvm.dll")" "$(w "$work/missing/bin")" - uses: actions/checkout@v4 + if: matrix.downstream with: repository: e-kotov/rJavaEnv ref: 9dd898e6a6d910ecae246c0771b7d5eb270031c4 path: downstream-source - uses: r-lib/actions/setup-r-dependencies@v2 + if: matrix.downstream with: working-directory: downstream-source dependencies: '"hard"' extra-packages: any::callr install-quarto: false - name: Test installed rJavaEnv and downstream packages with dynamic rJava + if: matrix.downstream working-directory: downstream-source shell: Rscript {0} - env: - RJE_JAVA_HOME_A: ${{ steps.java17.outputs.path }} - RJE_JAVA_HOME_B: ${{ steps.java21.outputs.path }} run: source("tools/test-downstream.R") - uses: actions/upload-artifact@v4 if: always() with: - name: dynamic-${{ matrix.os }} + name: dynamic-${{ matrix.os }}-${{ matrix.set }} path: | ${{ runner.temp }}/rJava_*.tar.gz ${{ runner.temp }}/linkage.txt diff --git a/tests-integration/JriAttach.java b/tests-integration/JriAttach.java new file mode 100644 index 0000000..090cec7 --- /dev/null +++ b/tests-integration/JriAttach.java @@ -0,0 +1,38 @@ +import org.rosuda.JRI.REXP; +import org.rosuda.JRI.Rengine; + +/* A Java application hosts the JVM and embeds R through JRI. rJava loaded in + that R must attach to the running JVM, not select the JAVA_HOME runtime. */ +public class JriAttach { + public static void main(String[] args) { + try { + run(); + } catch (Throwable e) { + e.printStackTrace(); + } + System.exit(1); /* the R thread is not a daemon, so always exit */ + } + + static void run() { + Rengine engine = new Rengine(new String[] { "--vanilla", "--no-echo" }, false, null); + if (!engine.waitForR()) { + System.err.println("FAIL: cannot start embedded R"); + System.exit(2); + } + REXP result = engine.eval("tryCatch({" + + " library(rJava); .jinit();" + + " paste('OK', .jvmState()$state, .jcall('java/lang/System', 'S', 'getProperty', 'java.home')," + + " !is.jnull(.jengine()), sep = '|')" + + "}, error = function(e) paste('ERROR', conditionMessage(e), sep = '|'))"); + String text = result == null ? "ERROR|no result" : result.asString(); + String expected = "OK|attached|" + System.getProperty("java.home") + "|TRUE"; + System.out.println("JRI host " + System.getProperty("java.version") + ": " + text); + engine.end(); + if (!expected.equals(text)) { + System.err.println("FAIL: expected " + expected); + return; + } + System.out.println("PASS jri-host"); + System.exit(0); + } +} diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 648d702..b9c7a11 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -1,56 +1,246 @@ -# Each scenario must run in a fresh R process against the installed source build. +# Each scenario must run in a fresh R process against the installed source build: +# Rscript --vanilla tests-integration/deferred.R [variant] +# RJE_JAVA_HOME_A and RJE_JAVA_HOME_B must name two different Java homes. args <- commandArgs(TRUE) scenario <- args[1] +variant <- if (length(args) > 1L) args[2] else "" +windows <- .Platform$OS.type == "windows" home_a <- Sys.getenv("RJE_JAVA_HOME_A") home_b <- Sys.getenv("RJE_JAVA_HOME_B") stopifnot(dir.exists(home_a), dir.exists(home_b)) -Sys.setenv(JAVA_HOME = file.path(tempdir(), "missing-jdk")) -old_path <- Sys.getenv("PATH") +log <- function(...) cat(scenario, variant, ":", ..., "\n") + +# Runner tool setup must not mask missing discovery or dependency handling: +# no Java directory is on PATH unless a scenario adds one. +sysroot <- Sys.getenv("SystemRoot", "C:\\Windows") +minimal_path <- if (windows) { + paste(normalizePath(c(R.home("bin"), file.path(sysroot, "System32"), sysroot, + file.path(sysroot, "System32", "Wbem")), winslash = "\\"), collapse = ";") +} else "/usr/bin:/bin" +Sys.setenv(PATH = minimal_path, JAVA_HOME = file.path(tempdir(), "missing-jdk")) +Sys.unsetenv(c("CLASSPATH", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS")) +options(java.parameters = "-Djava.awt.headless=true") ns <- loadNamespace("rJava") -state <- function() get(".jvmState", ns)() +internal <- function(name) get(name, ns) +state <- function() internal(".jvmState")() stopifnot(identical(state()$state, "not-loaded"), !state()$initialized, - identical(old_path, Sys.getenv("PATH"))) + identical(minimal_path, Sys.getenv("PATH"))) + lib <- function(home) { - pattern <- if (.Platform$OS.type == "windows") "^jvm[.]dll$" else "^libjvm[.](so|dylib)$" - paths <- list.files(home, pattern, recursive = TRUE, full.names = TRUE) - paths <- paths[grepl("/server/", paths, fixed = TRUE)] - stopifnot(length(paths) >= 1L) + paths <- internal(".find.jvm.library")(home) + stopifnot(length(paths) >= 1L, grepl("/server/", paths[1], fixed = TRUE)) normalizePath(paths[1], winslash = "/") } -if (scenario == "preloaded") { - # Detect a JVM library loaded outside this loader, without starting a VM. - dyn.load(lib(home_a)) - Sys.setenv(JAVA_HOME = home_b) - failed <- try(rJava::.jinit(), silent = TRUE) - stopifnot(inherits(failed, "try-error"), grepl("already mapped", failed)) - stopifnot(!state()$initialized) -} else if (scenario == "retry") { - failed <- try(rJava::.jinit(), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") - # A native library with no JNI exports must fail cleanly and permit retry. - loadNamespace("stats") - wrong <- getLoadedDLLs()[["stats"]][["path"]] - failed <- try(get(".jloadJVM", ns)(wrong), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$state == "not-loaded") -} else if (scenario == "unload") { - get(".jloadJVM", ns)(lib(home_a)) - stopifnot(state()$state == "none", !state()$initialized) - failed <- try(get(".junloadJVM", ns)(), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$state == "none") - failed <- try(get(".jloadJVM", ns)(lib(home_b)), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$state == "none") -} else stopifnot(scenario == "select") -selected <- if (scenario %in% c("unload", "preloaded")) home_a else home_b -Sys.setenv(JAVA_HOME = selected) -rJava::.jinit() -stopifnot(state()$initialized, state()$state == "created") -actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") -stopifnot(identical(normalizePath(actual), normalizePath(selected))) -# Exercise JNI signatures, references, arrays and exceptions, beyond initialization. -obj <- rJava::.jnew("java/lang/String", "deferred") -stopifnot(rJava::.jcall(obj, "I", "length") == 8L, - identical(rJava::.jevalArray(rJava::.jarray(1:3)), 1:3)) -failed <- try(get(".junloadJVM", ns)(), silent = TRUE) -stopifnot(inherits(failed, "try-error"), state()$state == "created", - rJava::.jcall(obj, "I", "length") == 8L) -cat("PASS", scenario, rJava::.jcall("java/lang/System", "S", "getProperty", "java.version"), "\n") +relative <- function(home) substring(internal(".find.jvm.library")(home)[1], nchar(home) + 2L) +same_home <- function(actual, expected) { + # JDK 8 reports its jre subdirectory as java.home. + actual <- normalizePath(actual, winslash = "/") + expected <- normalizePath(expected, winslash = "/") + identical(actual, expected) || identical(dirname(actual), expected) +} +fails <- function(expr, pattern = NULL) { + path <- Sys.getenv("PATH") + result <- suppressWarnings(try(expr, silent = TRUE)) + stopifnot(inherits(result, "try-error"), state()$state == "not-loaded", + identical(path, Sys.getenv("PATH"))) + if (!is.null(pattern) && !grepl(pattern, result)) + stop("unexpected error: ", result) + log("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L)) +} +# Map a JVM library as other native code in the process would, outside rJava. +preload <- function(path) { + old <- Sys.getenv("PATH") + # JDK 8 jvm.dll needs the C runtime from its bin directory. + if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(path)), winslash = "\\"), old, sep = ";")) + dyn.load(path, local = TRUE, now = TRUE) + Sys.setenv(PATH = old) +} +# A directory name with spaces and non-ASCII characters when the locale allows it. +odd_dir <- function() { + name <- if (isTRUE(l10n_info()[["UTF-8"]])) "rJava t\u00ebst \u00fc \u65e5\u672c" else "rJava test dir" + dir <- file.path(if (windows) "C:/rjava-paths" else tempdir(), name) + dir.create(dir, recursive = TRUE, showWarnings = FALSE) + stopifnot(dir.exists(dir)) + dir +} +# A second name for a Java home: a junction on Windows, a symbolic link elsewhere. +link_home <- function(home, dir = odd_dir(), name = "jdk link") { + link <- file.path(dir, name) + unlink(link) + ok <- if (windows) Sys.junction(normalizePath(home), link) else file.symlink(normalizePath(home), link) + stopifnot(isTRUE(ok), dir.exists(link)) + link +} +start <- function(expected) { + rJava::.jinit() + stopifnot(state()$initialized, state()$state %in% c("created", "attached")) + actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") + if (!same_home(actual, expected)) stop("java.home is ", actual, ", expected ", expected) + # Exercise JNI signatures, references, arrays and exceptions, beyond initialization. + obj <- rJava::.jnew("java/lang/String", "deferred") + stopifnot(rJava::.jcall(obj, "I", "length") == 8L, + identical(rJava::.jevalArray(rJava::.jarray(1:3)), 1:3), + identical(rJava::.jcall("java/lang/Long", "J", "parseLong", "-4294967297"), -4294967297), + identical(rJava::.jcall("java/lang/Math", "D", "scalb", 1.5, 2L), 6)) + thrown <- try(rJava::.jcall("java/lang/Integer", "I", "parseInt", "x"), silent = TRUE) + stopifnot(inherits(thrown, "try-error"), grepl("NumberFormatException", thrown)) + failed <- try(internal(".junloadJVM")(), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$initialized) + version <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.version") + log("started", version, "at", actual) + invisible(actual) +} +# Native libraries the JVM loads after startup, as real workloads would. +later_loads <- function() { + J <- function(...) rJava::.jcall(...) + deflater <- rJava::.jnew("java/util/zip/Deflater") + J(deflater, "V", "setInput", rJava::.jarray(as.raw(1:100))) + J(deflater, "V", "finish") + stopifnot(J(deflater, "I", "deflate", rJava::.jarray(raw(200))) > 0L) + address <- J("java/net/InetAddress", "Ljava/net/InetAddress;", "getByName", "127.0.0.1") + socket <- rJava::.jnew("java/net/ServerSocket", 0L, 1L, address) + stopifnot(J(socket, "I", "getLocalPort") > 0L) + J(socket, "V", "close") + digest <- J("java/security/MessageDigest", "Ljava/security/MessageDigest;", "getInstance", "SHA-256") + stopifnot(length(J(digest, "[B", "digest", rJava::.jarray(as.raw(1:3)))) == 32L) + bean <- J("java/lang/management/ManagementFactory", "Ljava/lang/management/RuntimeMXBean;", "getRuntimeMXBean") + stopifnot(J(bean, "J", "getUptime") >= 0) + if (windows) { + store <- J("java/security/KeyStore", "Ljava/security/KeyStore;", "getInstance", "Windows-MY") + J(store, "V", "load", rJava::.jnull("java/security/KeyStore$LoadStoreParameter")) + } + image <- rJava::.jnew("java/awt/image/BufferedImage", 40L, 20L, 1L) + graphics <- J(image, "Ljava/awt/Graphics2D;", "createGraphics") + J(graphics, "V", "drawString", "rJava", 2L, 15L) + out <- rJava::.jnew("java/io/ByteArrayOutputStream") + stopifnot(J("javax/imageio/ImageIO", "Z", "write", rJava::.jcast(image, "java/awt/image/RenderedImage"), + "png", rJava::.jcast(out, "java/io/OutputStream")), J(out, "I", "size") > 0L) + log("later native loads passed") + if (windows) { + ps <- file.path(sysroot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") + modules <- system2(ps, c("-NoProfile", "-Command", + shQuote(sprintf("(Get-Process -Id %d).Modules | ForEach-Object FileName", Sys.getpid()))), stdout = TRUE) + keep <- grepl("(java|jdk|jre|zulu|corretto|temurin|vcruntime|msvc|ucrtbase)", modules, ignore.case = TRUE) + cat("Loaded modules:", modules[keep], sep = "\n ") + } +} + +switch(scenario, + select = { + Sys.setenv(JAVA_HOME = home_b) + start(home_b) + stopifnot(identical(minimal_path, Sys.getenv("PATH"))) + }, + retry = { + empty <- file.path(tempdir(), "home without jvm") + dir.create(empty) + Sys.setenv(JAVA_HOME = empty) + fails(rJava::.jinit(), "Cannot find JVM library") + fails(internal(".jloadJVM")(file.path(empty, "server", basename(lib(home_a))))) + # A native library with no JNI exports must fail cleanly and permit retry. + loadNamespace("stats") + fails(internal(".jloadJVM")(getLoadedDLLs()[["stats"]][["path"]]), "entry points") + Sys.setenv(JAVA_HOME = home_b) + start(home_b) + }, + badinput = { + load <- internal(".jloadJVM") + fails(load("")) + fails(load(c(lib(home_a), lib(home_b))), "Invalid path") + fails(load(home_a)) + fails(load(strrep("a", 40000L))) + # A relative path that only exceeds the Windows path limit once completed. + fails(load(paste0(strrep("b", 32700L), "/jvm.dll"))) + fails(load(file.path(tempdir(), strrep("c", 300L), "server", basename(lib(home_a))))) + Sys.setenv(JAVA_HOME = home_b) + start(home_b) + }, + unload = { + internal(".jloadJVM")(lib(home_a)) + stopifnot(state()$state == "none", !state()$initialized) + failed <- try(internal(".junloadJVM")(), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$state == "none") + failed <- try(internal(".jloadJVM")(lib(home_b)), silent = TRUE) + stopifnot(inherits(failed, "try-error"), state()$state == "none") + Sys.setenv(JAVA_HOME = home_a) + start(home_a) + }, + preloaded = { + # A JVM library mapped outside this loader rejects a different runtime and + # permits the same one, without starting a VM. + preload(lib(home_a)) + mapped <- .Call(internal("RgetMappedJVM")) + stopifnot(!is.null(mapped), identical(mapped$vms, 0L)) + Sys.setenv(JAVA_HOME = home_b) + fails(rJava::.jinit(), "already mapped") + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") + Sys.setenv(JAVA_HOME = home_a) + start(home_a) + }, + alias = { + # The mapped library must be recognized by file identity, not by name. + real <- lib(home_a) + path <- switch(variant, + link = file.path(link_home(home_a), relative(home_a)), + short = { + stopifnot(windows) + long <- file.path(link_home(home_a, name = "jdk long directory name"), relative(home_a)) + short <- utils::shortPathName(long) + if (identical(tolower(gsub("\\\\", "/", short)), tolower(long))) stop("no 8.3 name was generated for ", long) + short + }, + spelling = if (windows) { + chartr("/", "\\", toupper(file.path(dirname(dirname(real)), ".", "server", "..", "server", basename(real)))) + } else file.path(dirname(dirname(real)), ".", "server", "..", "server//", basename(real)), + stop("unknown alias variant")) + log("alias", path, "of", real) + preload(real) + internal(".jloadJVM")(path) + stopifnot(state()$state == "none") + start(home_a) + }, + nonascii = { + # No preload: the loader itself must open a path with spaces and non-ASCII names. + Sys.setenv(JAVA_HOME = link_home(home_b)) + start(home_b) + }, + fallback = { + # An invalid explicit home is ignored with a warning, as before deferred + # loading, and discovery falls back. Expected homes are computed outside rJava. + # Windows: the CI job registers RJE_FALLBACK_HOME under HKCU\Software\JavaSoft. + # macOS: /usr/libexec/java_home if it finds a runtime. Otherwise: java on PATH. + expected <- Sys.getenv("RJE_FALLBACK_HOME") + java_home <- if (file.exists("/usr/libexec/java_home")) + suppressWarnings(system2("/usr/libexec/java_home", stdout = TRUE, stderr = FALSE)) + if (length(java_home) == 1L && is.null(attr(java_home, "status")) && dir.exists(java_home)) { + expected <- java_home + } else if (!windows) Sys.setenv(PATH = paste(file.path(expected, "bin"), minimal_path, sep = ":")) + stopifnot(dir.exists(expected)) + log("expecting fallback to", expected) + warned <- FALSE + withCallingHandlers(rJava::.jinit(), warning = function(w) { + if (grepl("INVALID", conditionMessage(w))) { warned <<- TRUE; invokeRestart("muffleWarning") } + }) + stopifnot(warned) + start(expected) + }, + laterload = { + Sys.setenv(JAVA_HOME = home_a) + start(home_a) + later_loads() + stopifnot(identical(minimal_path, Sys.getenv("PATH"))) + }, + jengine = { + # R hosts the JVM, then JRI is loaded into it as a later native library. + jri <- system.file("jri", package = "rJava") + if (windows && dir.exists(file.path(jri, "x64"))) jri <- file.path(jri, "x64") + options(java.parameters = c("-Djava.awt.headless=true", paste0("-Djava.library.path=", jri))) + Sys.setenv(JAVA_HOME = home_a) + start(home_a) + engine <- rJava::.jengine(TRUE) + stopifnot(!rJava::is.jnull(engine)) + stopifnot(identical(rJava::.jcall(engine, "Z", "isStandAlone"), FALSE)) + log("JRI engine started inside R") + }, + stop("unknown scenario: ", scenario)) +cat("PASS", scenario, variant, "\n") diff --git a/tests-integration/dll-search.c b/tests-integration/dll-search.c new file mode 100644 index 0000000..01d5351 --- /dev/null +++ b/tests-integration/dll-search.c @@ -0,0 +1,62 @@ +/* Windows only: the JVM bin directory may be added to the DLL search path + only while djni_load() loads jvm.dll, and must be removed again after both + failed and successful loads. Build and run with a stripped PATH: + gcc -Isrc -o dll-search.exe tests-integration/dll-search.c + dll-search.exe + Both bin directories contain rjava_marker.dll, which is nowhere else. */ +#include "../src/djni.c" + +static int failures; + +static void check(int ok, const char *what) { + printf("%s: %s\n", ok ? "ok" : "FAIL", what); + if (!ok) failures++; +} +/* Searches only directories added with AddDllDirectory. */ +static int marker_visible(void) { + HMODULE h = LoadLibraryExW(L"rjava_marker.dll", NULL, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_SEARCH_USER_DIRS); + if (h) FreeLibrary(h); + return h != NULL; +} + +int main(int argc, char **argv) { + DLL_DIRECTORY_COOKIE cookie; + wchar_t *bin; + char *path; + const char *mapped; + int vms, res; + if (argc != 4) { fprintf(stderr, "usage: dll-search \n"); return 2; } + check(!marker_visible(), "marker is not visible initially"); + bin = to_wide(argv[3]); + cookie = bin ? AddDllDirectory(bin) : NULL; + check(cookie && marker_visible(), "control: an added directory is searched"); + if (cookie) RemoveDllDirectory(cookie); + check(!marker_visible(), "control: a removed directory is not searched"); + + res = djni_load(argv[1]); + printf("missing library: %d %s\n", res, djni_last_error() ? djni_last_error() : ""); + check(res == -2 && !djni_loaded(), "a missing jvm.dll fails cleanly"); + check(!marker_visible(), "bin is not searched after a failed load"); + + /* Relative input that only exceeds the path limit once completed. */ + path = malloc(32800); + memset(path, 'b', 32700); + strcpy(path + 32700, "\\server\\jvm.dll"); + res = djni_load(path); + printf("over-long path: %d %.120s\n", res, djni_last_error() ? djni_last_error() : ""); + check(res == -2 && !djni_loaded(), "an over-long path fails cleanly"); + free(path); + + check(!djni_mapped(&vms), "no jvm.dll is mapped before loading"); + res = djni_load(argv[2]); + printf("copied library: %d %s\n", res, djni_last_error() ? djni_last_error() : ""); + check(res == 0 && djni_loaded(), "a jvm.dll with dependencies in bin loads"); + check(!marker_visible(), "bin is not searched after a successful load"); + mapped = djni_mapped(&vms); + printf("mapped: %s, %d VMs\n", mapped ? mapped : "(none)", vms); + check(mapped && vms == 0, "the loaded jvm.dll is reported without a VM"); + free(bin); + if (failures) { printf("FAIL dll-search: %d failures\n", failures); return 1; } + printf("PASS dll-search\n"); + return 0; +} diff --git a/tests-integration/jni-abi.R b/tests-integration/jni-abi.R new file mode 100644 index 0000000..ed24c21 --- /dev/null +++ b/tests-integration/jni-abi.R @@ -0,0 +1,51 @@ +# rJava compiles against its own src/jni.h, not the JDK headers. Check that its +# types and function tables have the layout of each JDK's jni.h: +# Rscript --vanilla tests-integration/jni-abi.R [ ...] +homes <- commandArgs(TRUE) +stopifnot(length(homes) >= 1L, all(dir.exists(homes))) +windows <- .Platform$OS.type == "windows" +quote <- function(x) shQuote(x, type = if (windows) "cmd" else "sh") +header <- readLines(file.path("src", "jni.h")) +members <- function(struct) { + start <- grep(paste0("^struct ", struct, " \\{"), header) + end <- start + grep("^\\};", header[-seq_len(start)])[1] + body <- header[start:end] + c(sub(".*void \\*(reserved[0-9]+);.*", "\\1", grep("void \\*reserved[0-9]+;", body, value = TRUE)), + sub(".*\\(JNICALL \\*([A-Za-z0-9_]+)\\).*", "\\1", grep("JNICALL \\*", body, value = TRUE))) +} +offsets <- function(struct) sprintf(' printf("%s.%%s %%lu\\n", "%s", (unsigned long) offsetof(struct %s, %s));', + struct, members(struct), struct, members(struct)) +types <- c("jbyte", "jboolean", "jchar", "jshort", "jint", "jlong", "jfloat", "jdouble", "jsize", + "jvalue", "jobject", "jfieldID", "jmethodID", "JavaVMOption", "JavaVMInitArgs", "JavaVMAttachArgs") +fields <- c("JavaVMInitArgs, version", "JavaVMInitArgs, nOptions", "JavaVMInitArgs, options", + "JavaVMInitArgs, ignoreUnrecognized", "JavaVMOption, extraInfo", "JavaVMAttachArgs, group") +probe <- c("#include ", "#include ", "#include ", "int main(void) {", + sprintf(' printf("sizeof %s %%lu\\n", (unsigned long) sizeof(%s));', types, types), + sprintf(' printf("offsetof %s %%lu\\n", (unsigned long) offsetof(%s));', fields, fields), + offsets("JNIInvokeInterface_"), offsets("JNINativeInterface_"), + ' printf("JNI_VERSION_1_2 %x\\n", JNI_VERSION_1_2);', " return 0;", "}") +dir <- tempfile("jni-abi-") +dir.create(dir) +src <- file.path(dir, "probe.c") +writeLines(probe, src) +cc <- system2(file.path(R.home("bin"), "R"), c("CMD", "config", "CC"), stdout = TRUE) +run <- function(name, includes) { + exe <- file.path(dir, paste0("probe-", name, if (windows) ".exe" else "")) + command <- paste(cc, paste0("-I", quote(includes), collapse = " "), "-o", quote(exe), quote(src)) + if (system(command) != 0L) stop("cannot compile the probe with ", paste(includes, collapse = ", ")) + system2(exe, stdout = TRUE) +} +# A directory holding only rJava's header, so that no JDK header can be picked up. +own <- file.path(dir, "own") +dir.create(own) +stopifnot(file.copy(file.path("src", "jni.h"), own)) +expected <- run("own", own) +stopifnot(length(expected) == length(types) + length(fields) + 1L + + length(members("JNIInvokeInterface_")) + length(members("JNINativeInterface_"))) +for (home in homes) { + include <- file.path(home, "include") + actual <- run(basename(tempfile("jdk")), c(include, list.dirs(include, recursive = FALSE))) + different <- setdiff(expected, actual) + if (length(different)) stop("JNI layout differs from ", home, ":\n", paste(different, collapse = "\n")) + cat("JNI layout of src/jni.h matches", home, "(", length(expected), "entries )\n") +} diff --git a/tests-integration/jri.R b/tests-integration/jri.R new file mode 100644 index 0000000..32d94d8 --- /dev/null +++ b/tests-integration/jri.R @@ -0,0 +1,30 @@ +# Runs a Java host (JDK A) embedding R through JRI, with JAVA_HOME naming JDK B: +# Rscript --vanilla tests-integration/jri.R +windows <- .Platform$OS.type == "windows" +home_a <- Sys.getenv("RJE_JAVA_HOME_A") +home_b <- Sys.getenv("RJE_JAVA_HOME_B") +stopifnot(dir.exists(home_a), dir.exists(home_b)) +jri <- system.file("jri", package = "rJava") +jar <- file.path(jri, "JRI.jar") +native <- if (windows && dir.exists(file.path(jri, "x64"))) file.path(jri, "x64") else jri +stopifnot(file.exists(jar), length(list.files(native, "jri[.](dll|so|jnilib)$")) == 1L) +classes <- file.path(tempdir(), "classes") +dir.create(classes) +exe <- function(home, tool) file.path(home, "bin", if (windows) paste0(tool, ".exe") else tool) +source_file <- file.path(getwd(), "tests-integration", "JriAttach.java") +stopifnot(system2(exe(home_a, "javac"), c("-cp", shQuote(jar), "-d", shQuote(classes), shQuote(source_file))) == 0L) +# The embedded R needs its own libraries, but no Java directory, on the search path. +sysroot <- Sys.getenv("SystemRoot", "C:\\Windows") +Sys.setenv( + JAVA_HOME = home_b, + R_HOME = R.home(), + R_LIBS = paste(.libPaths(), collapse = .Platform$path.sep), + PATH = if (windows) paste(normalizePath(c(R.home("bin"), file.path(sysroot, "System32"), sysroot), + winslash = "\\"), collapse = ";") else "/usr/bin:/bin") +if (Sys.info()[["sysname"]] == "Linux") Sys.setenv(LD_LIBRARY_PATH = file.path(R.home(), "lib")) +# A Java host provides rJava's class loader on its class path: JDK 9+ does not +# permit rJava to append it to a running application class loader. +boot <- file.path(system.file("java", package = "rJava"), "boot") +status <- system2(exe(home_a, "java"), c(paste0("-Djava.library.path=", shQuote(native)), + "-cp", shQuote(paste(jar, boot, classes, sep = .Platform$path.sep)), "JriAttach")) +stopifnot(status == 0L) From 5f1a70580803bdda8c647996530dc5cb8a43441a Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:14:10 +0200 Subject: [PATCH 19/63] fix: keep rJava's JNI calls bound to djni under symbol interposition djni defines wrappers named JNI_CreateJavaVM, JNI_GetCreatedJavaVMs and JNI_GetDefaultJavaVMInitArgs. On Linux, when a JVM library is already loaded with global symbols (the java launcher does this in a Java application embedding R through JRI), ELF interposition bound rJava's own calls to libjvm's exports instead: rJava saw a running VM, skipped selection, and .jinit() then failed with "No Java run-time is loaded". Give the wrappers hidden visibility so references bind within rJava.so. macOS two-level namespaces and Windows imports were not affected. --- src/djni.h | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/djni.h b/src/djni.h index 70335ad..0dbdc29 100644 --- a/src/djni.h +++ b/src/djni.h @@ -20,8 +20,18 @@ int djni_loaded(void); loading one; *vms is the number of created VMs, -1 if unknown */ const char* djni_mapped(int *vms); -jint JNI_GetDefaultJavaVMInitArgs(void *args); -jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); -jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs); +/* The wrappers below have the names of the JVM exports. Hide them, so that + calls from rJava bind to the wrappers even if a JVM library was loaded + with global symbols, as by the java launcher of an application + embedding R; ELF symbol interposition would otherwise bypass djni. */ +#if defined(__GNUC__) && !defined(_WIN32) +#define DJNI_LOCAL __attribute__((visibility("hidden"))) +#else +#define DJNI_LOCAL +#endif + +DJNI_LOCAL jint JNI_GetDefaultJavaVMInitArgs(void *args); +DJNI_LOCAL jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args); +DJNI_LOCAL jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs); #endif From 0eacc0462d4b7baefc48e92d193906c9c9bb2ea1 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:14:10 +0200 Subject: [PATCH 20/63] fix(windows): build JRI from its src/jri location Pre-existing since JRI moved to src/jri in 1.0-15 (e28ebb6): configure.win still tested jri/configure.win, so Windows source installs silently built no JRI. Use the src/jri paths. Needed to test JRI embedding on Windows; independent of deferred loading. --- configure.win | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/configure.win b/configure.win index f72c921..841e28a 100644 --- a/configure.win +++ b/configure.win @@ -22,7 +22,7 @@ echo " JAVA_HOME=$JAVA_HOME" echo "JAVA_HOME:=$JAVA_HOME" > src/Makevars.java -if [ -e jri/configure.win ]; then +if [ -e src/jri/configure.win ]; then echo "=== Building JRI ===" CONFIGURED=1 export CONFIGURED @@ -33,21 +33,21 @@ if [ -e jri/configure.win ]; then export R_HOME export RHOME export JAVA_HOME - cd jri + cd src/jri sh configure.win make BR=$? - cd .. + cd ../.. if [ $BR = 0 ]; then echo "=== JRI built ===" mkdir -p inst/jri # also copy into R_ARCH is present to avoid overwriting different archs if [ -n "${R_ARCH}" ]; then mkdir -p inst/jri${R_ARCH} - cp jri/src/jri.dll inst/jri${R_ARCH}/ + cp src/jri/src/jri.dll inst/jri${R_ARCH}/ fi # yet still install into JRI in case users get confused - cp -r jri/run.bat jri/src/jri.dll jri/src/JRI.jar jri/examples inst/jri/ + cp -r src/jri/run.bat src/jri/src/jri.dll src/jri/src/JRI.jar src/jri/examples inst/jri/ else echo "**** WARNING: JRI could NOT be built" >&2 if [ -z "$IGNORE" ]; then From 2d1f9ae2f5626043790d4af38c6787f846963ea0 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:14:10 +0200 Subject: [PATCH 21/63] test: preload a JVM library with global symbols before selection Reproduces, inside an R process, the symbol interposition a Java host causes on Linux. --- .github/workflows/dynamic.yaml | 1 + tests-integration/deferred.R | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 12de70b..4fb6163 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -115,6 +115,7 @@ jobs: for scenario in select retry badinput unload preloaded nonascii fallback laterload jengine; do run "$scenario" done + run preloaded global run alias link run alias spelling if [[ "$RUNNER_OS" == Windows ]]; then run alias short; fi diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index b9c7a11..3ebc455 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -48,11 +48,11 @@ fails <- function(expr, pattern = NULL) { log("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L)) } # Map a JVM library as other native code in the process would, outside rJava. -preload <- function(path) { +preload <- function(path, local = TRUE) { old <- Sys.getenv("PATH") # JDK 8 jvm.dll needs the C runtime from its bin directory. if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(path)), winslash = "\\"), old, sep = ";")) - dyn.load(path, local = TRUE, now = TRUE) + dyn.load(path, local = local, now = TRUE) Sys.setenv(PATH = old) } # A directory name with spaces and non-ASCII characters when the locale allows it. @@ -167,8 +167,10 @@ switch(scenario, }, preloaded = { # A JVM library mapped outside this loader rejects a different runtime and - # permits the same one, without starting a VM. - preload(lib(home_a)) + # permits the same one, without starting a VM. With global symbols, its + # JNI exports must not interpose on rJava's own loader wrappers. + stopifnot(variant %in% c("", "global")) + preload(lib(home_a), local = variant != "global") mapped <- .Call(internal("RgetMappedJVM")) stopifnot(!is.null(mapped), identical(mapped$vms, 0L)) Sys.setenv(JAVA_HOME = home_b) From a7e300ab01b803cedca353ba9220e1a85af976e3 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:19:44 +0200 Subject: [PATCH 22/63] Revert "fix(windows): build JRI from its src/jri location" Correcting configure.win only exposes a second pre-existing failure: src/jri/Rdecl.h includes , which R does not ship on Windows. Windows source installs keep building without JRI, as on upstream master. This reverts commit 0eacc0462d4b7baefc48e92d193906c9c9bb2ea1. --- configure.win | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/configure.win b/configure.win index 841e28a..f72c921 100644 --- a/configure.win +++ b/configure.win @@ -22,7 +22,7 @@ echo " JAVA_HOME=$JAVA_HOME" echo "JAVA_HOME:=$JAVA_HOME" > src/Makevars.java -if [ -e src/jri/configure.win ]; then +if [ -e jri/configure.win ]; then echo "=== Building JRI ===" CONFIGURED=1 export CONFIGURED @@ -33,21 +33,21 @@ if [ -e src/jri/configure.win ]; then export R_HOME export RHOME export JAVA_HOME - cd src/jri + cd jri sh configure.win make BR=$? - cd ../.. + cd .. if [ $BR = 0 ]; then echo "=== JRI built ===" mkdir -p inst/jri # also copy into R_ARCH is present to avoid overwriting different archs if [ -n "${R_ARCH}" ]; then mkdir -p inst/jri${R_ARCH} - cp src/jri/src/jri.dll inst/jri${R_ARCH}/ + cp jri/src/jri.dll inst/jri${R_ARCH}/ fi # yet still install into JRI in case users get confused - cp -r src/jri/run.bat src/jri/src/jri.dll src/jri/src/JRI.jar src/jri/examples inst/jri/ + cp -r jri/run.bat jri/src/jri.dll jri/src/JRI.jar jri/examples inst/jri/ else echo "**** WARNING: JRI could NOT be built" >&2 if [ -z "$IGNORE" ]; then From 41e86730cfba084001f0f9c295998cce5523abde Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:20:51 +0200 Subject: [PATCH 23/63] test: attach to a JVM created by native code, and hide the system VC++ runtime A helper compiled with R CMD SHLIB creates a JVM in the R process before rJava is used (with global symbols on POSIX), so attaching to a created JVM is tested on Windows too, where JRI is not built from source. On Windows, hide System32's VC++ runtime DLLs: jvm.dll and its startup libraries must load from the JDK alone. Record whether later loads that import another runtime DLL still work in that environment. --- .github/workflows/dynamic.yaml | 43 +++++++++++++++++++++++++++++++++- tests-integration/deferred.R | 19 +++++++++++++++ tests-integration/jvmhost.c | 33 ++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests-integration/jvmhost.c diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 4fb6163..2cb3c59 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -107,19 +107,32 @@ jobs: - name: Compare src/jni.h with the JDK headers shell: bash run: Rscript --vanilla tests-integration/jni-abi.R "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" + - name: Build a native helper that creates a JVM outside rJava + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/jvmhost" + cp tests-integration/jvmhost.c "$RUNNER_TEMP/jvmhost/" + src="$PWD/src" + if [[ "$RUNNER_OS" == Windows ]]; then src="$(cygpath -m "$src")"; fi + (cd "$RUNNER_TEMP/jvmhost" && PKG_CPPFLAGS="-I$src" R CMD SHLIB jvmhost.c) + echo "RJE_JVMHOST=$(ls "$RUNNER_TEMP"/jvmhost/jvmhost.{so,dll} 2>/dev/null)" >> "$GITHUB_ENV" - name: Check deferred loading in fresh processes shell: bash run: | set -euo pipefail run() { echo "::group::$*"; Rscript --vanilla tests-integration/deferred.R "$@"; echo "::endgroup::"; } - for scenario in select retry badinput unload preloaded nonascii fallback laterload jengine; do + for scenario in select retry badinput unload preloaded nonascii fallback laterload hosted; do run "$scenario" done + # Windows source installs build no JRI (pre-existing upstream build defects). + if [[ "$RUNNER_OS" != Windows ]]; then run jengine; fi run preloaded global run alias link run alias spelling if [[ "$RUNNER_OS" == Windows ]]; then run alias short; fi - name: Embed R in a Java host through JRI + if: runner.os != 'Windows' shell: bash timeout-minutes: 5 run: Rscript --vanilla tests-integration/jri.R @@ -140,6 +153,34 @@ jobs: w() { cygpath -w "$1"; } PATH="/c/Windows/System32:/c/Windows" "$work/dll-search.exe" \ "$(w "$work/missing/bin/server/jvm.dll")" "$(w "$work/copy/bin/server/jvm.dll")" "$(w "$work/missing/bin")" + - name: Start Java without the system VC++ runtime (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + # Runner images install the VC++ runtime in System32, which masks + # whether JVM dependencies resolve from the JDK. Hide it: jvm.dll and + # its startup libraries must still load. Later loads that import + # another runtime DLL (awt.dll -> MSVCP140.dll) are recorded only. + $hidden = @() + foreach ($f in 'msvcp140.dll', 'vcruntime140.dll', 'vcruntime140_1.dll') { + $p = Join-Path $env:SystemRoot "System32\$f" + if (Test-Path $p) { + takeown /f $p | Out-Null + icacls $p /grant '*S-1-5-32-544:F' | Out-Null + Rename-Item $p "$f.rjava-hidden" + $hidden += $p + } + } + Write-Output "Hidden: $hidden" + Get-ChildItem "$env:RJE_JAVA_HOME_A" -Recurse -Include 'msvc*.dll', 'vcruntime*.dll' | ForEach-Object FullName + Rscript --vanilla tests-integration/deferred.R select + $select = $LASTEXITCODE + Rscript --vanilla tests-integration/deferred.R laterload + $later = $LASTEXITCODE + foreach ($p in $hidden) { Rename-Item "$p.rjava-hidden" (Split-Path $p -Leaf) } + Write-Output "EVIDENCE without System32 VC++ runtime: select exit $select, laterload exit $later" + if ($select -ne 0) { exit 1 } + exit 0 - uses: actions/checkout@v4 if: matrix.downstream with: diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 3ebc455..3a13638 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -232,6 +232,25 @@ switch(scenario, later_loads() stopifnot(identical(minimal_path, Sys.getenv("PATH"))) }, + hosted = { + # Native code in this process created a JVM before rJava was used. rJava + # must attach to it, although JAVA_HOME names another runtime. A host + # provides rJava's class loader on its class path (JDK 9+ forbids adding it). + helper <- Sys.getenv("RJE_JVMHOST") + stopifnot(file.exists(helper)) + dyn.load(helper) + boot <- file.path(system.file("java", package = "rJava"), "boot") + old <- Sys.getenv("PATH") + if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(lib(home_a))), winslash = "\\"), old, sep = ";")) + created <- .C("rjava_test_create_jvm", lib(home_a), paste0("-Djava.class.path=", boot), result = 0L)$result + Sys.setenv(PATH = old) + stopifnot(created == 0L) + mapped <- .Call(internal("RgetMappedJVM")) + stopifnot(!is.null(mapped), identical(mapped$vms, 1L), state()$state == "not-loaded") + Sys.setenv(JAVA_HOME = home_b) + start(home_a) + stopifnot(state()$state == "attached") + }, jengine = { # R hosts the JVM, then JRI is loaded into it as a later native library. jri <- system.file("jri", package = "rJava") diff --git a/tests-integration/jvmhost.c b/tests-integration/jvmhost.c new file mode 100644 index 0000000..004d286 --- /dev/null +++ b/tests-integration/jvmhost.c @@ -0,0 +1,33 @@ +/* Test helper: creates a JVM in the R process outside rJava, as a native + application embedding both Java and R would. POSIX loads the library with + global symbols, like the java launcher. Build with + PKG_CPPFLAGS=-I/src R CMD SHLIB jvmhost.c */ +#ifdef _WIN32 +#include +#else +#include +#endif +#include "jni.h" + +void rjava_test_create_jvm(char **path, char **classpath, int *result) { + JNI_CreateJavaVM_fnptr create = 0; + JavaVMOption options[2]; + JavaVMInitArgs args; + JavaVM *vm; + void *env; +#ifdef _WIN32 + HMODULE lib = LoadLibraryExA(path[0], NULL, LOAD_WITH_ALTERED_SEARCH_PATH); + if (lib) create = (JNI_CreateJavaVM_fnptr) GetProcAddress(lib, "JNI_CreateJavaVM"); +#else + void *lib = dlopen(path[0], RTLD_NOW | RTLD_GLOBAL); + if (lib) *(void **) &create = dlsym(lib, "JNI_CreateJavaVM"); +#endif + if (!create) { *result = -100; return; } + options[0].optionString = classpath[0]; + options[1].optionString = "-Djava.awt.headless=true"; + args.version = JNI_VERSION_1_2; + args.nOptions = 2; + args.options = options; + args.ignoreUnrecognized = JNI_TRUE; + *result = create(&vm, &env, &args); +} From dbbc4ee8fd7c5b29f21c4ed16fb35510f7ae7906 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:31:54 +0200 Subject: [PATCH 24/63] docs: state the deferred loading contract, Windows evidence and limits Rewrite tests-integration/README.md: selection order and file-identity rule; why selected libraries are never unloaded (a separate safety boundary); scoped Windows DLL search and the CI evidence that later loads of awt.dll/sunmscapi.dll need a VC++ runtime outside the JDK, as upstream, with two unimplemented options for maintainers; the test matrix; and what is not covered, including Windows JRI (two pre-existing build defects). Also run the later-load scenario with JDK B, so all six runtimes of the matrix are exercised. --- .github/workflows/dynamic.yaml | 5 +- tests-integration/README.md | 181 +++++++++++++++++++++++++-------- tests-integration/deferred.R | 5 +- 3 files changed, 148 insertions(+), 43 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 2cb3c59..2679d80 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -128,6 +128,7 @@ jobs: # Windows source installs build no JRI (pre-existing upstream build defects). if [[ "$RUNNER_OS" != Windows ]]; then run jengine; fi run preloaded global + run laterload b run alias link run alias spelling if [[ "$RUNNER_OS" == Windows ]]; then run alias short; fi @@ -177,8 +178,10 @@ jobs: $select = $LASTEXITCODE Rscript --vanilla tests-integration/deferred.R laterload $later = $LASTEXITCODE + Rscript --vanilla tests-integration/deferred.R laterload b + $laterb = $LASTEXITCODE foreach ($p in $hidden) { Rename-Item "$p.rjava-hidden" (Split-Path $p -Leaf) } - Write-Output "EVIDENCE without System32 VC++ runtime: select exit $select, laterload exit $later" + Write-Output "EVIDENCE without System32 VC++ runtime: select exit $select, laterload exit $later (A), $laterb (B)" if ($select -ne 0) { exit 1 } exit 0 - uses: actions/checkout@v4 diff --git a/tests-integration/README.md b/tests-integration/README.md index d485301..72528ac 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -2,46 +2,147 @@ This branch ports Simon Urbanek's `feature/dynamic` work onto upstream master (`534ff33`) and includes Egor Kotov's earlier Windows loader work. It is an -experimental fork, not a released rJava API contract. - -The acceptance boundary is **before the first JVM library load**. Loading the -rJava namespace, including through another package's Imports, must not select, -load, or initialize Java. `.jinit()` discovers the selected JVM at call time. -An invalid explicit Java home fails at initialization, allowing correction and -retry while no library has loaded. On Windows, registry discovery is deferred -until initialization too. - -`dynamic.yaml` builds an actual source distribution with `sh mkdist`, installs -it in an isolated R library, and tests it on native Linux, macOS, and Windows -runners with Temurin 17 and 21. Every scenario uses a new R process. The native -library must have no direct JVM linkage. Tests cover selection after namespace -load, recovery after missing-library/missing-JNI-export errors, JNI calls, -and rejection of unloading any selected JVM library. They also install the rJavaEnv -feature branch and its two minimal downstream packages to test real Imports, -setup, startup, compatible guards, and child-process isolation. - -To run locally, build and install the source tarball in a temporary library, -then set `R_LIBS` to it and `RJE_JAVA_HOME_A`/`RJE_JAVA_HOME_B` to two real JDK -homes. Run `Rscript --vanilla tests-integration/deferred.R select`, `retry`, and -`unload` as separate commands. The last case checks that unloading and changing -a selected, unstarted runtime both fail cleanly, then starts that same runtime. +experimental fork, not a released rJava API contract. Green CI jobs show that +the cases below work on hosted runners. They do not establish production +safety for every system. + +## Contract + +Loading the rJava namespace, including through another package's Imports, +does not select, load, or initialize Java. The first `.jinit()` (or +`.jloadJVM()`) selects a JVM library, in this order: + +1. A JVM already created in the process, e.g. by a Java application + embedding R through JRI: rJava attaches to it whatever Java home is + configured. That is the only runtime the process can use, and rJava + linked against the JVM library behaved the same way. +2. `getOption("java.home")` (Windows only, as before) or `JAVA_HOME`. A value + that is not an existing directory is ignored with the upstream warning, + as rJava did on Windows and macOS before deferred loading. +3. Windows: `CurrentVersion` of the JavaSoft registry keys (HKLM, then HKCU). +4. macOS: `/usr/libexec/java_home`. +5. The `java.home` property of `java` on `PATH`. + +Within a Java home, known layouts are checked first (`lib/server`, +`jre/lib//server`, `jre/lib/server`, `bin/server`, `jre/bin/server`), +then a recursive search. An existing Java home without a JVM library is an +error. Every failure before a library is loaded leaves the state +`"not-loaded"`, so the setting can be corrected and `.jinit()` retried. + +If another JVM library is mapped into the process without a created VM, +only the same file can be selected. Identity is the file, not its name: +device and inode on Linux and macOS, volume serial and file ID on Windows. +So symbolic links, junctions, 8.3 short names and other spellings of the +loaded library are accepted, and djni references the mapped module rather +than loading the alias. A different or unverifiable file is rejected. + +## Selected libraries are not unloaded + +`.junloadJVM()` errors whenever a JVM library is loaded, even before a VM was +created. This is a deliberate safety boundary, separate from the Windows port +(commit "fix: retain JVM library selection until the R process exits"). +`dlclose()`/`FreeLibrary()` success does not mean the library was unmapped: +macOS dyld never unloads images with thread-local variables, and libjvm has +them. In local macOS probes, loading Corretto 17 or Temurin 27, unloading it +before any VM was created, then starting another JDK failed with symbols +resolved against the first library. After `DestroyJavaVM`, creating a VM +from the same library failed (`JNI_ERR`), and switching from Corretto 17 to +Temurin 17 crashed in libjvm. +Use a new R process to run another JVM. `djni_unload()` remains only to +clean up a library that lacks the JNI entry points, and for Simon's +standalone `DJNI_MAIN_TEST` probe. + +## Windows DLL search state + +To load `jvm.dll`, djni adds the JDK `bin` directory (the parent of +`bin\server`, needed for JDK 8's C runtime) with `AddDllDirectory` only for +that `LoadLibraryExW` call, then removes it after success or failure. `PATH` +is never changed. `dll-search.c` checks the removal with a positive control. + +Later native loads by the JVM (`zip`, `net`, `nio`, `management`, +`sunmscapi`, `awt`, `fontmanager`, `freetype`) work with `PATH` reduced to +system and R directories, for both JDKs of every Windows job. JDK libraries +that import each other resolve as already-loaded modules. The logged module +lists show the one external later dependency: `awt.dll` (and on JDK 8 also +`sunmscapi.dll`) imports the VC++ runtime (`MSVCP140.dll`, and +`VCRUNTIME140_1.dll` on JDK 8), which Windows then looks for in System32 and +on `PATH`, not in the JDK. Upstream rJava has the same exposure, because it +restores `PATH` right after linking `jvm.dll`. + +The Windows job therefore hides System32's VC++ runtime DLLs, as on a +machine without the VC++ redistributable (for example with a JDK unpacked +from a zip archive and not on `PATH`). JVM startup must still succeed, and +does for Temurin 8, Temurin 17 and Corretto 11: the scoped `bin` search +covers `jvm.dll` and its startup libraries. The later loads then fail, as +recorded in the log: `sunmscapi.dll` on Temurin 8 and `awt.dll` on Temurin +17 and Corretto 11, with "Can't find dependent libraries". This branch does +not change that. + +Options for maintainers, **not implemented**: (1) append the JDK `bin` +directory to `PATH` after selection, as an installer that adds Java to +`PATH` effectively does; this persists for the process and its children. +(2) After loading `jvm.dll`, load the VC++ runtime DLLs bundled in the JDK +`bin` only if System32 has none; the process then binds every later import +of those names to the JDK's copies, possibly older than a copy another +component bundles. Both are permanent process-wide changes that upstream +never made. + +## Tests + +Every scenario runs in a new R process with `PATH` reduced to system and R +directories and `JAVA_HOME` set explicitly, so the runner's Java setup cannot +mask discovery or dependency failures. `dynamic.yaml` builds a source +distribution with `sh mkdist`, installs it in an isolated library, and runs: + +| Check | What it shows | +|---|---| +| linkage | `rJava.so`/`rJava.dll` has no JVM library dependency | +| `jni-abi.R` | `src/jni.h` type sizes, invocation struct fields and every function table offset match each JDK's `jni.h` | +| `select`, `retry`, `badinput` | selection after namespace load; recovery after an empty home, missing file, library without JNI exports, empty, over-long and relative paths | +| `unload` | unloading or changing a selected, unstarted library fails | +| `preloaded`, `preloaded global` | a library mapped outside rJava (also with global symbols) rejects another runtime and permits the same one | +| `alias link/spelling/short` | the same preloaded library through a symbolic link or junction with spaces and non-ASCII characters, an alternative spelling, and an 8.3 name (Windows) | +| `nonascii` | loading through such a path without a preload | +| `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | +| `laterload` | the later native loads above | +| `hosted` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK | +| `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where rJava attaches to the host JVM (Linux and macOS) | +| `dll-search.c` | Windows DLL search state after failed and successful loads | +| downstream | rJavaEnv and two minimal consumer packages (current pair only) | + +Bounded matrix, each on Linux, macOS (arm64) and Windows with R release; the +build JDK is Temurin 17: + +| Set | JDK A | JDK B | +|---|---|---| +| legacy | Temurin 8 (Zulu 8 on macOS) | Temurin 11 | +| current | Temurin 17 | Temurin 21 | +| vendor | Corretto 11 | Zulu 25 | + +The upstream `check.yaml` matrix (R devel, Zulu 8 and 11, five runner images +including Intel macOS) also runs on this branch. + +To run locally, build with `sh mkdist`, install the tarball in a temporary +library, set `R_LIBS` to it and `RJE_JAVA_HOME_A`/`RJE_JAVA_HOME_B` to two JDK +homes, and run `Rscript --vanilla tests-integration/deferred.R ` +from the repository root, one process per scenario. ## Limits -Namespace loading is different from JVM library loading, which is different -from JVM startup. Calling `.jloadJVM()` fixes the runtime selection boundary -even before `.jinit()`. A successful OS-level unload does not establish that -all runtime symbols and dependencies were removed. In a local macOS experiment, -loading Java 17, unloading before startup, then starting Java 27 failed with a -Java 17/27 symbol mismatch. Do not use `.junloadJVM()` to switch JDKs. - -A selected JVM library cannot be unloaded by this R API, even before startup. Restarting a worker process -remains the supported way to run another JVM. A package that calls `.jinit()` -in `.onLoad()` still selects Java during package loading; this branch cannot -make that package's initialization lazy. - -The hosted matrix covers current 64-bit R and two Temurin versions. It does not -establish compatibility for 32-bit Windows, every vendor, JDK 8, Java embedding -R through JRI, or every downstream package. The source build includes JRI, but -embedding/attach behavior requires separate regression testing before upstream -release. +- Namespace loading, JVM library loading and JVM startup are different + boundaries. `.jloadJVM()` fixes the selection even before `.jinit()`. A + package that calls `.jinit()` in `.onLoad()` still selects Java when it + loads. +- Attaching to a JVM created by a host requires rJava's `java/boot` on the + host's class path on JDK 9+, as before: rJava cannot add it to a running + application class loader there. +- Windows source installs build no JRI, as on upstream master: + `configure.win` still looks for `jri/` although JRI moved to `src/jri` in + 1.0-15, and `src/jri/Rdecl.h` includes ``, which R does not + ship on Windows. JRI embedding is therefore tested on Linux and macOS only; + attaching to a created JVM is tested on Windows with the native helper. +- Windows AWT later loads depend on a VC++ runtime outside the JDK, as above. +- Not covered: 32-bit Windows, Windows on ARM, Linux on ARM, musl, OpenJ9 and + other non-HotSpot VMs, Java runtimes without a JDK layout, R versions other + than current release (and devel in `check.yaml`), and downstream packages + beyond the two test consumers. diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 3a13638..c87b94e 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -227,8 +227,9 @@ switch(scenario, start(expected) }, laterload = { - Sys.setenv(JAVA_HOME = home_a) - start(home_a) + home <- if (variant == "b") home_b else home_a + Sys.setenv(JAVA_HOME = home) + start(home) later_loads() stopifnot(identical(minimal_path, Sys.getenv("PATH"))) }, From f679c678898ee6cdf124339b2239ea6e1be2a5ef Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 20:40:51 +0200 Subject: [PATCH 25/63] docs: report the hidden-runtime results for all six JDKs --- tests-integration/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests-integration/README.md b/tests-integration/README.md index 72528ac..e365adc 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -72,11 +72,11 @@ restores `PATH` right after linking `jvm.dll`. The Windows job therefore hides System32's VC++ runtime DLLs, as on a machine without the VC++ redistributable (for example with a JDK unpacked from a zip archive and not on `PATH`). JVM startup must still succeed, and -does for Temurin 8, Temurin 17 and Corretto 11: the scoped `bin` search -covers `jvm.dll` and its startup libraries. The later loads then fail, as -recorded in the log: `sunmscapi.dll` on Temurin 8 and `awt.dll` on Temurin -17 and Corretto 11, with "Can't find dependent libraries". This branch does -not change that. +does for all six JDKs: the scoped `bin` search covers `jvm.dll` and its +startup libraries. The later loads then fail, as recorded in the log, with +"Can't find dependent libraries": `sunmscapi.dll` on Temurin 8, and +`awt.dll` on Temurin 11, 17 and 21, Corretto 11 and Zulu 25. This branch +does not change that. Options for maintainers, **not implemented**: (1) append the JDK `bin` directory to `PATH` after selection, as an installer that adds Java to From b6174defc6ac21b4778061c116cc9004b02e1eef Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 21:18:23 +0200 Subject: [PATCH 26/63] fix: find mapped JVM libraries by their exports, not only by name A JVM library mapped under another basename (renamed copy, hard link) was invisible to djni_mapped() and the conflict check, so djni_load() mapped a second runtime. djni now lists every loaded image (dyld, dl_iterate_phdr, EnumProcessModules) without loading anything, and treats an image as a JVM if it has a JVM library name or itself defines JNI_CreateJavaVM, JNI_GetCreatedJavaVMs and JVM_CurrentTimeMillis. Inherited definitions, forwarded Windows exports, rJava's own wrappers and the macOS libjli (JNI shims only) do not count. Several mapped JVM images reject selection. On Windows a JVM mapped under another module name is rejected, as the JDK's DLLs import jvm.dll by name. If images cannot be listed, selection fails and RgetMappedJVM signals an error. Tests: renamed copy/hardlink/symlink, hosted renamed and a jli decoy; CI checks each runtime's exports. Pins rJavaEnv at 693b87c. --- .github/workflows/dynamic.yaml | 24 +- src/Rglue.c | 7 +- src/djni.c | 422 ++++++++++++++++++++++++++------- src/djni.h | 4 +- tests-integration/README.md | 43 +++- tests-integration/deferred.R | 92 ++++++- 6 files changed, 497 insertions(+), 95 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 2679d80..feb615e 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -57,6 +57,16 @@ jobs: for home in "$JAVA_A" "$JAVA_B"; do echo "== $home"; "$home/bin/java" -version 2>&1 find "$home" \( -name 'libjvm.*' -o -name jvm.dll \) -print + # djni recognizes a JVM library under any name by these exports. + lib="$(find "$home" \( -name 'libjvm.*' -o -name jvm.dll \) -path '*server*' -print -quit)" + case "$RUNNER_OS" in + Linux) syms="$(nm -D --defined-only "$lib")" ;; + macOS) syms="$(nm -gU "$lib")" ;; + Windows) syms="$(objdump -p "$lib" | tr -d '\r')" ;; + esac + for sym in JNI_CreateJavaVM JNI_GetCreatedJavaVMs JVM_CurrentTimeMillis; do + if grep -Eq "[ _]$sym(@|\$| )" <<< "$syms"; then echo "exports $sym"; else echo "::error::$lib does not export $sym"; exit 1; fi + done done echo "RJE_JAVA_HOME_A=$JAVA_A" >> "$GITHUB_ENV" echo "RJE_JAVA_HOME_B=$JAVA_B" >> "$GITHUB_ENV" @@ -132,6 +142,18 @@ jobs: run alias link run alias spelling if [[ "$RUNNER_OS" == Windows ]]; then run alias short; fi + # JVM libraries mapped under names no JVM uses, and a non-JVM library with JNI exports. + run renamed copy + run renamed hardlink + # A renamed jvm.dll cannot start a VM: JDK DLLs import jvm.dll by name. + if [[ "$RUNNER_OS" != Windows ]]; then run renamed symlink; run hosted renamed; fi + run decoy + - name: Remove hard links left in the JDK directories + if: always() + shell: bash + run: | + # A mapped DLL cannot be deleted, so Windows leaves them behind until here. + find "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" -name 'rjava-alias-*' -print -delete - name: Embed R in a Java host through JRI if: runner.os != 'Windows' shell: bash @@ -188,7 +210,7 @@ jobs: if: matrix.downstream with: repository: e-kotov/rJavaEnv - ref: 9dd898e6a6d910ecae246c0771b7d5eb270031c4 + ref: 693b87c67f8c521388c3fd8627c8afbc29d8766a path: downstream-source - uses: r-lib/actions/setup-r-dependencies@v2 if: matrix.downstream diff --git a/src/Rglue.c b/src/Rglue.c index fd6a1f6..7609e4d 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1175,7 +1175,12 @@ REPC SEXP RgetMappedJVM(void) { int vms; const char *path = djni_mapped(&vms); SEXP res, names; - if (!path) return R_NilValue; + if (!path) { + if (vms == -2) + Rf_error("Cannot determine whether a JVM library is mapped: %s", + djni_last_error() ? djni_last_error() : "unknown error"); + return R_NilValue; + } res = PROTECT(Rf_allocVector(VECSXP, 2)); names = PROTECT(Rf_allocVector(STRSXP, 2)); #ifdef _WIN32 diff --git a/src/djni.c b/src/djni.c index 8d0f232..a799612 100644 --- a/src/djni.c +++ b/src/djni.c @@ -4,7 +4,10 @@ #endif #ifdef _WIN32 #define _WIN32_WINNT 0x0602 +/* EnumProcessModules is K32EnumProcessModules in kernel32, without psapi.dll */ +#define PSAPI_VERSION 2 #include +#include #else #include #include @@ -14,6 +17,7 @@ #include #endif #endif +#include #include #include #include @@ -138,11 +142,119 @@ static int same_file(const wchar_t *a, const wchar_t *b) { if (hb != INVALID_HANDLE_VALUE) CloseHandle(hb); return res; } +static int in_image(DWORD rva, DWORD count, DWORD width, DWORD size) { + return rva < size && count <= (size - rva) / width; +} +/* Whether a mapped module defines, rather than forwards, an export. The + export table is read in memory, so nothing is loaded; GetProcAddress + would load the target of a forwarded export. */ +static int owns_export(HMODULE module, const char *sym) { + const BYTE *base = (const BYTE *) module; + const IMAGE_DOS_HEADER *dos = (const IMAGE_DOS_HEADER *) base; + const IMAGE_NT_HEADERS *nt; + const IMAGE_DATA_DIRECTORY *dir; + const IMAGE_EXPORT_DIRECTORY *exp; + const DWORD *names, *functions; + const WORD *ordinals; + DWORD size, i, len = (DWORD) strlen(sym) + 1; + if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew < (LONG) sizeof(*dos) || + dos->e_lfanew > 4096 - (LONG) sizeof(*nt)) + return 0; + nt = (const IMAGE_NT_HEADERS *) (base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC || + nt->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) + return 0; + size = nt->OptionalHeader.SizeOfImage; + dir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (!dir->VirtualAddress || dir->VirtualAddress >= size || dir->Size < sizeof(*exp) || + dir->Size > size - dir->VirtualAddress) + return 0; + exp = (const IMAGE_EXPORT_DIRECTORY *) (base + dir->VirtualAddress); + if (!in_image(exp->AddressOfNames, exp->NumberOfNames, 4, size) || + !in_image(exp->AddressOfNameOrdinals, exp->NumberOfNames, 2, size) || + !in_image(exp->AddressOfFunctions, exp->NumberOfFunctions, 4, size)) + return 0; + names = (const DWORD *) (base + exp->AddressOfNames); + ordinals = (const WORD *) (base + exp->AddressOfNameOrdinals); + functions = (const DWORD *) (base + exp->AddressOfFunctions); + /* Names are usually sorted, but a linear scan does not depend on it. */ + for (i = 0; i < exp->NumberOfNames; i++) { + DWORD f; + if (names[i] >= size || len > size - names[i] || memcmp(base + names[i], sym, len)) continue; + if (ordinals[i] >= exp->NumberOfFunctions) return 0; + f = functions[ordinals[i]]; + /* An address inside the export directory names a forwarded export. */ + return f && f < size && (f < dir->VirtualAddress || f - dir->VirtualAddress >= dir->Size); + } + return 0; +} +static int is_jvm_name(const wchar_t *path) { + const wchar_t *base = wcsrchr(path, L'\\'); + return !_wcsicmp(base ? base + 1 : path, L"jvm.dll"); +} +/* A module is a JVM if it is named jvm.dll, even if malformed, or if it + defines the JNI invocation exports and a JVM_ export, whatever its name. + JNI exports alone do not suffice: rJava's wrappers have them too. */ +static int is_jvm_module(HMODULE module, const wchar_t *path) { + return (path && is_jvm_name(path)) || + (owns_export(module, "JVM_CurrentTimeMillis") && owns_export(module, "JNI_CreateJavaVM") && + owns_export(module, "JNI_GetCreatedJavaVMs")); +} +struct jvm_image { HMODULE handle; wchar_t *path; }; +/* Lists the JVM modules mapped into the process, each with a reference + (and path if known). Returns their number, -1 if modules cannot be listed. */ +static int find_jvms(struct jvm_image **out) { + HMODULE *mods = NULL; + struct jvm_image *v = NULL; + DWORD need = 0, size = 0, count, i; + int n = 0, tries; + *out = NULL; + for (tries = 0; tries < 4 && need >= size; tries++) { + HMODULE *m = realloc(mods, (size = need + 64 * sizeof(HMODULE))); + if (!m) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); goto fail; } + mods = m; + if (!K32EnumProcessModules(GetCurrentProcess(), mods, size, &need)) goto fail; + } + if (need >= size) { SetLastError(ERROR_INSUFFICIENT_BUFFER); goto fail; } + count = need / sizeof(HMODULE); + if (count && !(v = calloc(count, sizeof(*v)))) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); goto fail; } + for (i = 0; i < count; i++) { + HMODULE module = NULL; + wchar_t *path; + /* Hold a reference while reading the module: a module unloaded since + the enumeration is skipped, and cannot be replaced while read. */ + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR) mods[i], &module)) continue; + if (module != mods[i]) { FreeLibrary(module); continue; } + path = module_path(module); + if (is_jvm_module(module, path)) { + v[n].handle = module; + v[n++].path = path; + } else { + free(path); + FreeLibrary(module); + } + } + free(mods); + *out = v; + return n; +fail: + free(mods); + free(v); + return -1; +} +static void free_jvms(struct jvm_image *v, int n) { + int i; + for (i = 0; i < n; i++) { + if (v[i].handle) FreeLibrary(v[i].handle); + free(v[i].path); + } + free(v); +} static int load_library(const char *path) { - wchar_t *wide = NULL, *full = NULL, *mapped_path = NULL, *bin = NULL, *slash; - HMODULE mapped; + wchar_t *wide = NULL, *full = NULL, *bin = NULL, *slash; + struct jvm_image *jvms = NULL; DLL_DIRECTORY_COOKIE cookie = NULL; - int res = -2; + int res = -2, n = 0; if (!(wide = to_wide(path))) { save_win_error("Cannot convert JVM library path to UTF-16", path, GetLastError()); goto done; @@ -152,20 +264,33 @@ static int load_library(const char *path) { goto done; } for (slash = full; *slash; slash++) if (*slash == L'/') *slash = L'\\'; - if ((mapped = GetModuleHandleW(L"jvm.dll"))) { - int same = (mapped_path = module_path(mapped)) ? same_file(full, mapped_path) : -1; - char *name = mapped_path ? to_utf8(mapped_path) : NULL; - if (same == 1) { + if ((n = find_jvms(&jvms)) < 0) { + save_win_error("Cannot list the modules of", "this process", GetLastError()); + res = -4; + goto done; + } + if (n) { + /* Two mapped JVM modules are two runtimes, even if they are one file. */ + int same = (n == 1 && jvms[0].path) ? same_file(full, jvms[0].path) : -1; + char *name = jvms[0].path ? to_utf8(jvms[0].path) : NULL; + if (same == 1 && !is_jvm_name(jvms[0].path)) { + /* The JDK's DLLs import jvm.dll by name, and the loader matches + loaded modules by base name: starting this one would load + another jvm.dll or fail. */ + snprintf(last_error, sizeof(last_error), "The JVM library %s is already mapped under another name: %s. Use a new R process.", + path, name ? name : "unknown path"); + res = -4; + } else if (same == 1) { /* Reference the mapped module itself: loading an alias of its path - must never map a second copy of jvm.dll. */ - if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR) mapped, &jni_dl)) - res = 0; - else { - jni_dl = NULL; - save_win_error("Cannot reference the mapped JVM library", name ? name : path, GetLastError()); - } + must never map a second copy of the JVM. */ + jni_dl = jvms[0].handle; + jvms[0].handle = NULL; + res = 0; } else { - if (same) + if (n > 1) + snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", + n, name ? name : "unknown path"); + else if (same) snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s. Use a new R process.", path, name ? name : "unknown path"); else @@ -194,27 +319,40 @@ static int load_library(const char *path) { else save_win_error("Cannot load JVM library", path, GetLastError()); if (cookie) RemoveDllDirectory(cookie); done: - free(wide); free(full); free(mapped_path); free(bin); + if (jvms) free_jvms(jvms, n); + free(wide); free(full); free(bin); return res; } const char *djni_mapped(int *vms) { - HMODULE module = NULL; - wchar_t *path; - JNI_GetCreatedJavaVMs_fnptr created; + struct jvm_image *jvms; + int n; *vms = -1; free(mapped_name); mapped_name = NULL; - if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, L"jvm.dll", &module)) + if ((n = find_jvms(&jvms)) < 0) { + save_win_error("Cannot list the modules of", "this process", GetLastError()); + *vms = -2; + return NULL; + } + if (!n) { + free(jvms); return NULL; - if ((created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"))) { + } + /* With several JVM modules, the VM count of one is not the answer. */ + if (n == 1 && owns_export(jvms[0].handle, "JNI_GetCreatedJavaVMs")) { + JNI_GetCreatedJavaVMs_fnptr created = + (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(jvms[0].handle, "JNI_GetCreatedJavaVMs"); JavaVM *vm; - jsize n = 0; - if (created(&vm, 1, &n) == JNI_OK) *vms = (int) n; + jsize count = 0; + if (created && created(&vm, 1, &count) == JNI_OK) *vms = (int) count; } - if ((path = module_path(module))) { - mapped_name = to_utf8(path); - free(path); + mapped_name = jvms[0].path ? to_utf8(jvms[0].path) : NULL; + if (!mapped_name) mapped_name = strdup("(unknown path)"); + if (!mapped_name) { + snprintf(last_error, sizeof(last_error), "Cannot report the mapped JVM library: out of memory"); + *vms = -2; } + free_jvms(jvms, n); return mapped_name; } #else @@ -223,94 +361,216 @@ static int same_file(const char *a, const char *b) { if (stat(a, &sa) || stat(b, &sb)) return -1; return sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; } -static int is_jvm_image(const char *name) { +static int is_jvm_name(const char *name) { const char *base = strrchr(name, '/'); base = base ? base + 1 : name; return !strcmp(base, "libjvm.dylib") || !strcmp(base, "libjvm.so"); } -typedef int (*image_fn)(const char *name, void *data); +/* An image mapped into the process: its name as the dynamic loader records + it, and where it is mapped, to tell its own definitions from those of its + dependencies. */ +struct image { + char *name; +#ifdef __APPLE__ + const void *header; +#else + size_t nseg; + uintptr_t *seg; /* start and end of each PT_LOAD segment */ +#endif +}; +struct images { struct image *v; size_t n, size; int failed; }; +static struct image *add_image(struct images *l, const char *name) { + struct image *im; + if (l->n == l->size) { + size_t size = l->size ? 2 * l->size : 64; + struct image *v = realloc(l->v, size * sizeof(*v)); + if (!v) { l->failed = 1; return NULL; } + l->v = v; + l->size = size; + } + im = &l->v[l->n]; + memset(im, 0, sizeof(*im)); + if (!(im->name = strdup(name))) { l->failed = 1; return NULL; } + l->n++; + return im; +} #ifdef __linux__ -struct phdr_data { image_fn fn; void *data; }; +/* Only records images: dlopen() must not be called from this callback. */ static int phdr_image(struct dl_phdr_info *info, size_t size, void *data) { - struct phdr_data *d = (struct phdr_data *) data; - return info->dlpi_name ? d->fn(info->dlpi_name, d->data) : 0; + struct images *l = (struct images *) data; + struct image *im; + size_t i, n = 0; + if (!info->dlpi_name || !*info->dlpi_name) return 0; /* the executable */ + if (!(im = add_image(l, info->dlpi_name))) return 1; + for (i = 0; i < info->dlpi_phnum; i++) if (info->dlpi_phdr[i].p_type == PT_LOAD) n++; + if (n && !(im->seg = malloc(2 * n * sizeof(uintptr_t)))) { l->failed = 1; return 1; } + for (i = 0; i < info->dlpi_phnum; i++) + if (info->dlpi_phdr[i].p_type == PT_LOAD) { + uintptr_t start = (uintptr_t) info->dlpi_addr + (uintptr_t) info->dlpi_phdr[i].p_vaddr; + im->seg[2 * im->nseg] = start; + im->seg[2 * im->nseg++ + 1] = start + (uintptr_t) info->dlpi_phdr[i].p_memsz; + } + return 0; } #endif -/* Calls fn for each image mapped into the process until it returns nonzero. */ -static int each_image(image_fn fn, void *data) { +static void free_images(struct images *l) { + size_t i; + for (i = 0; i < l->n; i++) { + free(l->v[i].name); +#ifndef __APPLE__ + free(l->v[i].seg); +#endif + } + free(l->v); +} +/* Returns 0, or -1 if the images cannot be listed. Other systems list none. */ +static int list_images(struct images *l) { #ifdef __APPLE__ uint32_t i, n = _dyld_image_count(); for (i = 0; i < n; i++) { const char *name = _dyld_get_image_name(i); - if (name && fn(name, data)) return 1; + const struct mach_header *header = _dyld_get_image_header(i); + struct image *im; + if (!name || !header) continue; + if (!(im = add_image(l, name))) break; + im->header = header; } - return 0; #elif defined(__linux__) - struct phdr_data d = { fn, data }; - return dl_iterate_phdr(phdr_image, &d); -#else - return 0; + dl_iterate_phdr(phdr_image, l); #endif + return l->failed ? -1 : 0; } -struct jvm_match { const char *requested; char *mapped; }; -/* Device and inode identify the file across symbolic links, hard links and - equivalent spellings; a requested file that cannot be found differs. */ -static int check_mapped(const char *name, void *data) { - struct jvm_match *m = (struct jvm_match *) data; - if (!is_jvm_image(name)) return 0; - if (same_file(name, m->requested) == 1) { - if (!m->mapped) m->mapped = strdup(name); +#ifdef RTLD_NOLOAD +/* Whether the image defines sym itself: dlsym() on its handle also finds + definitions it inherits from the libraries it depends on. */ +static int owns(void *handle, const struct image *im, const char *sym) { + void *p = dlsym(handle, sym); + if (!p) return 0; +#ifdef __APPLE__ + { + Dl_info info; + return dladdr(p, &info) && info.dli_fbase == im->header; + } +#else + { + uintptr_t a = (uintptr_t) p; + size_t i; + for (i = 0; i < im->nseg; i++) + if (a >= im->seg[2 * i] && a < im->seg[2 * i + 1]) return 1; return 0; } - snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", name); - return 1; +#endif } -static int find_jvm(const char *name, void *data) { - if (!is_jvm_image(name)) return 0; - *(char **) data = strdup(name); - return 1; +#endif +struct jvm_image { char *name; void *handle; int owns_created; }; +/* Lists the JVM images mapped into the process, each with a reference if + one can be taken without loading anything. An image is a JVM if it has a + JVM library name, even if malformed, or if it defines the JNI invocation + exports and a JVM_ export, whatever its name. JNI exports alone do not + suffice: the macOS libjli defines them to forward to libjvm. Returns the + number of JVM images, or -1 if the images cannot be listed. */ +static int find_jvms(struct jvm_image **out) { + struct images l = { NULL, 0, 0, 0 }; + struct jvm_image *v = NULL; + size_t i; + int n = 0; + *out = NULL; + if (list_images(&l) || (l.n && !(v = calloc(l.n, sizeof(*v))))) { + free_images(&l); + return -1; + } + for (i = 0; i < l.n; i++) { + void *handle = NULL; + int jvm = is_jvm_name(l.v[i].name); +#ifdef RTLD_NOLOAD + /* RTLD_NOLOAD returns a reference only if the image is already mapped, + and RTLD_LAZY leaves the relocation of other images unchanged. */ + handle = dlopen(l.v[i].name, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + if (handle && !jvm) + jvm = owns(handle, &l.v[i], "JVM_CurrentTimeMillis") && owns(handle, &l.v[i], "JNI_CreateJavaVM") && + owns(handle, &l.v[i], "JNI_GetCreatedJavaVMs"); + if (jvm) v[n].owns_created = handle && owns(handle, &l.v[i], "JNI_GetCreatedJavaVMs"); +#endif + if (jvm) { + v[n].name = l.v[i].name; + l.v[i].name = NULL; + v[n++].handle = handle; + } else if (handle) + dlclose(handle); + } + dlerror(); /* failed lookups are not errors of the caller */ + free_images(&l); + *out = v; + return n; +} +static void free_jvms(struct jvm_image *v, int n) { + int i; + for (i = 0; i < n; i++) { + if (v[i].handle) dlclose(v[i].handle); + free(v[i].name); + } + free(v); } +/* Device and inode identify the file across symbolic links, hard links and + equivalent spellings; a requested file that cannot be found differs. */ static int load_library(const char *path) { - struct jvm_match m = { path, NULL }; - if (each_image(check_mapped, &m)) { - free(m.mapped); + struct jvm_image *jvms; + int n = find_jvms(&jvms), res = 0; + if (n < 0) { + snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process. Use a new R process."); return -4; } - /* Open a mapped JVM by its own name, so an alias cannot map another copy. */ - jni_dl = dlopen(m.mapped ? m.mapped : path, RTLD_LOCAL | RTLD_NOW); - free(m.mapped); - if (!jni_dl) { + if (n > 1) { + snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", + n, jvms[0].name); + res = -4; + } else if (n == 1) { + if (same_file(jvms[0].name, path) != 1) { + snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", jvms[0].name); + res = -4; + } else if (!jvms[0].handle) { + snprintf(last_error, sizeof(last_error), "Cannot reference the JVM library already mapped: %s. Use a new R process.", jvms[0].name); + res = -4; + } else { + /* Reference the mapped image itself, so an alias cannot map another copy. */ + jni_dl = jvms[0].handle; + jvms[0].handle = NULL; + } + } else if (!(jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW))) { const char *error = dlerror(); snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); - return -2; + res = -2; } - return 0; + free_jvms(jvms, n); + return res; } const char *djni_mapped(int *vms) { - char *found = NULL; + struct jvm_image *jvms; + int n; *vms = -1; free(mapped_name); mapped_name = NULL; - each_image(find_jvm, &found); - if (!found) return NULL; -#ifdef RTLD_NOLOAD - { - /* RTLD_NOLOAD returns a reference only if the image is already mapped. */ - void *handle = dlopen(found, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); - if (handle) { - JNI_GetCreatedJavaVMs_fnptr created; - *(void **) &created = dlsym(handle, "JNI_GetCreatedJavaVMs"); - if (created) { - JavaVM *vm; - jsize n = 0; - if (created(&vm, 1, &n) == JNI_OK) *vms = (int) n; - } - dlclose(handle); + if ((n = find_jvms(&jvms)) < 0) { + snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process"); + *vms = -2; + return NULL; + } + /* With several JVM images, the VM count of one is not the answer. */ + if (n == 1 && jvms[0].owns_created) { + JNI_GetCreatedJavaVMs_fnptr created; + *(void **) &created = dlsym(jvms[0].handle, "JNI_GetCreatedJavaVMs"); + if (created) { + JavaVM *vm; + jsize count = 0; + if (created(&vm, 1, &count) == JNI_OK) *vms = (int) count; } } -#endif - mapped_name = found; - return found; + if (n) { + mapped_name = jvms[0].name; + jvms[0].name = NULL; + } + free_jvms(jvms, n); + return mapped_name; } #endif diff --git a/src/djni.h b/src/djni.h index 0dbdc29..9affa03 100644 --- a/src/djni.h +++ b/src/djni.h @@ -17,7 +17,9 @@ int djni_unload(void); const char* djni_last_error(void); int djni_loaded(void); /* path of a JVM library already mapped into the process (or NULL), without - loading one; *vms is the number of created VMs, -1 if unknown */ + loading one; *vms is the number of created VMs, -1 if unknown (also if + several JVM libraries are mapped). NULL with *vms == -2 means the mapped + libraries could not be listed, see djni_last_error(). */ const char* djni_mapped(int *vms); /* The wrappers below have the names of the JVM exports. Hide them, so that diff --git a/tests-integration/README.md b/tests-integration/README.md index e365adc..951cccc 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -30,11 +30,32 @@ error. Every failure before a library is loaded leaves the state `"not-loaded"`, so the setting can be corrected and `.jinit()` retried. If another JVM library is mapped into the process without a created VM, -only the same file can be selected. Identity is the file, not its name: -device and inode on Linux and macOS, volume serial and file ID on Windows. -So symbolic links, junctions, 8.3 short names and other spellings of the -loaded library are accepted, and djni references the mapped module rather -than loading the alias. A different or unverifiable file is rejected. +only the same file can be selected. djni finds mapped JVM libraries among +all images the dynamic loader lists (`dl_iterate_phdr` on Linux, dyld on +macOS, `EnumProcessModules` on Windows), without loading anything. An image +is a JVM library if it has a JVM library name (`libjvm.so`, `libjvm.dylib`, +`jvm.dll`), even if malformed, or whatever its name if it defines itself +`JNI_CreateJavaVM`, `JNI_GetCreatedJavaVMs` and `JVM_CurrentTimeMillis`. +"Defines itself" excludes definitions inherited from a dependency +(the symbol address lies outside the image) and forwarded Windows exports +(read from the export table in memory). JNI exports alone do not suffice: +rJava's own wrappers have them, and so does the macOS `libjli`, which +forwards them to libjvm. Every HotSpot JDK in the matrix exports the three +symbols; CI checks this for each runtime. + +Identity is then the file, not its name: device and inode on Linux and +macOS, volume serial and file ID on Windows. So symbolic links, junctions, +hard links, 8.3 short names, other spellings and renamed copies of the +loaded library are recognized: the same file is accepted, and djni +references the mapped image rather than loading the alias. On Windows the +same file is rejected if mapped under another module name than `jvm.dll`: +the JDK's DLLs import `jvm.dll` by name, and the loader matches loaded +modules by base name, so starting a VM would load another `jvm.dll` or +fail. A different or +unverifiable file is rejected, and so is any selection while more than one +JVM image is mapped (`RgetMappedJVM` then reports one of them with `vms` +`-1`). If the loaded images cannot be listed, selection fails and +`RgetMappedJVM` signals an error, so callers do not mistake it for "none". ## Selected libraries are not unloaded @@ -102,10 +123,14 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `unload` | unloading or changing a selected, unstarted library fails | | `preloaded`, `preloaded global` | a library mapped outside rJava (also with global symbols) rejects another runtime and permits the same one | | `alias link/spelling/short` | the same preloaded library through a symbolic link or junction with spaces and non-ASCII characters, an alternative spelling, and an 8.3 name (Windows) | +| `renamed copy/hardlink/symlink` | a JVM library mapped under a name no JVM uses: a copy of B rejects A and B's own library; a hard link of A in its directory (and a symbolic link, Linux and macOS) rejects B, then the same file starts A (Windows: is rejected as mapped under another name) | +| `hosted renamed` | native code creates the VM from the hard link; it is reported with one VM, B is rejected, and `.jinit()` attaches (Linux and macOS) | +| `decoy` | the JDK's `jli` library (JNI exports on macOS, no `JVM_` exports) is not reported and does not block selecting B | | `nonascii` | loading through such a path without a preload | | `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | | `laterload` | the later native loads above | | `hosted` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK | +| every scenario | before anything is mapped, `RgetMappedJVM` is `NULL`: rJava's own JNI wrappers are not taken for a JVM | | `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where rJava attaches to the host JVM (Linux and macOS) | | `dll-search.c` | Windows DLL search state after failed and successful loads | | downstream | rJavaEnv and two minimal consumer packages (current pair only) | @@ -142,6 +167,14 @@ from the repository root, one process per scenario. ship on Windows. JRI embedding is therefore tested on Linux and macOS only; attaching to a created JVM is tested on Windows with the native helper. - Windows AWT later loads depend on a VC++ runtime outside the JDK, as above. +- Mapped JVM discovery covers Linux, macOS and Windows only; elsewhere djni + lists no mapped libraries, so it cannot reject a conflict. It cannot see libraries + in another `dlmopen()` namespace, and identifies a renamed JVM only by + the exports above (a JVM without `JVM_CurrentTimeMillis` must keep its + usual name). It compares the file now at the recorded path: a mapped + library whose file was deleted or replaced is rejected, not matched. dyld + records a library loaded through a symbolic link by its target, so on + macOS the `symlink` variant shows the target name. - Not covered: 32-bit Windows, Windows on ARM, Linux on ARM, musl, OpenJ9 and other non-HotSpot VMs, Java runtimes without a JDK layout, R versions other than current release (and devel in `check.yaml`), and downstream packages diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index c87b94e..1634691 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -23,8 +23,10 @@ options(java.parameters = "-Djava.awt.headless=true") ns <- loadNamespace("rJava") internal <- function(name) get(name, ns) state <- function() internal(".jvmState")() +mapped_jvm <- function() .Call(internal("RgetMappedJVM")) +# rJava's own JNI wrappers (exported by rJava.dll) must not count as a JVM. stopifnot(identical(state()$state, "not-loaded"), !state()$initialized, - identical(minimal_path, Sys.getenv("PATH"))) + identical(minimal_path, Sys.getenv("PATH")), is.null(mapped_jvm())) lib <- function(home) { paths <- internal(".find.jvm.library")(home) @@ -48,13 +50,33 @@ fails <- function(expr, pattern = NULL) { log("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L)) } # Map a JVM library as other native code in the process would, outside rJava. -preload <- function(path, local = TRUE) { +preload <- function(path, local = TRUE, bin = dirname(dirname(path))) { old <- Sys.getenv("PATH") # JDK 8 jvm.dll needs the C runtime from its bin directory. - if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(path)), winslash = "\\"), old, sep = ";")) + if (windows) Sys.setenv(PATH = paste(normalizePath(bin, winslash = "\\"), old, sep = ";")) dyn.load(path, local = local, now = TRUE) Sys.setenv(PATH = old) } +# Another name for the JVM library of a home, with a basename no JVM uses: +# a hard link in its own directory, from which the JVM still finds its home. +# A mapped DLL cannot be deleted, so Windows removes earlier links first. +alias_lib <- function(home) { + real <- lib(home) + unlink(Sys.glob(file.path(dirname(real), "rjava-alias-*"))) + alias <- file.path(dirname(real), paste0("rjava-alias-", Sys.getpid(), ".", tools::file_ext(real))) + stopifnot(file.link(real, alias)) + reg.finalizer(globalenv(), function(e) unlink(alias), onexit = TRUE) + alias +} +# The library must be recognized by its exports, not by its name. +expect_mapped <- function(path, vms) { + mapped <- mapped_jvm() + if (is.null(mapped)) stop("no mapped JVM library was found; expected ", path) + log("mapped", mapped$path, "with", mapped$vms, "VMs") + stopifnot(identical(mapped$vms, vms)) + if (!is.null(path) && !identical(basename(mapped$path), basename(path))) + stop("mapped library is ", mapped$path, ", expected ", path) +} # A directory name with spaces and non-ASCII characters when the locale allows it. odd_dir <- function() { name <- if (isTRUE(l10n_info()[["UTF-8"]])) "rJava t\u00ebst \u00fc \u65e5\u672c" else "rJava test dir" @@ -201,6 +223,59 @@ switch(scenario, stopifnot(state()$state == "none") start(home_a) }, + renamed = { + # A JVM library mapped under a name no JVM uses, by a copy, hard link or + # symbolic link, rejects another runtime; the same file is still accepted. + if (variant == "copy") { + # A copy of B is another file than B's own library, and than A's. + copy <- file.path(odd_dir(), paste0("renamed runtime.", tools::file_ext(lib(home_b)))) + stopifnot(file.copy(lib(home_b), copy, overwrite = TRUE)) + preload(copy, bin = dirname(dirname(lib(home_b)))) + expect_mapped(copy, 0L) + Sys.setenv(JAVA_HOME = home_a) + fails(rJava::.jinit(), "already mapped") + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") + expect_mapped(copy, 0L) + } else { + path <- switch(variant, + hardlink = alias_lib(home_a), + symlink = { + stopifnot(!windows) + link <- file.path(odd_dir(), paste0("renamed link.", tools::file_ext(lib(home_a)))) + unlink(link) + stopifnot(file.symlink(lib(home_a), link)) + link + }, + stop("unknown renamed variant")) + log("renamed", path, "of", lib(home_a)) + preload(path, bin = dirname(dirname(lib(home_a)))) + # dyld may record a symbolic link by its target, so only a hard link + # is certain to be mapped under the unusual name. + expect_mapped(if (variant == "hardlink") path, 0L) + Sys.setenv(JAVA_HOME = home_b) + fails(rJava::.jinit(), "already mapped") + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") + Sys.setenv(JAVA_HOME = home_a) + if (windows) { + # The JDK's DLLs import jvm.dll by name, which this module lacks. + fails(rJava::.jinit(), "under another name") + } else { + start(home_a) + expect_mapped(if (variant == "hardlink") path, 1L) + } + } + }, + decoy = { + # The Java launcher library (jli) has JNI exports on macOS that forward to + # libjvm, but is no JVM: it must neither be reported nor block selection. + jli <- list.files(home_a, "^(lib)?jli[.](so|dylib|dll)$", recursive = TRUE, full.names = TRUE) + stopifnot(length(jli) >= 1L) + log("decoy", jli[1]) + preload(jli[1], bin = dirname(jli[1])) + stopifnot(is.null(mapped_jvm())) + Sys.setenv(JAVA_HOME = home_b) + start(home_b) + }, nonascii = { # No preload: the loader itself must open a path with spaces and non-ASCII names. Sys.setenv(JAVA_HOME = link_home(home_b)) @@ -243,12 +318,17 @@ switch(scenario, boot <- file.path(system.file("java", package = "rJava"), "boot") old <- Sys.getenv("PATH") if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(lib(home_a))), winslash = "\\"), old, sep = ";")) - created <- .C("rjava_test_create_jvm", lib(home_a), paste0("-Djava.class.path=", boot), result = 0L)$result + # Variant renamed: the host loads the runtime under a name no JVM uses. + # Not on Windows, where the JDK's DLLs then cannot import jvm.dll by name. + stopifnot(variant %in% c("", "renamed"), !(windows && variant == "renamed")) + path <- if (variant == "renamed") alias_lib(home_a) else lib(home_a) + created <- .C("rjava_test_create_jvm", path, paste0("-Djava.class.path=", boot), result = 0L)$result Sys.setenv(PATH = old) stopifnot(created == 0L) - mapped <- .Call(internal("RgetMappedJVM")) - stopifnot(!is.null(mapped), identical(mapped$vms, 1L), state()$state == "not-loaded") + expect_mapped(path, 1L) + stopifnot(state()$state == "not-loaded") Sys.setenv(JAVA_HOME = home_b) + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") start(home_a) stopifnot(state()$state == "attached") }, From 1b1001e134811c4ce22098714aa515dee33c6697 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 21:29:52 +0200 Subject: [PATCH 27/63] ci: follow symbolic-link Java homes when checking JVM exports --- .github/workflows/dynamic.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index feb615e..c2bb4e5 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -56,9 +56,10 @@ jobs: set -euo pipefail for home in "$JAVA_A" "$JAVA_B"; do echo "== $home"; "$home/bin/java" -version 2>&1 - find "$home" \( -name 'libjvm.*' -o -name jvm.dll \) -print + # The trailing slash follows a home that is a symbolic link (Linux). + find "$home/" \( -name 'libjvm.*' -o -name jvm.dll \) -print # djni recognizes a JVM library under any name by these exports. - lib="$(find "$home" \( -name 'libjvm.*' -o -name jvm.dll \) -path '*server*' -print -quit)" + lib="$(find "$home/" \( -name 'libjvm.*' -o -name jvm.dll \) -path '*server*' -print -quit)" case "$RUNNER_OS" in Linux) syms="$(nm -D --defined-only "$lib")" ;; macOS) syms="$(nm -gU "$lib")" ;; @@ -149,11 +150,11 @@ jobs: if [[ "$RUNNER_OS" != Windows ]]; then run renamed symlink; run hosted renamed; fi run decoy - name: Remove hard links left in the JDK directories - if: always() + if: always() && env.RJE_JAVA_HOME_A != '' shell: bash run: | # A mapped DLL cannot be deleted, so Windows leaves them behind until here. - find "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" -name 'rjava-alias-*' -print -delete + find "$RJE_JAVA_HOME_A/" "$RJE_JAVA_HOME_B/" -name 'rjava-alias-*' -print -delete - name: Embed R in a Java host through JRI if: runner.os != 'Windows' shell: bash From 4cd072c2eab80683ba3715397494f1b91d6a40e8 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 21:59:00 +0200 Subject: [PATCH 28/63] fix: avoid a helper collision in the macOS loader diagnostic --- src/djni.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/djni.c b/src/djni.c index a799612..93d2ea5 100644 --- a/src/djni.c +++ b/src/djni.c @@ -731,7 +731,7 @@ int test_jvm(const char *path) { #ifdef __APPLE__ #include -static void add_image(const struct mach_header* mh, intptr_t vmaddr_slide) { +static void debug_add_image(const struct mach_header* mh, intptr_t vmaddr_slide) { Dl_info DlInfo; dladdr(mh, &DlInfo); printf("LOADED: %s\n", DlInfo.dli_fname); @@ -746,7 +746,7 @@ static void rm_image(const struct mach_header* mh, intptr_t vmaddr_slide) { int main(int ac, char **av) { #ifdef __APPLE__ - _dyld_register_func_for_add_image(add_image); + _dyld_register_func_for_add_image(debug_add_image); _dyld_register_func_for_remove_image(rm_image); #endif From 84aec9685539f15bbb4e8d6e344bc49bbf19815b Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 22:13:15 +0200 Subject: [PATCH 29/63] fix: identify a mapped JVM library by its mapped file, not its current name The file at the name the loader recorded may have been deleted or replaced since the library was mapped, e.g. by a JDK upgrade. Comparing the requested file with it accepted B for a mapped A and reused A's handle. Identify the mapped file from the mapping itself: /proc/self/maps on Linux, the region vnode from proc_pidinfo on macOS, GetMappedFileNameW on Windows. Selecting the reported name of a library with a created VM still attaches to it. Add deleted, replaced and hosted replaced scenarios with two JDKs. --- .github/workflows/dynamic.yaml | 4 + src/djni.c | 152 ++++++++++++++++++++++++++------- tests-integration/README.md | 25 +++++- tests-integration/deferred.R | 73 +++++++++++++++- 4 files changed, 216 insertions(+), 38 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index c2bb4e5..c8797d0 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -149,6 +149,10 @@ jobs: # A renamed jvm.dll cannot start a VM: JDK DLLs import jvm.dll by name. if [[ "$RUNNER_OS" != Windows ]]; then run renamed symlink; run hosted renamed; fi run decoy + # The file of a mapped JVM library is deleted, or replaced by B's. + run replaced deleted + run replaced replaced + if [[ "$RUNNER_OS" != Windows ]]; then run hosted replaced; fi - name: Remove hard links left in the JDK directories if: always() && env.RJE_JAVA_HOME_A != '' shell: bash diff --git a/src/djni.c b/src/djni.c index 93d2ea5..2bd9f15 100644 --- a/src/djni.c +++ b/src/djni.c @@ -10,11 +10,15 @@ #include #else #include +#include #include #ifdef __APPLE__ #include +#include #elif defined(__linux__) #include +#include +#include #endif #endif #include @@ -142,6 +146,21 @@ static int same_file(const wchar_t *a, const wchar_t *b) { if (hb != INVALID_HANDLE_VALUE) CloseHandle(hb); return res; } +/* The file mapped for a module, as a path that opens it: the module's + recorded name may now be another file, if the mapped one was renamed and + replaced. GetMappedFileNameW gives an NT device path. */ +static wchar_t *mapped_path(HMODULE module) { + static const wchar_t root[] = L"\\\\?\\GLOBALROOT"; + DWORD size = 32768, skip = (DWORD) wcslen(root), n; + wchar_t *buf = malloc(sizeof(wchar_t) * (skip + size)); + if (!buf) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); return NULL; } + wcscpy(buf, root); + n = K32GetMappedFileNameW(GetCurrentProcess(), (LPVOID) module, buf + skip, size); + if (n && n < size - 1) return buf; + if (n) SetLastError(ERROR_INSUFFICIENT_BUFFER); + free(buf); + return NULL; +} static int in_image(DWORD rva, DWORD count, DWORD width, DWORD size) { return rva < size && count <= (size - rva) / width; } @@ -250,6 +269,15 @@ static void free_jvms(struct jvm_image *v, int n) { } free(v); } +/* The number of VMs created by a JVM module, or -1 if unknown. */ +static int created_vms(HMODULE module) { + JNI_GetCreatedJavaVMs_fnptr created; + JavaVM *vm; + jsize count = 0; + if (!owns_export(module, "JNI_GetCreatedJavaVMs")) return -1; + created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"); + return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; +} static int load_library(const char *path) { wchar_t *wide = NULL, *full = NULL, *bin = NULL, *slash; struct jvm_image *jvms = NULL; @@ -270,9 +298,17 @@ static int load_library(const char *path) { goto done; } if (n) { - /* Two mapped JVM modules are two runtimes, even if they are one file. */ - int same = (n == 1 && jvms[0].path) ? same_file(full, jvms[0].path) : -1; + /* Two mapped JVM modules are two runtimes, even if they are one file. + A created VM is the only one the process can run: the name recorded + for its module, as djni_mapped() reports it, selects that module. */ + wchar_t *mapped = NULL; char *name = jvms[0].path ? to_utf8(jvms[0].path) : NULL; + int same = -1; + if (n == 1 && name && created_vms(jvms[0].handle) > 0 && !strcmp(path, name)) + same = 1; + else if (n == 1 && jvms[0].path && (mapped = mapped_path(jvms[0].handle))) + same = same_file(full, mapped); + free(mapped); if (same == 1 && !is_jvm_name(jvms[0].path)) { /* The JDK's DLLs import jvm.dll by name, and the loader matches loaded modules by base name: starting this one would load @@ -339,13 +375,7 @@ const char *djni_mapped(int *vms) { return NULL; } /* With several JVM modules, the VM count of one is not the answer. */ - if (n == 1 && owns_export(jvms[0].handle, "JNI_GetCreatedJavaVMs")) { - JNI_GetCreatedJavaVMs_fnptr created = - (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(jvms[0].handle, "JNI_GetCreatedJavaVMs"); - JavaVM *vm; - jsize count = 0; - if (created && created(&vm, 1, &count) == JNI_OK) *vms = (int) count; - } + if (n == 1) *vms = created_vms(jvms[0].handle); mapped_name = jvms[0].path ? to_utf8(jvms[0].path) : NULL; if (!mapped_name) mapped_name = strdup("(unknown path)"); if (!mapped_name) { @@ -356,10 +386,53 @@ const char *djni_mapped(int *vms) { return mapped_name; } #else -static int same_file(const char *a, const char *b) { - struct stat sa, sb; - if (stat(a, &sa) || stat(b, &sb)) return -1; - return sa.st_dev == sb.st_dev && sa.st_ino == sb.st_ino; +/* Whether path names the file mapped at addr, by device and inode, which + hold across symbolic links, hard links and equivalent spellings. The name + the loader recorded is not used: the file there may have been deleted or + replaced since, while the mapping keeps the old file. Returns 1 for the + same file, 0 for another or no file, -1 if the mapped file is unknown, and + -2 on overlayfs, where a mismatch proves nothing (below). */ +static int same_as_mapped(const void *addr, const char *path) { + struct stat s; + int res = -1; + if (stat(path, &s)) return 0; +#ifdef __APPLE__ + { + struct proc_regionwithpathinfo r; + uint64_t a = (uint64_t) (uintptr_t) addr; + /* The region returned starts after addr if nothing is mapped there. */ + if (addr && proc_pidinfo(getpid(), PROC_PIDREGIONPATHINFO, a, &r, sizeof(r)) == (int) sizeof(r) && + a >= r.prp_prinfo.pri_address && a - r.prp_prinfo.pri_address < r.prp_prinfo.pri_size && + r.prp_vip.vip_vi.vi_stat.vst_ino) + res = r.prp_vip.vip_vi.vi_stat.vst_dev == (uint32_t) s.st_dev && + r.prp_vip.vip_vi.vi_stat.vst_ino == (uint64_t) s.st_ino; + } +#elif defined(__linux__) + { + FILE *f = addr ? fopen("/proc/self/maps", "r") : NULL; + char *line = NULL; + size_t size = 0; + unsigned long long a = (uintptr_t) addr, start, end, ino; + unsigned int maj, min; + struct statfs fs; + if (f) { + while (getline(&line, &size, f) > 0) + if (sscanf(line, "%llx-%llx %*s %*s %x:%x %llu", &start, &end, &maj, &min, &ino) == 5 && + a >= start && a < end) { + if (ino) + res = major(s.st_dev) == maj && minor(s.st_dev) == min && + (unsigned long long) s.st_ino == ino; + break; + } + free(line); + fclose(f); + } + /* Before Linux 6.8, /proc/self/maps shows the device and inode of the + layer file under an overlayfs mapping, but stat() the overlay device. */ + if (!res && !statfs(path, &fs) && fs.f_type == 0x794c7630) res = -2; + } +#endif + return res; } static int is_jvm_name(const char *name) { const char *base = strrchr(name, '/'); @@ -462,7 +535,7 @@ static int owns(void *handle, const struct image *im, const char *sym) { #endif } #endif -struct jvm_image { char *name; void *handle; int owns_created; }; +struct jvm_image { char *name; void *handle; int owns_created; const void *addr; }; /* Lists the JVM images mapped into the process, each with a reference if one can be taken without loading anything. An image is a JVM if it has a JVM library name, even if malformed, or if it defines the JNI invocation @@ -494,6 +567,11 @@ static int find_jvms(struct jvm_image **out) { if (jvm) { v[n].name = l.v[i].name; l.v[i].name = NULL; +#ifdef __APPLE__ + v[n].addr = l.v[i].header; +#else + v[n].addr = l.v[i].nseg ? (const void *) l.v[i].seg[0] : NULL; +#endif v[n++].handle = handle; } else if (handle) dlclose(handle); @@ -511,11 +589,18 @@ static void free_jvms(struct jvm_image *v, int n) { } free(v); } -/* Device and inode identify the file across symbolic links, hard links and - equivalent spellings; a requested file that cannot be found differs. */ +/* The number of VMs created by a JVM image, or -1 if unknown. */ +static int created_vms(const struct jvm_image *im) { + JNI_GetCreatedJavaVMs_fnptr created; + JavaVM *vm; + jsize count = 0; + if (!im->owns_created) return -1; + *(void **) &created = dlsym(im->handle, "JNI_GetCreatedJavaVMs"); + return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; +} static int load_library(const char *path) { struct jvm_image *jvms; - int n = find_jvms(&jvms), res = 0; + int n = find_jvms(&jvms), res = -4; if (n < 0) { snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process. Use a new R process."); return -4; @@ -523,24 +608,33 @@ static int load_library(const char *path) { if (n > 1) { snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", n, jvms[0].name); - res = -4; } else if (n == 1) { - if (same_file(jvms[0].name, path) != 1) { + /* A created VM is the only one the process can run: the name the + loader recorded for its image, as djni_mapped() reports it, selects + that image, whatever file is at that path now. */ + int same = (created_vms(&jvms[0]) > 0 && !strcmp(path, jvms[0].name)) ? 1 : + same_as_mapped(jvms[0].addr, path); + if (!same) snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", jvms[0].name); - res = -4; - } else if (!jvms[0].handle) { + else if (same == -2) + snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s, because overlayfs before Linux 6.8 reports another file for the mapping. Use a new R process.", + path, jvms[0].name); + else if (same < 0) + snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s. Use a new R process.", path, jvms[0].name); + else if (!jvms[0].handle) snprintf(last_error, sizeof(last_error), "Cannot reference the JVM library already mapped: %s. Use a new R process.", jvms[0].name); - res = -4; - } else { + else { /* Reference the mapped image itself, so an alias cannot map another copy. */ jni_dl = jvms[0].handle; jvms[0].handle = NULL; + res = 0; } } else if (!(jni_dl = dlopen(path, RTLD_LOCAL | RTLD_NOW))) { const char *error = dlerror(); snprintf(last_error, sizeof(last_error), "%s", error ? error : "dynamic loader error"); res = -2; - } + } else + res = 0; free_jvms(jvms, n); return res; } @@ -556,15 +650,7 @@ const char *djni_mapped(int *vms) { return NULL; } /* With several JVM images, the VM count of one is not the answer. */ - if (n == 1 && jvms[0].owns_created) { - JNI_GetCreatedJavaVMs_fnptr created; - *(void **) &created = dlsym(jvms[0].handle, "JNI_GetCreatedJavaVMs"); - if (created) { - JavaVM *vm; - jsize count = 0; - if (created(&vm, 1, &count) == JNI_OK) *vms = (int) count; - } - } + if (n == 1) *vms = created_vms(&jvms[0]); if (n) { mapped_name = jvms[0].name; jvms[0].name = NULL; diff --git a/tests-integration/README.md b/tests-integration/README.md index 951cccc..38c4fc9 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -44,7 +44,15 @@ forwards them to libjvm. Every HotSpot JDK in the matrix exports the three symbols; CI checks this for each runtime. Identity is then the file, not its name: device and inode on Linux and -macOS, volume serial and file ID on Windows. So symbolic links, junctions, +macOS, volume serial and file ID on Windows. The mapped side is the file the +image was mapped from, not the file now at the name the loader recorded, +which may have been deleted or replaced since: on Linux the mapping's entry +in `/proc/self/maps`, on macOS the vnode of the mapped region +(`proc_pidinfo` with `PROC_PIDREGIONPATHINFO`), on Windows the file +`GetMappedFileNameW` names for the module, opened by its device path. So an +upgrade that replaces the loaded library's file is not taken for the new +runtime, and the same file is still accepted under any of its names. +Symbolic links, junctions, hard links, 8.3 short names, other spellings and renamed copies of the loaded library are recognized: the same file is accepted, and djni references the mapped image rather than loading the alias. On Windows the @@ -56,6 +64,9 @@ unverifiable file is rejected, and so is any selection while more than one JVM image is mapped (`RgetMappedJVM` then reports one of them with `vms` `-1`). If the loaded images cannot be listed, selection fails and `RgetMappedJVM` signals an error, so callers do not mistake it for "none". +Once the mapped library has created a VM, the only runtime the process can +run, selecting the exact name `RgetMappedJVM` reports attaches to it without +comparing files, as `.jinit()` does; any other name is compared as above. ## Selected libraries are not unloaded @@ -125,6 +136,8 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `alias link/spelling/short` | the same preloaded library through a symbolic link or junction with spaces and non-ASCII characters, an alternative spelling, and an 8.3 name (Windows) | | `renamed copy/hardlink/symlink` | a JVM library mapped under a name no JVM uses: a copy of B rejects A and B's own library; a hard link of A in its directory (and a symbolic link, Linux and macOS) rejects B, then the same file starts A (Windows: is rejected as mapped under another name) | | `hosted renamed` | native code creates the VM from the hard link; it is reported with one VM, B is rejected, and `.jinit()` attaches (Linux and macOS) | +| `replaced deleted/replaced` | the file of the mapped library is deleted, or replaced by a hard link of B's (Windows: a mapped DLL cannot be deleted, so it is renamed in a copy of A's home and B's linked as `jvm.dll`): B, the replaced name and `.jinit()` with `JAVA_HOME` naming B are rejected, A's own file is accepted, and after replacement A starts | +| `hosted replaced` | the same replacement after a host created the VM: B is rejected, `.jinit()` attaches to A (Linux and macOS) | | `decoy` | the JDK's `jli` library (JNI exports on macOS, no `JVM_` exports) is not reported and does not block selecting B | | `nonascii` | loading through such a path without a preload | | `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | @@ -171,8 +184,14 @@ from the repository root, one process per scenario. lists no mapped libraries, so it cannot reject a conflict. It cannot see libraries in another `dlmopen()` namespace, and identifies a renamed JVM only by the exports above (a JVM without `JVM_CurrentTimeMillis` must keep its - usual name). It compares the file now at the recorded path: a mapped - library whose file was deleted or replaced is rejected, not matched. dyld + usual name). If the mapped file cannot be identified (no `/proc` on + Linux, a failed query), selection is rejected. On overlayfs (e.g. Docker) + with Linux before 6.8, `/proc/self/maps` reports the layer file's device, + while `stat()` reports the overlay's, so a preloaded library without a + created VM is rejected as unverifiable even when it is the same file; + attaching to a created VM through its reported name is unaffected. This + follows from the kernel source (`show_map_vma` uses `file_user_inode` + only from 6.8) and is not tested. dyld records a library loaded through a symbolic link by its target, so on macOS the `symlink` variant shows the target name. - Not covered: 32-bit Windows, Windows on ARM, Linux on ARM, musl, OpenJ9 and diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 1634691..b7708d3 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -265,6 +265,69 @@ switch(scenario, } } }, + replaced = { + # The file the mapped JVM library was loaded from is deleted, or replaced + # by B's library under the same name. Identity is that of the mapped file, + # not of the file now at its name: B must be rejected, A still accepted. + stopifnot(variant %in% c("deleted", "replaced")) + if (windows) { + # A mapped DLL cannot be deleted, but may be renamed. JDK DLLs import + # jvm.dll by name, so replace jvm.dll in a copy of A's home. + copy <- file.path("C:/rjava-paths", paste0("replaced-", Sys.getpid())) + dir.create(copy, recursive = TRUE) + stopifnot(file.copy(normalizePath(home_a), copy, recursive = TRUE)) + home <- file.path(copy, basename(normalizePath(home_a))) + path <- file.path(home, relative(home_a)) + preload(path) + expect_mapped(path, 0L) + stopifnot(unlink(path) != 0L || file.exists(path)) + log("Windows refused deleting the mapped", path) + if (variant == "deleted") { + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") + internal(".jloadJVM")(path) + stopifnot(state()$state == "none") + start(home) + } else { + moved <- file.path(dirname(path), "jvm-mapped.dll") + stopifnot(file.rename(path, moved), file.link(lib(home_b), path)) + log("renamed the mapped DLL to", moved, "and linked B's in its place") + expect_mapped(NULL, 0L) + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") + fails(internal(".jloadJVM")(path), "already mapped") + Sys.setenv(JAVA_HOME = home_b) + fails(rJava::.jinit(), "already mapped") + internal(".jloadJVM")(moved) + stopifnot(state()$state == "none") + start(home) + expect_mapped(NULL, 1L) + } + } else { + path <- alias_lib(home_a) + preload(path) + expect_mapped(path, 0L) + stopifnot(unlink(path) == 0L, !file.exists(path)) + if (variant == "replaced") { + # A hard link, so the old check (the file now at the mapped name is B's + # library) accepted B and kept A's handle. + stopifnot(file.link(lib(home_b), path)) + log("replaced", path, "by a hard link of", lib(home_b)) + } + fails(internal(".jloadJVM")(lib(home_b)), "already mapped") + fails(internal(".jloadJVM")(path), "already mapped") + Sys.setenv(JAVA_HOME = home_b) + fails(rJava::.jinit(), "already mapped") + # A's library is the mapped file, whatever is now at the mapped name. + internal(".jloadJVM")(lib(home_a)) + stopifnot(state()$state == "none") + expect_mapped(path, 0L) + # The JVM finds its home from its own file name, which no longer exists + # if deleted. + if (variant == "replaced") { + start(home_a) + expect_mapped(path, 1L) + } + } + }, decoy = { # The Java launcher library (jli) has JNI exports on macOS that forward to # libjvm, but is no JVM: it must neither be reported nor block selection. @@ -320,12 +383,18 @@ switch(scenario, if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(lib(home_a))), winslash = "\\"), old, sep = ";")) # Variant renamed: the host loads the runtime under a name no JVM uses. # Not on Windows, where the JDK's DLLs then cannot import jvm.dll by name. - stopifnot(variant %in% c("", "renamed"), !(windows && variant == "renamed")) - path <- if (variant == "renamed") alias_lib(home_a) else lib(home_a) + # Variant replaced: B's library then replaces that name; rJava attaches + # to the created VM through its recorded name, but B is rejected. + stopifnot(variant %in% c("", "renamed", "replaced"), !(windows && variant != "")) + path <- if (variant != "") alias_lib(home_a) else lib(home_a) created <- .C("rjava_test_create_jvm", path, paste0("-Djava.class.path=", boot), result = 0L)$result Sys.setenv(PATH = old) stopifnot(created == 0L) expect_mapped(path, 1L) + if (variant == "replaced") { + stopifnot(unlink(path) == 0L, file.link(lib(home_b), path)) + log("replaced", path, "by a hard link of", lib(home_b)) + } stopifnot(state()$state == "not-loaded") Sys.setenv(JAVA_HOME = home_b) fails(internal(".jloadJVM")(lib(home_b)), "already mapped") From 731a78b261072a7cc789a0d7e7d1fb5ef7437f28 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 22:36:42 +0200 Subject: [PATCH 30/63] fix: attach to a created JVM without accepting its recorded name as a path .jloadJVM(path) accepted the name the loader recorded for a JVM image that had created a VM without comparing files, so a public call with that name succeeded after another runtime replaced the file there. Explicit paths are now always compared with the mapped file. .jinit() attaches through a new internal RattachJVM, which takes no path: it requires exactly one mapped JVM image with a created VM and selects it through the reference djni holds. --- .github/workflows/dynamic.yaml | 3 +- R/jfirst.R | 2 +- R/jinit.R | 6 +- man/jinit.Rd | 6 +- src/Rglue.c | 24 ++++++-- src/djni.c | 99 ++++++++++++++++++++++++++------ src/djni.h | 5 ++ tests-integration/JriAttach.java | 6 +- tests-integration/README.md | 40 +++++++++---- tests-integration/deferred.R | 47 +++++++++++---- 10 files changed, 186 insertions(+), 52 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index c8797d0..e4e2167 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -136,6 +136,7 @@ jobs: for scenario in select retry badinput unload preloaded nonascii fallback laterload hosted; do run "$scenario" done + run hosted explicit # Windows source installs build no JRI (pre-existing upstream build defects). if [[ "$RUNNER_OS" != Windows ]]; then run jengine; fi run preloaded global @@ -152,7 +153,7 @@ jobs: # The file of a mapped JVM library is deleted, or replaced by B's. run replaced deleted run replaced replaced - if [[ "$RUNNER_OS" != Windows ]]; then run hosted replaced; fi + run hosted replaced - name: Remove hard links left in the JDK directories if: always() && env.RJE_JAVA_HOME_A != '' shell: bash diff --git a/R/jfirst.R b/R/jfirst.R index 7c20d37..e757dae 100644 --- a/R/jfirst.R +++ b/R/jfirst.R @@ -22,7 +22,7 @@ "RgetShortArrayCont", "RgetStringArrayCont", "RidenticalRef", "RgetSimpleClassNames", "RisAssignableFrom", "RpollException", "RsetField", "RthrowException", "javaObjectCache", "initRJavaTools", "newRJavaLookupTable", "useDynamicSymbols", - "RgetJVMstate", "RloadJVM", "RunloadJVM", "RgetMappedJVM", + "RgetJVMstate", "RloadJVM", "RunloadJVM", "RgetMappedJVM", "RattachJVM", # .External "RcreateObject", "RgetStringValue", "RinitJVM", "RtoString", "RcallMethod", # .C diff --git a/R/jinit.R b/R/jinit.R index 9ce9932..27edb9e 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -18,10 +18,12 @@ .jfindAndLoadJVM <- function() { ## A Java application embedding R (e.g. JRI) has already created a JVM. It ## is the only runtime this process can use, so attach to it whatever Java - ## home is configured, as rJava linked against the JVM library did. + ## home is configured, as rJava linked against the JVM library did. The + ## mapped image is selected itself, not through its name: .jloadJVM(path) + ## accepts only a path that names the mapped file. mapped <- .Call(RgetMappedJVM) if (!is.null(mapped) && isTRUE(mapped$vms > 0L)) - return(.jloadJVM(mapped$path)) + return(.Call(RattachJVM)) home <- .find.java.runtime() if (!nzchar(home)) stop("Cannot find Java. Set JAVA_HOME to an existing Java home before initializing the JVM.") diff --git a/man/jinit.Rd b/man/jinit.Rd index 0ccdef4..92ac76b 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -107,8 +107,10 @@ loaded, \code{.jinit} fails and can be called again after correcting the setting. Once a run-time library is loaded, it remains selected for the life of the R process: a different JVM requires a new R - process. If another JVM library is already mapped into the process, - but no JVM was created in it, only that same library can be selected. + process. If a JVM library is already mapped into the process, a JVM + library path, such as one given to \code{.jloadJVM}, selects it only if + it names that same file, also when a JVM was created in it; attaching to + a created JVM is done by \code{.jinit} without a path. NOTE: \code{.jinit()} can be called implicitly by other functions such as \code{\link{.jpackage}} or \code{\link{J}}. diff --git a/src/Rglue.c b/src/Rglue.c index 7609e4d..b826d1c 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1137,6 +1137,15 @@ REPC SEXP RgetJVMstate(void) { return res; } +static void load_error(int res) { + switch(res) { + case -1: Rf_error("JVM run-time is already loaded"); + case -2: Rf_error("Cannot load JVM run-time (%s)", djni_last_error() ? djni_last_error() : "unsuccessful"); + case -3: Rf_error("JVM run-time does not have required entry points"); + case -4: Rf_error("%s", djni_last_error()); + } +} + REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { const char *path; int res, silent = Rf_asInteger(sSilent); @@ -1152,12 +1161,15 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { res = djni_load(path); if (silent) return Rf_ScalarInteger(res); - switch(res) { - case -1: Rf_error("JVM run-time is already loaded"); - case -2: Rf_error("Cannot load JVM run-time (%s)", djni_last_error() ? djni_last_error() : "unsuccessful"); - case -3: Rf_error("JVM run-time does not have required entry points"); - case -4: Rf_error("%s", djni_last_error()); - } + load_error(res); + return Rf_ScalarLogical(1); +} + +/* Selects the JVM library that created the VM already running in the + process, e.g. in a Java application embedding R. Unlike RloadJVM, no path + names it: the file at its recorded name may have been replaced since. */ +REPC SEXP RattachJVM(void) { + load_error(djni_attach()); return Rf_ScalarLogical(1); } diff --git a/src/djni.c b/src/djni.c index 2bd9f15..864cc8b 100644 --- a/src/djni.c +++ b/src/djni.c @@ -299,14 +299,12 @@ static int load_library(const char *path) { } if (n) { /* Two mapped JVM modules are two runtimes, even if they are one file. - A created VM is the only one the process can run: the name recorded - for its module, as djni_mapped() reports it, selects that module. */ + A path selects the mapped module only if it names the mapped file, + also once a VM was created: djni_attach() attaches without a path. */ wchar_t *mapped = NULL; char *name = jvms[0].path ? to_utf8(jvms[0].path) : NULL; int same = -1; - if (n == 1 && name && created_vms(jvms[0].handle) > 0 && !strcmp(path, name)) - same = 1; - else if (n == 1 && jvms[0].path && (mapped = mapped_path(jvms[0].handle))) + if (n == 1 && jvms[0].path && (mapped = mapped_path(jvms[0].handle))) same = same_file(full, mapped); free(mapped); if (same == 1 && !is_jvm_name(jvms[0].path)) { @@ -359,6 +357,35 @@ static int load_library(const char *path) { free(wide); free(full); free(bin); return res; } +/* Takes a reference to the only mapped JVM module if it has created a VM. + The module's name is not checked: no VM is started, so the JDK's DLLs, + which import jvm.dll by name, are not loaded again. */ +static int attach_library(void) { + struct jvm_image *jvms; + char *name; + int n = find_jvms(&jvms), res = -4; + if (n < 0) { + save_win_error("Cannot list the modules of", "this process", GetLastError()); + return -4; + } + name = (n && jvms[0].path) ? to_utf8(jvms[0].path) : NULL; + if (!n) + snprintf(last_error, sizeof(last_error), "No JVM library is mapped into this process."); + else if (n > 1) + snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", + n, name ? name : "unknown path"); + else if (created_vms(jvms[0].handle) <= 0) + snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", + name ? name : "unknown path"); + else { + jni_dl = jvms[0].handle; + jvms[0].handle = NULL; + res = 0; + } + free(name); + free_jvms(jvms, n); + return res; +} const char *djni_mapped(int *vms) { struct jvm_image *jvms; int n; @@ -609,11 +636,9 @@ static int load_library(const char *path) { snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", n, jvms[0].name); } else if (n == 1) { - /* A created VM is the only one the process can run: the name the - loader recorded for its image, as djni_mapped() reports it, selects - that image, whatever file is at that path now. */ - int same = (created_vms(&jvms[0]) > 0 && !strcmp(path, jvms[0].name)) ? 1 : - same_as_mapped(jvms[0].addr, path); + /* A path selects the mapped image only if it names the mapped file, + also once a VM was created: djni_attach() attaches without a path. */ + int same = same_as_mapped(jvms[0].addr, path); if (!same) snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", jvms[0].name); else if (same == -2) @@ -638,6 +663,32 @@ static int load_library(const char *path) { free_jvms(jvms, n); return res; } +/* Takes the reference held to the only mapped JVM image if it has created + a VM. Its recorded name is not interpreted: the file there may have been + deleted or replaced, and on overlayfs the mapped file may be unverifiable, + but the created VM is the image's own. */ +static int attach_library(void) { + struct jvm_image *jvms; + int n = find_jvms(&jvms), res = -4; + if (n < 0) { + snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process. Use a new R process."); + return -4; + } + if (!n) + snprintf(last_error, sizeof(last_error), "No JVM library is mapped into this process."); + else if (n > 1) + snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", + n, jvms[0].name); + else if (created_vms(&jvms[0]) <= 0) + snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", jvms[0].name); + else { + jni_dl = jvms[0].handle; + jvms[0].handle = NULL; + res = 0; + } + free_jvms(jvms, n); + return res; +} const char *djni_mapped(int *vms) { struct jvm_image *jvms; int n; @@ -676,13 +727,8 @@ static int load_sym(const char *sym, void **ptr) { return 0; } -/* returns: 0 = success, -1 = already loaded, -2 = load error, -3 = symbols - not found, -4 = another JVM library is already mapped */ -int djni_load(const char *path) { - int res; - last_error[0] = 0; - if (jni_dl) return -1; - if ((res = load_library(path))) return res; +/* Resolves the JNI entry points of jni_dl, releasing it if one is missing. */ +static int load_syms(void) { if (load_sym("JNI_GetDefaultJavaVMInitArgs", (void **) &JNI_GetDefaultJavaVMInitArgs_fn) || load_sym("JNI_CreateJavaVM", (void **) &JNI_CreateJavaVM_fn) || load_sym("JNI_GetCreatedJavaVMs", (void **) &JNI_GetCreatedJavaVMs_fn)) { @@ -691,6 +737,25 @@ int djni_load(const char *path) { } return 0; } + +/* returns: 0 = success, -1 = already loaded, -2 = load error, -3 = symbols + not found, -4 = another JVM library is already mapped */ +int djni_load(const char *path) { + int res; + last_error[0] = 0; + if (jni_dl) return -1; + if ((res = load_library(path))) return res; + return load_syms(); +} +/* returns: 0 = success, -1 = already loaded, -3 = symbols not found, -4 = no + single mapped JVM library with a created VM */ +int djni_attach(void) { + int res; + last_error[0] = 0; + if (jni_dl) return -1; + if ((res = attach_library())) return res; + return load_syms(); +} int djni_unload(void) { if (!jni_dl) return -1; #ifdef _WIN32 diff --git a/src/djni.h b/src/djni.h index 9affa03..9a7d8f2 100644 --- a/src/djni.h +++ b/src/djni.h @@ -12,7 +12,12 @@ /* returned if no run-time is loaded */ #define JNI_NO_DJNI -99 +/* Selects the JVM library at path. If one is already mapped, path must name + the mapped file, whether or not it has created a VM. */ int djni_load(const char *path); +/* Selects the only JVM library mapped into the process if it has created a + VM, by its mapped image: no path is interpreted and nothing is loaded. */ +int djni_attach(void); int djni_unload(void); const char* djni_last_error(void); int djni_loaded(void); diff --git a/tests-integration/JriAttach.java b/tests-integration/JriAttach.java index 090cec7..c84ae89 100644 --- a/tests-integration/JriAttach.java +++ b/tests-integration/JriAttach.java @@ -19,8 +19,12 @@ static void run() { System.err.println("FAIL: cannot start embedded R"); System.exit(2); } + /* An explicit path must name the mapped file: JAVA_HOME's runtime is rejected. */ REXP result = engine.eval("tryCatch({" + - " library(rJava); .jinit();" + + " library(rJava);" + + " b <- try(.jloadJVM(rJava:::.find.jvm.library(Sys.getenv('JAVA_HOME'))[1]), silent = TRUE);" + + " if (!inherits(b, 'try-error') || .jvmState()$state != 'not-loaded') stop('JAVA_HOME runtime was selected');" + + " .jinit();" + " paste('OK', .jvmState()$state, .jcall('java/lang/System', 'S', 'getProperty', 'java.home')," + " !is.jnull(.jengine()), sep = '|')" + "}, error = function(e) paste('ERROR', conditionMessage(e), sep = '|'))"); diff --git a/tests-integration/README.md b/tests-integration/README.md index 38c4fc9..379cec0 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -29,8 +29,9 @@ then a recursive search. An existing Java home without a JVM library is an error. Every failure before a library is loaded leaves the state `"not-loaded"`, so the setting can be corrected and `.jinit()` retried. -If another JVM library is mapped into the process without a created VM, -only the same file can be selected. djni finds mapped JVM libraries among +If a JVM library is already mapped into the process, a path given to +`.jloadJVM()`, or found from a Java home, selects it only if it names the +mapped file, whether or not that library has created a VM. djni finds mapped JVM libraries among all images the dynamic loader lists (`dl_iterate_phdr` on Linux, dyld on macOS, `EnumProcessModules` on Windows), without loading anything. An image is a JVM library if it has a JVM library name (`libjvm.so`, `libjvm.dylib`, @@ -64,9 +65,23 @@ unverifiable file is rejected, and so is any selection while more than one JVM image is mapped (`RgetMappedJVM` then reports one of them with `vms` `-1`). If the loaded images cannot be listed, selection fails and `RgetMappedJVM` signals an error, so callers do not mistake it for "none". -Once the mapped library has created a VM, the only runtime the process can -run, selecting the exact name `RgetMappedJVM` reports attaches to it without -comparing files, as `.jinit()` does; any other name is compared as above. + +### Explicit selection and attaching + +Selecting a path and attaching to a created VM are separate operations. +`.jloadJVM(path)` always compares files as above; no name is exempt, not +even the one `RgetMappedJVM` reports for a created VM, since the file now +at that name may be another runtime. `.jinit()` instead attaches through +the internal `RattachJVM`, which takes no path: it requires exactly one +mapped JVM image, with at least one created VM, and selects that image +through the reference djni holds to it, without loading anything. So it +attaches even if the file at the image's recorded name was deleted or +replaced, or cannot be identified (overlayfs, below). It never selects an +image without a created VM, never resolves an ambiguity between several +images, and fails if the image lacks a JNI entry point. On Windows it does +not apply the `jvm.dll` name rule, which concerns starting a VM (untested: +the JDK cannot start a VM from a renamed `jvm.dll`). No +exported function attaches without a path. ## Selected libraries are not unloaded @@ -132,19 +147,19 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `jni-abi.R` | `src/jni.h` type sizes, invocation struct fields and every function table offset match each JDK's `jni.h` | | `select`, `retry`, `badinput` | selection after namespace load; recovery after an empty home, missing file, library without JNI exports, empty, over-long and relative paths | | `unload` | unloading or changing a selected, unstarted library fails | -| `preloaded`, `preloaded global` | a library mapped outside rJava (also with global symbols) rejects another runtime and permits the same one | +| `preloaded`, `preloaded global` | a library mapped outside rJava (also with global symbols) rejects another runtime and permits the same one; `RattachJVM` fails with nothing mapped and with no created VM | | `alias link/spelling/short` | the same preloaded library through a symbolic link or junction with spaces and non-ASCII characters, an alternative spelling, and an 8.3 name (Windows) | | `renamed copy/hardlink/symlink` | a JVM library mapped under a name no JVM uses: a copy of B rejects A and B's own library; a hard link of A in its directory (and a symbolic link, Linux and macOS) rejects B, then the same file starts A (Windows: is rejected as mapped under another name) | | `hosted renamed` | native code creates the VM from the hard link; it is reported with one VM, B is rejected, and `.jinit()` attaches (Linux and macOS) | | `replaced deleted/replaced` | the file of the mapped library is deleted, or replaced by a hard link of B's (Windows: a mapped DLL cannot be deleted, so it is renamed in a copy of A's home and B's linked as `jvm.dll`): B, the replaced name and `.jinit()` with `JAVA_HOME` naming B are rejected, A's own file is accepted, and after replacement A starts | -| `hosted replaced` | the same replacement after a host created the VM: B is rejected, `.jinit()` attaches to A (Linux and macOS) | +| `hosted replaced` | the same replacement after a host created the VM (Windows: renamed in a copy of A's home, as above): `.jloadJVM()` rejects B, the replaced name and the name `RgetMappedJVM` reports, all before `.jinit()` attaches to A | | `decoy` | the JDK's `jli` library (JNI exports on macOS, no `JVM_` exports) is not reported and does not block selecting B | | `nonascii` | loading through such a path without a preload | | `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | | `laterload` | the later native loads above | -| `hosted` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK | +| `hosted`, `hosted explicit` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK; `.jloadJVM()` rejects B, and with `explicit` selects A's own file first | | every scenario | before anything is mapped, `RgetMappedJVM` is `NULL`: rJava's own JNI wrappers are not taken for a JVM | -| `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where rJava attaches to the host JVM (Linux and macOS) | +| `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where `.jloadJVM()` rejects the `JAVA_HOME` runtime and rJava attaches to the host JVM (Linux and macOS) | | `dll-search.c` | Windows DLL search state after failed and successful loads | | downstream | rJavaEnv and two minimal consumer packages (current pair only) | @@ -187,9 +202,10 @@ from the repository root, one process per scenario. usual name). If the mapped file cannot be identified (no `/proc` on Linux, a failed query), selection is rejected. On overlayfs (e.g. Docker) with Linux before 6.8, `/proc/self/maps` reports the layer file's device, - while `stat()` reports the overlay's, so a preloaded library without a - created VM is rejected as unverifiable even when it is the same file; - attaching to a created VM through its reported name is unaffected. This + while `stat()` reports the overlay's, so `.jloadJVM(path)` rejects a + mapped library as unverifiable even when it is the same file, with or + without a created VM; `.jinit()` attaching to a created VM is unaffected, + as it compares no files. This follows from the kernel source (`show_map_vma` uses `file_user_inode` only from 6.8) and is not tested. dyld records a library loaded through a symbolic link by its target, so on diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index b7708d3..ddc0ffd 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -192,9 +192,13 @@ switch(scenario, # permits the same one, without starting a VM. With global symbols, its # JNI exports must not interpose on rJava's own loader wrappers. stopifnot(variant %in% c("", "global")) + # The internal attach used by .jinit() selects only a created VM. + attach <- function() .Call(internal("RattachJVM")) + fails(attach(), "No JVM library is mapped") preload(lib(home_a), local = variant != "global") mapped <- .Call(internal("RgetMappedJVM")) stopifnot(!is.null(mapped), identical(mapped$vms, 0L)) + fails(attach(), "created no VM") Sys.setenv(JAVA_HOME = home_b) fails(rJava::.jinit(), "already mapped") fails(internal(".jloadJVM")(lib(home_b)), "already mapped") @@ -375,30 +379,53 @@ switch(scenario, # Native code in this process created a JVM before rJava was used. rJava # must attach to it, although JAVA_HOME names another runtime. A host # provides rJava's class loader on its class path (JDK 9+ forbids adding it). + # Variant explicit: .jloadJVM() selects A's own file before .jinit(). + # Variant renamed: the host loads the runtime under a name no JVM uses. + # Not on Windows, where the JDK's DLLs then cannot import jvm.dll by name. + # Variant replaced: B's library then replaces that name (on Windows, the + # jvm.dll of a copy of A's home, renamed as in replaced). .jinit() attaches + # to the created VM, but .jloadJVM() rejects that name and B: a path + # selects only the file mapped, also when a VM was created. + stopifnot(variant %in% c("", "explicit", "renamed", "replaced"), !(windows && variant == "renamed")) helper <- Sys.getenv("RJE_JVMHOST") stopifnot(file.exists(helper)) dyn.load(helper) boot <- file.path(system.file("java", package = "rJava"), "boot") + home <- home_a + if (windows && variant == "replaced") { + copy <- file.path("C:/rjava-paths", paste0("hosted-", Sys.getpid())) + dir.create(copy, recursive = TRUE) + stopifnot(file.copy(normalizePath(home_a), copy, recursive = TRUE)) + home <- file.path(copy, basename(normalizePath(home_a))) + path <- file.path(home, relative(home_a)) + } else path <- if (variant %in% c("renamed", "replaced")) alias_lib(home_a) else lib(home_a) old <- Sys.getenv("PATH") - if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(lib(home_a))), winslash = "\\"), old, sep = ";")) - # Variant renamed: the host loads the runtime under a name no JVM uses. - # Not on Windows, where the JDK's DLLs then cannot import jvm.dll by name. - # Variant replaced: B's library then replaces that name; rJava attaches - # to the created VM through its recorded name, but B is rejected. - stopifnot(variant %in% c("", "renamed", "replaced"), !(windows && variant != "")) - path <- if (variant != "") alias_lib(home_a) else lib(home_a) + if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(path)), winslash = "\\"), old, sep = ";")) created <- .C("rjava_test_create_jvm", path, paste0("-Djava.class.path=", boot), result = 0L)$result Sys.setenv(PATH = old) stopifnot(created == 0L) expect_mapped(path, 1L) + # The name djni_mapped() reports, which .jinit() once passed to .jloadJVM(). + recorded <- mapped_jvm()$path if (variant == "replaced") { - stopifnot(unlink(path) == 0L, file.link(lib(home_b), path)) + if (windows) { + moved <- file.path(dirname(path), "jvm-mapped.dll") + stopifnot(file.rename(path, moved), file.link(lib(home_b), path)) + } else stopifnot(unlink(path) == 0L, file.link(lib(home_b), path)) log("replaced", path, "by a hard link of", lib(home_b)) + expect_mapped(if (!windows) path, 1L) } stopifnot(state()$state == "not-loaded") Sys.setenv(JAVA_HOME = home_b) - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - start(home_a) + fails(rJava::.jloadJVM(lib(home_b)), "already mapped") + if (variant == "replaced") { + fails(rJava::.jloadJVM(recorded), "already mapped") + fails(rJava::.jloadJVM(path), "already mapped") + } + if (variant == "explicit") { + stopifnot(isTRUE(rJava::.jloadJVM(lib(home_a))), state()$state == "detached") + } + start(home) stopifnot(state()$state == "attached") }, jengine = { From 67fc4fc6858537d2f4a3f0292e13cd4168a7e23f Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sat, 17 Oct 2026 22:50:55 +0200 Subject: [PATCH 31/63] docs: distinguish automatic discovery from explicit selection [skip ci] --- tests-integration/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests-integration/README.md b/tests-integration/README.md index 379cec0..862dd19 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -9,8 +9,8 @@ safety for every system. ## Contract Loading the rJava namespace, including through another package's Imports, -does not select, load, or initialize Java. The first `.jinit()` (or -`.jloadJVM()`) selects a JVM library, in this order: +does not select, load, or initialize Java. The first `.jinit()` selects a +JVM library in this order: 1. A JVM already created in the process, e.g. by a Java application embedding R through JRI: rJava attaches to it whatever Java home is @@ -80,8 +80,8 @@ replaced, or cannot be identified (overlayfs, below). It never selects an image without a created VM, never resolves an ambiguity between several images, and fails if the image lacks a JNI entry point. On Windows it does not apply the `jvm.dll` name rule, which concerns starting a VM (untested: -the JDK cannot start a VM from a renamed `jvm.dll`). No -exported function attaches without a path. +the JDK cannot start a VM from a renamed `jvm.dll`). The native attach entry +point has no exported R wrapper. ## Selected libraries are not unloaded From 88e73cea3714546f6eef75fb0bb7da785de08836 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 09:44:58 +0200 Subject: [PATCH 32/63] fix: build Windows JRI and resolve bundled late-load VC++ dependencies --- .github/workflows/dynamic.yaml | 53 +++++++++++++++++----------------- configure.win | 10 +++---- src/djni.c | 37 +++++++++++++++++++++++- src/jri/Makefile.win | 1 + src/jri/Makevars.win | 3 ++ src/jri/src/Makefile.win | 1 + src/jri/src/Rdecl.h | 5 ++++ 7 files changed, 77 insertions(+), 33 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index e4e2167..b61cbe3 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -137,8 +137,7 @@ jobs: run "$scenario" done run hosted explicit - # Windows source installs build no JRI (pre-existing upstream build defects). - if [[ "$RUNNER_OS" != Windows ]]; then run jengine; fi + run jengine run preloaded global run laterload b run alias link @@ -161,10 +160,11 @@ jobs: # A mapped DLL cannot be deleted, so Windows leaves them behind until here. find "$RJE_JAVA_HOME_A/" "$RJE_JAVA_HOME_B/" -name 'rjava-alias-*' -print -delete - name: Embed R in a Java host through JRI - if: runner.os != 'Windows' shell: bash timeout-minutes: 5 - run: Rscript --vanilla tests-integration/jri.R + run: | + Rscript --vanilla tests-integration/jri.R + RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R - name: Check Windows DLL search state after loads if: runner.os == 'Windows' shell: bash @@ -186,32 +186,31 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - # Runner images install the VC++ runtime in System32, which masks - # whether JVM dependencies resolve from the JDK. Hide it: jvm.dll and - # its startup libraries must still load. Later loads that import - # another runtime DLL (awt.dll -> MSVCP140.dll) are recorded only. + $ErrorActionPreference = 'Stop' $hidden = @() - foreach ($f in 'msvcp140.dll', 'vcruntime140.dll', 'vcruntime140_1.dll') { - $p = Join-Path $env:SystemRoot "System32\$f" - if (Test-Path $p) { - takeown /f $p | Out-Null - icacls $p /grant '*S-1-5-32-544:F' | Out-Null - Rename-Item $p "$f.rjava-hidden" - $hidden += $p + try { + foreach ($f in 'msvcp140.dll', 'vcruntime140.dll', 'vcruntime140_1.dll') { + $p = Join-Path $env:SystemRoot "System32\$f" + if (Test-Path $p) { + takeown /f $p | Out-Null + if ($LASTEXITCODE -ne 0) { throw "takeown failed: $p" } + icacls $p /grant '*S-1-5-32-544:F' | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed: $p" } + Rename-Item $p "$f.rjava-hidden" + $hidden += $p + } + if (Test-Path $p) { throw "Runtime still visible: $p" } } + Write-Output "Hidden: $hidden" + Rscript --vanilla tests-integration/deferred.R select + if ($LASTEXITCODE -ne 0) { throw 'JVM startup failed without system VC++ runtime' } + Rscript --vanilla tests-integration/deferred.R laterload + if ($LASTEXITCODE -ne 0) { throw 'Late native loads failed for JDK A' } + Rscript --vanilla tests-integration/deferred.R laterload b + if ($LASTEXITCODE -ne 0) { throw 'Late native loads failed for JDK B' } + } finally { + foreach ($p in $hidden) { Rename-Item "$p.rjava-hidden" (Split-Path $p -Leaf) } } - Write-Output "Hidden: $hidden" - Get-ChildItem "$env:RJE_JAVA_HOME_A" -Recurse -Include 'msvc*.dll', 'vcruntime*.dll' | ForEach-Object FullName - Rscript --vanilla tests-integration/deferred.R select - $select = $LASTEXITCODE - Rscript --vanilla tests-integration/deferred.R laterload - $later = $LASTEXITCODE - Rscript --vanilla tests-integration/deferred.R laterload b - $laterb = $LASTEXITCODE - foreach ($p in $hidden) { Rename-Item "$p.rjava-hidden" (Split-Path $p -Leaf) } - Write-Output "EVIDENCE without System32 VC++ runtime: select exit $select, laterload exit $later (A), $laterb (B)" - if ($select -ne 0) { exit 1 } - exit 0 - uses: actions/checkout@v4 if: matrix.downstream with: diff --git a/configure.win b/configure.win index f72c921..841e28a 100644 --- a/configure.win +++ b/configure.win @@ -22,7 +22,7 @@ echo " JAVA_HOME=$JAVA_HOME" echo "JAVA_HOME:=$JAVA_HOME" > src/Makevars.java -if [ -e jri/configure.win ]; then +if [ -e src/jri/configure.win ]; then echo "=== Building JRI ===" CONFIGURED=1 export CONFIGURED @@ -33,21 +33,21 @@ if [ -e jri/configure.win ]; then export R_HOME export RHOME export JAVA_HOME - cd jri + cd src/jri sh configure.win make BR=$? - cd .. + cd ../.. if [ $BR = 0 ]; then echo "=== JRI built ===" mkdir -p inst/jri # also copy into R_ARCH is present to avoid overwriting different archs if [ -n "${R_ARCH}" ]; then mkdir -p inst/jri${R_ARCH} - cp jri/src/jri.dll inst/jri${R_ARCH}/ + cp src/jri/src/jri.dll inst/jri${R_ARCH}/ fi # yet still install into JRI in case users get confused - cp -r jri/run.bat jri/src/jri.dll jri/src/JRI.jar jri/examples inst/jri/ + cp -r src/jri/run.bat src/jri/src/jri.dll src/jri/src/JRI.jar src/jri/examples inst/jri/ else echo "**** WARNING: JRI could NOT be built" >&2 if [ -z "$IGNORE" ]; then diff --git a/src/djni.c b/src/djni.c index 864cc8b..0130c8e 100644 --- a/src/djni.c +++ b/src/djni.c @@ -278,6 +278,37 @@ static int created_vms(HMODULE module) { created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"); return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } +/* Later JVM loads (AWT, Windows keystore) import MSVC DLLs by basename. + Prefer a loaded or system runtime; otherwise retain the selected JDK's + bundled copy for the process lifetime. No persistent search directory or + PATH change is needed. Missing optional copies remain the JDK's concern. */ +static void load_bundled_msvc(void) { + static const wchar_t *names[] = { L"vcruntime140.dll", L"vcruntime140_1.dll", L"msvcp140.dll" }; + wchar_t *path = module_path(jni_dl), *slash, *candidate; + size_t i, len; + if (!path) return; + slash = wcsrchr(path, L'\\'); + if (!slash) { free(path); return; } + *slash = 0; /* bin\server */ + slash = wcsrchr(path, L'\\'); + if (!slash) { free(path); return; } + *slash = 0; /* bin */ + len = wcslen(path); + candidate = malloc(sizeof(wchar_t) * (len + 32)); + if (candidate) { + for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + if (GetModuleHandleW(names[i])) continue; + if (LoadLibraryExW(names[i], NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)) continue; + wcscpy(candidate, path); + candidate[len] = L'\\'; + wcscpy(candidate + len + 1, names[i]); + if (GetFileAttributesW(candidate) != INVALID_FILE_ATTRIBUTES) + LoadLibraryExW(candidate, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + } + free(candidate); + } + free(path); +} static int load_library(const char *path) { wchar_t *wide = NULL, *full = NULL, *bin = NULL, *slash; struct jvm_image *jvms = NULL; @@ -745,7 +776,11 @@ int djni_load(const char *path) { last_error[0] = 0; if (jni_dl) return -1; if ((res = load_library(path))) return res; - return load_syms(); + res = load_syms(); +#ifdef _WIN32 + if (!res) load_bundled_msvc(); +#endif + return res; } /* returns: 0 = success, -1 = already loaded, -3 = symbols not found, -4 = no single mapped JVM library with a created VM */ diff --git a/src/jri/Makefile.win b/src/jri/Makefile.win index 69770f5..5ce6cb7 100644 --- a/src/jri/Makefile.win +++ b/src/jri/Makefile.win @@ -1,4 +1,5 @@ # Windows-specific part of the Makefile +.DEFAULT_GOAL := windows.all include src/Makefile.wconf include Makevars.win diff --git a/src/jri/Makevars.win b/src/jri/Makevars.win index 64047b5..fc2cdbe 100644 --- a/src/jri/Makevars.win +++ b/src/jri/Makevars.win @@ -6,6 +6,9 @@ JNISO=.dll JAVA_LIBS=-Lwin32 -ljvm JNILD=-shared $(JAVA_LIBS) -L$(RHOME)/src/gnuwin32 -L$(RHOME)/bin$(R_ARCH) -lR -Wl,--kill-at CFLAGS+=-DWin32 -D_JNI_IMPLEMENTATION_ +ifeq ($(R_ARCH),/x64) +CFLAGS+=-DWIN64 +endif JAVA_PROG=$(JAVAHOME)/bin/java JAVA=$(JAVAHOME)/bin/java JAVAC=$(JAVAHOME)/bin/javac diff --git a/src/jri/src/Makefile.win b/src/jri/src/Makefile.win index a443119..678e2fe 100644 --- a/src/jri/src/Makefile.win +++ b/src/jri/src/Makefile.win @@ -1,4 +1,5 @@ # Windows-specific part of the Makefile +.DEFAULT_GOAL := all include $(R_HOME)/etc$(R_ARCH)/Makeconf include Makefile.wconf diff --git a/src/jri/src/Rdecl.h b/src/jri/src/Rdecl.h index abe9913..aba9342 100644 --- a/src/jri/src/Rdecl.h +++ b/src/jri/src/Rdecl.h @@ -6,9 +6,14 @@ #include +#ifndef Win32 #define R_INTERFACE_PTRS 1 #define CSTACK_DEFNS 1 #include /* R_ReadConsole since 4.6.0 */ +#else +/* Rinterface.h is Unix-only and is not shipped by Windows R. */ +int R_ReadConsole(const char *, unsigned char *, int, int); +#endif /* some have been added to R 4.6.0 */ #if R_VERSION >= R_Version(4,6,0) From 4098626ce73d41d881aa22c436c16bdb437d6cd9 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 09:46:07 +0200 Subject: [PATCH 33/63] test: exercise overlayfs with real pre-6.8 and current guest kernels --- .github/workflows/dynamic.yaml | 10 +++-- .github/workflows/overlay.yaml | 31 ++++++++++++++ tests-integration/overlay-vm.sh | 76 +++++++++++++++++++++++++++++++++ tests-integration/overlay.R | 54 +++++++++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/overlay.yaml create mode 100644 tests-integration/overlay-vm.sh create mode 100644 tests-integration/overlay.R diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index b61cbe3..b22bdb5 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -69,9 +69,11 @@ jobs: if grep -Eq "[ _]$sym(@|\$| )" <<< "$syms"; then echo "exports $sym"; else echo "::error::$lib does not export $sym"; exit 1; fi done done - echo "RJE_JAVA_HOME_A=$JAVA_A" >> "$GITHUB_ENV" - echo "RJE_JAVA_HOME_B=$JAVA_B" >> "$GITHUB_ENV" - echo "RJE_FALLBACK_HOME=$JAVA_B" >> "$GITHUB_ENV" + { + echo "RJE_JAVA_HOME_A=$JAVA_A" + echo "RJE_JAVA_HOME_B=$JAVA_B" + echo "RJE_FALLBACK_HOME=$JAVA_B" + } >> "$GITHUB_ENV" - name: Prepare Windows discovery and path fixtures if: runner.os == 'Windows' shell: pwsh @@ -164,7 +166,7 @@ jobs: timeout-minutes: 5 run: | Rscript --vanilla tests-integration/jri.R - RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R + env RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R - name: Check Windows DLL search state after loads if: runner.os == 'Windows' shell: bash diff --git a/.github/workflows/overlay.yaml b/.github/workflows/overlay.yaml new file mode 100644 index 0000000..9d069ae --- /dev/null +++ b/.github/workflows/overlay.yaml @@ -0,0 +1,31 @@ +name: Overlayfs kernel compatibility +on: + push: + branches: [feature/dynamic-cross-platform] + workflow_dispatch: +permissions: + contents: read +jobs: + overlay: + name: Overlayfs (${{ matrix.release }}, ${{ matrix.expect }}) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - {release: jammy, expect: legacy} + - {release: noble, expect: current} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install VM tools + run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends qemu-system-x86 qemu-utils cloud-image-utils + - name: Check a real guest kernel and overlay mount + run: bash tests-integration/overlay-vm.sh '${{ matrix.release }}' '${{ matrix.expect }}' + - uses: actions/upload-artifact@v4 + if: always() + with: + name: overlay-${{ matrix.release }} + path: ${{ runner.temp }}/overlay-${{ matrix.release }}/serial.log diff --git a/tests-integration/overlay-vm.sh b/tests-integration/overlay-vm.sh new file mode 100644 index 0000000..48584a0 --- /dev/null +++ b/tests-integration/overlay-vm.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Boot a real kernel: containers would reuse the Actions runner's kernel. +set -euo pipefail +release=$1 +expect=$2 +work="$RUNNER_TEMP/overlay-$release" +mkdir -p "$work" +case "$release" in + jammy) version=22.04 ;; + noble) version=24.04 ;; + *) exit 2 ;; +esac +image="ubuntu-$version-server-cloudimg-amd64.img" +base="https://cloud-images.ubuntu.com/releases/$release/release" +curl --fail --location --retry 3 "$base/$image" -o "$work/$image" +curl --fail --location --retry 3 "$base/SHA256SUMS" -o "$work/SHA256SUMS" +(cd "$work" && awk -v image="$image" '$2 == image || $2 == "*" image' SHA256SUMS > selected.sha256 && test -s selected.sha256 && sha256sum -c selected.sha256) +qemu-img resize "$work/$image" 16G +ssh-keygen -t ed25519 -N '' -f "$work/key" +cat > "$work/user-data" < "$work/meta-data" +cloud-localds "$work/seed.img" "$work/user-data" "$work/meta-data" +sudo qemu-system-x86_64 -accel kvm -cpu host -smp 2 -m 3072 \ + -drive "file=$work/$image,if=virtio,format=qcow2" \ + -drive "file=$work/seed.img,if=virtio,format=raw" \ + -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=net0 \ + -display none -serial "file:$work/serial.log" -daemonize -pidfile "$work/qemu.pid" +trap 'sudo kill "$(cat "$work/qemu.pid")" || true' EXIT +ssh_opts=(-i "$work/key" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5) +ready=false +for attempt in {1..60}; do + if ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 true 2>/dev/null; then ready=true; break; fi + sleep 5 +done +if ! "$ready"; then cat "$work/serial.log"; exit 1; fi +# Include the checked-out REngine submodule, without Git metadata or VM files. +tar --exclude=.git -czf "$work/source.tar.gz" . +scp "${ssh_opts[@]}" -P 2222 "$work/source.tar.gz" ubuntu@127.0.0.1:source.tar.gz +ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 "sudo bash -s -- '$expect'" <<'GUEST' +set -euo pipefail +export DEBIAN_FRONTEND=noninteractive +cloud-init status --wait +apt-get update -qq +apt-get install -y --no-install-recommends r-base-dev openjdk-17-jdk-headless make g++ unzip zip +mkdir /home/ubuntu/source +cd /home/ubuntu/source +tar -xzf ../source.tar.gz +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +R CMD javareconf +sh mkdist > /home/ubuntu/build.log 2>&1 || { cat /home/ubuntu/build.log; exit 1; } +mkdir /opt/rjava-lib +R CMD INSTALL --library=/opt/rjava-lib ../rJava_*.tar.gz +export R_LIBS=/opt/rjava-lib +mkdir -p /opt/overlay/{lower,upper,work,merged} /opt/jdk-b +cp -a "$JAVA_HOME/." /opt/overlay/lower/ +cp -a "$JAVA_HOME/." /opt/jdk-b/ +mount -t overlay overlay -o lowerdir=/opt/overlay/lower,upperdir=/opt/overlay/upper,workdir=/opt/overlay/work /opt/overlay/merged +findmnt /opt/overlay/merged +uname -a +export RJE_JAVA_HOME_A=/opt/overlay/merged RJE_JAVA_HOME_B=/opt/jdk-b RJE_OVERLAY_EXPECT="$1" +mkdir /tmp/jvmhost +cp tests-integration/jvmhost.c /tmp/jvmhost/ +(cd /tmp/jvmhost && PKG_CPPFLAGS=-I/home/ubuntu/source/src R CMD SHLIB jvmhost.c) +export RJE_JVMHOST=/tmp/jvmhost/jvmhost.so +for scenario in fresh preloaded hosted replaced; do + Rscript --vanilla tests-integration/overlay.R "$scenario" +done +GUEST diff --git a/tests-integration/overlay.R b/tests-integration/overlay.R new file mode 100644 index 0000000..75244ae --- /dev/null +++ b/tests-integration/overlay.R @@ -0,0 +1,54 @@ +# Real overlayfs in a VM. Each mode runs in a fresh process. +scenario <- commandArgs(TRUE)[1] +expect <- Sys.getenv("RJE_OVERLAY_EXPECT") +stopifnot(expect %in% c("legacy", "current"), Sys.info()[["sysname"]] == "Linux") +kernel <- numeric_version(sub("-.*", "", Sys.info()[["release"]])) +stopifnot(if (expect == "legacy") kernel < "6.8" else kernel >= "6.8") +a <- Sys.getenv("RJE_JAVA_HOME_A") +b <- Sys.getenv("RJE_JAVA_HOME_B") +Sys.setenv(JAVA_HOME = a, PATH = "/usr/bin:/bin") +ns <- loadNamespace("rJava") +lib <- function(home) get(".find.jvm.library", ns)(home)[1] +state <- function() rJava::.jvmState()$state +mapped <- function() .Call(get("RgetMappedJVM", ns)) +reject <- function(path, pattern) { + error <- tryCatch(rJava::.jloadJVM(path), error = identity) + stopifnot(inherits(error, "error"), grepl(pattern, conditionMessage(error)), state() == "not-loaded") + cat("Expected refusal:", conditionMessage(error), "\n") +} +start <- function() { + rJava::.jinit() + actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") + stopifnot(identical(normalizePath(actual), normalizePath(a))) +} +stopifnot(is.null(mapped()), state() == "not-loaded") +if (scenario == "fresh") { + start() # New selection works even on an older overlayfs kernel. +} else { + if (scenario == "hosted") { + dyn.load(Sys.getenv("RJE_JVMHOST")) + boot <- file.path(system.file("java", package = "rJava"), "boot") + stopifnot(.C("rjava_test_create_jvm", lib(a), paste0("-Djava.class.path=", boot), result = 0L)$result == 0L) + } else dyn.load(lib(a), local = TRUE) + stopifnot(!is.null(mapped()), mapped()$vms == if (scenario == "hosted") 1L else 0L) + # Record the actual mapping and overlay inode in CI, not just kernel versions. + cat(readLines("/proc/self/maps")[grepl("libjvm", readLines("/proc/self/maps"))], sep = "\n") + system2("stat", c("-Lc", shQuote("overlay device=%d inode=%i"), shQuote(lib(a)))) + reject(lib(b), "already mapped") + if (scenario == "replaced") { + # Replacing through the overlay triggers copy-up/whiteout behavior. + path <- lib(a) + stopifnot(unlink(path) == 0L, file.copy(lib(b), path)) + reject(path, "already mapped") + } else { + if (expect == "legacy") reject(lib(a), "overlayfs before Linux 6.8") else { + stopifnot(isTRUE(rJava::.jloadJVM(lib(a)))) + } + if (scenario == "hosted") { + Sys.setenv(JAVA_HOME = b) + start() # Attach has no pathname comparison, including on old overlayfs. + stopifnot(state() == "attached") + } else if (expect == "current") start() + } +} +cat("PASS overlay", expect, as.character(kernel), scenario, "\n") From 5c906d89e0543f12f4369f45d6f09cca9a505cb6 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 09:48:23 +0200 Subject: [PATCH 34/63] test: verify bundled runtime origins and supply guest build dependencies --- .github/workflows/dynamic.yaml | 4 ++-- tests-integration/msvc.R | 13 +++++++++++++ tests-integration/overlay-vm.sh | 5 +++-- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 tests-integration/msvc.R diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index b22bdb5..b98d692 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -206,9 +206,9 @@ jobs: Write-Output "Hidden: $hidden" Rscript --vanilla tests-integration/deferred.R select if ($LASTEXITCODE -ne 0) { throw 'JVM startup failed without system VC++ runtime' } - Rscript --vanilla tests-integration/deferred.R laterload + Rscript --vanilla tests-integration/msvc.R laterload if ($LASTEXITCODE -ne 0) { throw 'Late native loads failed for JDK A' } - Rscript --vanilla tests-integration/deferred.R laterload b + Rscript --vanilla tests-integration/msvc.R laterload b if ($LASTEXITCODE -ne 0) { throw 'Late native loads failed for JDK B' } } finally { foreach ($p in $hidden) { Rename-Item "$p.rjava-hidden" (Split-Path $p -Leaf) } diff --git a/tests-integration/msvc.R b/tests-integration/msvc.R new file mode 100644 index 0000000..44941b5 --- /dev/null +++ b/tests-integration/msvc.R @@ -0,0 +1,13 @@ +# Runs laterload (A or B) with System32 VC++ DLLs hidden by the workflow, +# then proves Windows actually loaded the selected JDK's bundled runtimes. +source("tests-integration/deferred.R") +stopifnot(windows, scenario == "laterload") +ps <- file.path(sysroot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") +modules <- system2(ps, c("-NoProfile", "-Command", + shQuote(sprintf("(Get-Process -Id %d).Modules | ForEach-Object FileName", Sys.getpid()))), stdout = TRUE) +stopifnot(is.null(attr(modules, "status"))) +runtimes <- modules[grepl("[/\\\\](msvcp140|vcruntime140|vcruntime140_1)[.]dll$", modules, ignore.case = TRUE)] +stopifnot(any(tolower(basename(chartr("\\", "/", runtimes))) == "msvcp140.dll")) +selected <- normalizePath(if (variant == "b") home_b else home_a, winslash = "/") +stopifnot(all(startsWith(tolower(normalizePath(runtimes, winslash = "/")), paste0(tolower(selected), "/")))) +cat("PASS bundled MSVC dependencies:", runtimes, sep = "\n ") diff --git a/tests-integration/overlay-vm.sh b/tests-integration/overlay-vm.sh index 48584a0..a414db8 100644 --- a/tests-integration/overlay-vm.sh +++ b/tests-integration/overlay-vm.sh @@ -33,10 +33,11 @@ sudo qemu-system-x86_64 -accel kvm -cpu host -smp 2 -m 3072 \ -drive "file=$work/seed.img,if=virtio,format=raw" \ -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=net0 \ -display none -serial "file:$work/serial.log" -daemonize -pidfile "$work/qemu.pid" +sudo chmod a+r "$work/qemu.pid" "$work/serial.log" trap 'sudo kill "$(cat "$work/qemu.pid")" || true' EXIT ssh_opts=(-i "$work/key" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5) ready=false -for attempt in {1..60}; do +for _attempt in {1..60}; do if ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 true 2>/dev/null; then ready=true; break; fi sleep 5 done @@ -49,7 +50,7 @@ set -euo pipefail export DEBIAN_FRONTEND=noninteractive cloud-init status --wait apt-get update -qq -apt-get install -y --no-install-recommends r-base-dev openjdk-17-jdk-headless make g++ unzip zip +apt-get install -y --no-install-recommends r-base-dev openjdk-17-jdk-headless libtirpc-dev libicu-dev make g++ unzip zip mkdir /home/ubuntu/source cd /home/ubuntu/source tar -xzf ../source.tar.gz From 8fdbd8f3612d29933fd15ce642ceb6ddaca5baf3 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 09:50:40 +0200 Subject: [PATCH 35/63] test: distinguish distro overlay behavior from a frozen pre-6.8 kernel --- .github/workflows/overlay.yaml | 3 ++- tests-integration/overlay-vm.sh | 38 +++++++++++++++++++++++++++------ tests-integration/overlay.R | 6 +++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/.github/workflows/overlay.yaml b/.github/workflows/overlay.yaml index 9d069ae..042e986 100644 --- a/.github/workflows/overlay.yaml +++ b/.github/workflows/overlay.yaml @@ -15,6 +15,7 @@ jobs: matrix: include: - {release: jammy, expect: legacy} + - {release: jammy, expect: distro} - {release: noble, expect: current} steps: - uses: actions/checkout@v4 @@ -27,5 +28,5 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: overlay-${{ matrix.release }} + name: overlay-${{ matrix.release }}-${{ matrix.expect }} path: ${{ runner.temp }}/overlay-${{ matrix.release }}/serial.log diff --git a/tests-integration/overlay-vm.sh b/tests-integration/overlay-vm.sh index a414db8..c12e236 100644 --- a/tests-integration/overlay-vm.sh +++ b/tests-integration/overlay-vm.sh @@ -36,12 +36,38 @@ sudo qemu-system-x86_64 -accel kvm -cpu host -smp 2 -m 3072 \ sudo chmod a+r "$work/qemu.pid" "$work/serial.log" trap 'sudo kill "$(cat "$work/qemu.pid")" || true' EXIT ssh_opts=(-i "$work/key" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5) -ready=false -for _attempt in {1..60}; do - if ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 true 2>/dev/null; then ready=true; break; fi - sleep 5 -done -if ! "$ready"; then cat "$work/serial.log"; exit 1; fi +wait_for_ssh() { + for _attempt in {1..60}; do + if ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 true 2>/dev/null; then return 0; fi + sleep 5 + done + cat "$work/serial.log" + return 1 +} +wait_for_ssh +if [[ "$expect" == legacy ]]; then + # Current distro 5.15 kernels can carry overlayfs fixes. Also boot a frozen + # pre-6.8 mainline build, so the ambiguous-identity case is really exercised. + mainline=https://kernel.ubuntu.com/mainline/v6.1/amd64 + kernel_version=6.1.0-060100 + kernel_build=6.1.0-060100.202303090726 + packages=("linux-image-unsigned-${kernel_version}-generic_${kernel_build}_amd64.deb" + "linux-modules-${kernel_version}-generic_${kernel_build}_amd64.deb") + for package in "${packages[@]}"; do + curl --fail --location --retry 3 "$mainline/$package" -o "$work/$package" + done + # SHA256 values from the release's CHECKSUMS, pinned with the package names. + cat > "$work/kernel.sha256" <= "6.8") +stopifnot(if (expect == "current") kernel >= "6.8" else kernel < "6.8") a <- Sys.getenv("RJE_JAVA_HOME_A") b <- Sys.getenv("RJE_JAVA_HOME_B") Sys.setenv(JAVA_HOME = a, PATH = "/usr/bin:/bin") @@ -48,7 +48,7 @@ if (scenario == "fresh") { Sys.setenv(JAVA_HOME = b) start() # Attach has no pathname comparison, including on old overlayfs. stopifnot(state() == "attached") - } else if (expect == "current") start() + } else if (expect != "legacy") start() } } cat("PASS overlay", expect, as.character(kernel), scenario, "\n") From 2a9b38f01416e2e2152c461ce22dc21bdb3a00d1 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 09:53:00 +0200 Subject: [PATCH 36/63] test: schedule guest reboot after the SSH command exits [skip ci] --- tests-integration/overlay-vm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-integration/overlay-vm.sh b/tests-integration/overlay-vm.sh index c12e236..5c8c7d3 100644 --- a/tests-integration/overlay-vm.sh +++ b/tests-integration/overlay-vm.sh @@ -63,7 +63,7 @@ ef4cf195e9e6840318e7fe9227d4ddccaf44ce44f0324d5dbd418ecaa72dc2d4 ${packages[1]} SUMS (cd "$work" && sha256sum -c kernel.sha256) scp "${ssh_opts[@]}" -P 2222 "${packages[@]/#/$work/}" ubuntu@127.0.0.1: - ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 'cloud-init status --wait; sudo dpkg -i linux-modules-*.deb linux-image-*.deb && sudo reboot' + ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 'cloud-init status --wait && sudo dpkg -i linux-modules-*.deb linux-image-*.deb && sudo systemd-run --on-active=3s /sbin/reboot' sleep 15 wait_for_ssh ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 'test "$(uname -r)" = 6.1.0-060100-generic' From fee5d72542eeed640325a35a8fa93dfe0fb25d77 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 09:53:49 +0200 Subject: [PATCH 37/63] fix: describe overlay identity failures without assuming a kernel version --- src/djni.c | 6 +-- tests-integration/README.md | 99 ++++++++++++++++++++++--------------- tests-integration/overlay.R | 2 +- 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/djni.c b/src/djni.c index 0130c8e..b6690ff 100644 --- a/src/djni.c +++ b/src/djni.c @@ -485,8 +485,8 @@ static int same_as_mapped(const void *addr, const char *path) { free(line); fclose(f); } - /* Before Linux 6.8, /proc/self/maps shows the device and inode of the - layer file under an overlayfs mapping, but stat() the overlay device. */ + /* Some overlayfs kernels report a layer file in /proc/self/maps, + while stat() reports the overlay identity. A mismatch is ambiguous. */ if (!res && !statfs(path, &fs) && fs.f_type == 0x794c7630) res = -2; } #endif @@ -673,7 +673,7 @@ static int load_library(const char *path) { if (!same) snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", jvms[0].name); else if (same == -2) - snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s, because overlayfs before Linux 6.8 reports another file for the mapping. Use a new R process.", + snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s, because the overlayfs mapping identity cannot be verified. Use a new R process.", path, jvms[0].name); else if (same < 0) snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s. Use a new R process.", path, jvms[0].name); diff --git a/tests-integration/README.md b/tests-integration/README.md index 862dd19..7b09cbc 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -107,32 +107,53 @@ that `LoadLibraryExW` call, then removes it after success or failure. `PATH` is never changed. `dll-search.c` checks the removal with a positive control. Later native loads by the JVM (`zip`, `net`, `nio`, `management`, -`sunmscapi`, `awt`, `fontmanager`, `freetype`) work with `PATH` reduced to -system and R directories, for both JDKs of every Windows job. JDK libraries -that import each other resolve as already-loaded modules. The logged module -lists show the one external later dependency: `awt.dll` (and on JDK 8 also -`sunmscapi.dll`) imports the VC++ runtime (`MSVCP140.dll`, and -`VCRUNTIME140_1.dll` on JDK 8), which Windows then looks for in System32 and -on `PATH`, not in the JDK. Upstream rJava has the same exposure, because it -restores `PATH` right after linking `jvm.dll`. - -The Windows job therefore hides System32's VC++ runtime DLLs, as on a -machine without the VC++ redistributable (for example with a JDK unpacked -from a zip archive and not on `PATH`). JVM startup must still succeed, and -does for all six JDKs: the scoped `bin` search covers `jvm.dll` and its -startup libraries. The later loads then fail, as recorded in the log, with -"Can't find dependent libraries": `sunmscapi.dll` on Temurin 8, and -`awt.dll` on Temurin 11, 17 and 21, Corretto 11 and Zulu 25. This branch -does not change that. - -Options for maintainers, **not implemented**: (1) append the JDK `bin` -directory to `PATH` after selection, as an installer that adds Java to -`PATH` effectively does; this persists for the process and its children. -(2) After loading `jvm.dll`, load the VC++ runtime DLLs bundled in the JDK -`bin` only if System32 has none; the process then binds every later import -of those names to the JDK's copies, possibly older than a copy another -component bundles. Both are permanent process-wide changes that upstream -never made. +`sunmscapi`, `awt`, `fontmanager`, `freetype`) also need the JDK's MSVC runtime +DLLs to be resolvable by basename. After verifying the JNI entry points, +djni retains `vcruntime140.dll`, `vcruntime140_1.dll` and `msvcp140.dll` when +available: an already loaded runtime is used first, then System32, then the +copy bundled in the selected JDK's `bin`. No persistent search directory or +`PATH` change is made. These references last until process exit, like the +selected JVM itself. This can bind later imports in other native components +to that runtime too; an already loaded or system copy is never replaced. +JDKs that omit a required runtime still need the appropriate redistributable. + +The Windows jobs hide all three System32 runtime DLLs, run JVM startup and +all later native loads for both JDKs, and fail on any error. `msvc.R` also +checks the loaded module paths: the runtimes must come from the selected +JDK, so another installed copy cannot silently mask a missing dependency. +The workflow restores the hidden DLLs in a `finally` block, including on a +failed test. + +## Windows JRI builds + +`configure.win` now builds JRI at its current location, `src/jri`, and +installs `JRI.jar` and `jri.dll` alongside rJava. The Windows Makefiles set +explicit default targets so R's included Makeconf cannot select an unrelated +rule. Windows uses its own console declaration instead of the Unix-only +`Rinterface.h`; the x64 build also enables JRI's existing Win64 branches. + +Every Windows runtime pair now runs `.jengine()` from R and a Java host +embedding R through JRI. The latter runs with both JDKs as host, with +`JAVA_HOME` deliberately naming the other runtime. The test requires rJava +to reject that other runtime and attach to the host's already running JVM. + +## Overlayfs kernel checks + +`overlay.yaml` boots real QEMU/KVM guests; a container alone would reuse the +runner's kernel. Each guest records its actual kernel, mount, mapping device +and inode, and the inode returned by `stat()`. Tests cover a fresh JVM, +a preloaded JVM library, a host-created JVM and replacement of a mapped +library through overlayfs. Each case runs in a fresh R process. + +The matrix includes Ubuntu 22.04's current 5.15 kernel, Ubuntu 24.04's +current kernel (at least 6.8), and the frozen Ubuntu mainline 6.1.0 build +`6.1.0-060100.202303090726` (packages pinned by SHA256). A current distro +kernel can carry fixes, so a release number below 6.8 alone does not predict +its behavior. The distro 5.15 and current-kernel jobs require same-file +reuse to succeed. The frozen mainline job requires the ambiguous overlay +identity to be rejected with the explicit diagnostic. Fresh selection and +attachment to a host-created VM must work on all three. Selecting a +different or replaced library must always fail. ## Tests @@ -159,8 +180,10 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `laterload` | the later native loads above | | `hosted`, `hosted explicit` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK; `.jloadJVM()` rejects B, and with `explicit` selects A's own file first | | every scenario | before anything is mapped, `RgetMappedJVM` is `NULL`: rJava's own JNI wrappers are not taken for a JVM | -| `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where `.jloadJVM()` rejects the `JAVA_HOME` runtime and rJava attaches to the host JVM (Linux and macOS) | +| `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where `.jloadJVM()` rejects the `JAVA_HOME` runtime and rJava attaches to the host JVM (all three platforms, both host JDKs) | | `dll-search.c` | Windows DLL search state after failed and successful loads | +| `msvc.R` | Windows later native loads without System32 VC++ DLLs; loaded runtime origins | +| `overlay.R` | actual distro 5.15, frozen mainline 6.1.0 and current-kernel overlayfs behavior | | downstream | rJavaEnv and two minimal consumer packages (current pair only) | Bounded matrix, each on Linux, macOS (arm64) and Windows with R release; the @@ -189,25 +212,19 @@ from the repository root, one process per scenario. - Attaching to a JVM created by a host requires rJava's `java/boot` on the host's class path on JDK 9+, as before: rJava cannot add it to a running application class loader there. -- Windows source installs build no JRI, as on upstream master: - `configure.win` still looks for `jri/` although JRI moved to `src/jri` in - 1.0-15, and `src/jri/Rdecl.h` includes ``, which R does not - ship on Windows. JRI embedding is therefore tested on Linux and macOS only; - attaching to a created JVM is tested on Windows with the native helper. -- Windows AWT later loads depend on a VC++ runtime outside the JDK, as above. - Mapped JVM discovery covers Linux, macOS and Windows only; elsewhere djni lists no mapped libraries, so it cannot reject a conflict. It cannot see libraries in another `dlmopen()` namespace, and identifies a renamed JVM only by the exports above (a JVM without `JVM_CurrentTimeMillis` must keep its usual name). If the mapped file cannot be identified (no `/proc` on - Linux, a failed query), selection is rejected. On overlayfs (e.g. Docker) - with Linux before 6.8, `/proc/self/maps` reports the layer file's device, - while `stat()` reports the overlay's, so `.jloadJVM(path)` rejects a - mapped library as unverifiable even when it is the same file, with or - without a created VM; `.jinit()` attaching to a created VM is unaffected, - as it compares no files. This - follows from the kernel source (`show_map_vma` uses `file_user_inode` - only from 6.8) and is not tested. dyld + Linux, a failed query), selection is rejected. On overlayfs (e.g. Docker), + kernels without the mapping-identity fix can report the layer file's + device/inode in `/proc/self/maps` while `stat()` reports the overlay's. + `.jloadJVM(path)` then rejects a mapped library as unverifiable, even when + it is the same file, with or without a created VM. A new R process can + select it normally; `.jinit()` attaching to a created VM compares no files. + This is exercised on the frozen mainline 6.1.0 guest, alongside successful + reuse on the current Ubuntu 5.15 and current-kernel guests. dyld records a library loaded through a symbolic link by its target, so on macOS the `symlink` variant shows the target name. - Not covered: 32-bit Windows, Windows on ARM, Linux on ARM, musl, OpenJ9 and diff --git a/tests-integration/overlay.R b/tests-integration/overlay.R index 7696f48..9c5c163 100644 --- a/tests-integration/overlay.R +++ b/tests-integration/overlay.R @@ -41,7 +41,7 @@ if (scenario == "fresh") { stopifnot(unlink(path) == 0L, file.copy(lib(b), path)) reject(path, "already mapped") } else { - if (expect == "legacy") reject(lib(a), "overlayfs before Linux 6.8") else { + if (expect == "legacy") reject(lib(a), "overlayfs mapping identity cannot be verified") else { stopifnot(isTRUE(rJava::.jloadJVM(lib(a)))) } if (scenario == "hosted") { From 3f17fb24b38e4897a2ae051f0337453181d96dd4 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 10:01:23 +0200 Subject: [PATCH 38/63] fix: preserve pre-4.6 Windows JRI declarations and test R 4.5 --- .github/workflows/dynamic.yaml | 3 ++- src/jri/src/Rdecl.h | 5 +++-- tests-integration/README.md | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index b98d692..23b63af 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -20,6 +20,7 @@ jobs: - { set: legacy, os: macos-latest, a-dist: zulu } - { set: current, a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21', downstream: true } - { set: vendor, a-dist: corretto, a-version: '11', b-dist: zulu, b-version: '25' } + - { set: r45, os: windows-latest, r: '4.5', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -30,7 +31,7 @@ jobs: submodules: recursive - uses: r-lib/actions/setup-r@v2 with: - r-version: release + r-version: ${{ matrix.r || 'release' }} rtools-version: '45' - uses: actions/setup-java@v5 id: java-a diff --git a/src/jri/src/Rdecl.h b/src/jri/src/Rdecl.h index aba9342..55d554b 100644 --- a/src/jri/src/Rdecl.h +++ b/src/jri/src/Rdecl.h @@ -10,8 +10,9 @@ #define R_INTERFACE_PTRS 1 #define CSTACK_DEFNS 1 #include /* R_ReadConsole since 4.6.0 */ -#else -/* Rinterface.h is Unix-only and is not shipped by Windows R. */ +#elif R_VERSION >= R_Version(4,6,0) +/* Rinterface.h is Unix-only and is not shipped by Windows R. + Older R uses the declaration in the compatibility block below. */ int R_ReadConsole(const char *, unsigned char *, int, int); #endif diff --git a/tests-integration/README.md b/tests-integration/README.md index 7b09cbc..66c0c02 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -195,6 +195,9 @@ build JDK is Temurin 17: | current | Temurin 17 | Temurin 21 | | vendor | Corretto 11 | Zulu 25 | +An additional Windows R 4.5 job runs the same native, JRI and VC++ checks +with Temurin 17/21, including the console declarations for R before 4.6. + The upstream `check.yaml` matrix (R devel, Zulu 8 and 11, five runner images including Intel macOS) also runs on this branch. @@ -229,5 +232,5 @@ from the repository root, one process per scenario. macOS the `symlink` variant shows the target name. - Not covered: 32-bit Windows, Windows on ARM, Linux on ARM, musl, OpenJ9 and other non-HotSpot VMs, Java runtimes without a JDK layout, R versions other - than current release (and devel in `check.yaml`), and downstream packages + than current release, Windows 4.5 and devel in `check.yaml`, and downstream packages beyond the two test consumers. From 0fe44df41c987e846c188f3ff924164c8a66b56a Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 11:23:27 +0200 Subject: [PATCH 39/63] fix: resolve review findings and exercise runtime and overlay boundaries --- .github/workflows/dynamic.yaml | 41 ++++------- configure.win | 5 +- src/djni.c | 65 ++++++++++++----- src/djni.h | 3 +- src/jri/Makevars.win | 3 - src/jri/src/Makefile.all | 3 + src/jri/src/Rdecl.h | 8 +++ src/jri/src/Rengine.c | 9 +-- src/jri/src/Rinit.c | 9 +-- tests-integration/JriAttach.java | 22 ++++++ tests-integration/README.md | 101 +++++++++++++++++++-------- tests-integration/deferred.R | 20 +++++- tests-integration/jri-optional.sh | 25 +++++++ tests-integration/jvmhost.c | 7 ++ tests-integration/msvc.R | 18 +++-- tests-integration/overlay-layouts.sh | 61 ++++++++++++++++ tests-integration/overlay-vm.sh | 35 ++++------ tests-integration/overlay.R | 19 +++-- tests-integration/windows-msvc.ps1 | 91 ++++++++++++++++++++++++ 19 files changed, 415 insertions(+), 130 deletions(-) create mode 100644 tests-integration/jri-optional.sh create mode 100644 tests-integration/overlay-layouts.sh create mode 100644 tests-integration/windows-msvc.ps1 diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 23b63af..f2b7577 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -185,35 +185,22 @@ jobs: w() { cygpath -w "$1"; } PATH="/c/Windows/System32:/c/Windows" "$work/dll-search.exe" \ "$(w "$work/missing/bin/server/jvm.dll")" "$(w "$work/copy/bin/server/jvm.dll")" "$(w "$work/missing/bin")" - - name: Start Java without the system VC++ runtime (Windows) + - name: Verify bundled runtimes with installed system DLLs if: runner.os == 'Windows' shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $hidden = @() - try { - foreach ($f in 'msvcp140.dll', 'vcruntime140.dll', 'vcruntime140_1.dll') { - $p = Join-Path $env:SystemRoot "System32\$f" - if (Test-Path $p) { - takeown /f $p | Out-Null - if ($LASTEXITCODE -ne 0) { throw "takeown failed: $p" } - icacls $p /grant '*S-1-5-32-544:F' | Out-Null - if ($LASTEXITCODE -ne 0) { throw "icacls failed: $p" } - Rename-Item $p "$f.rjava-hidden" - $hidden += $p - } - if (Test-Path $p) { throw "Runtime still visible: $p" } - } - Write-Output "Hidden: $hidden" - Rscript --vanilla tests-integration/deferred.R select - if ($LASTEXITCODE -ne 0) { throw 'JVM startup failed without system VC++ runtime' } - Rscript --vanilla tests-integration/msvc.R laterload - if ($LASTEXITCODE -ne 0) { throw 'Late native loads failed for JDK A' } - Rscript --vanilla tests-integration/msvc.R laterload b - if ($LASTEXITCODE -ne 0) { throw 'Late native loads failed for JDK B' } - } finally { - foreach ($p in $hidden) { Rename-Item "$p.rjava-hidden" (Split-Path $p -Leaf) } - } + run: ./tests-integration/windows-msvc.ps1 -Mode normal + - name: Verify startup and host attachment without system VC++ DLLs + if: runner.os == 'Windows' + shell: pwsh + run: ./tests-integration/windows-msvc.ps1 -Mode missing + - name: Verify startup and host attachment with VC++ 2015 in System32 + if: runner.os == 'Windows' + shell: pwsh + run: ./tests-integration/windows-msvc.ps1 -Mode old + - name: Check Windows JRI failure and opt-out behavior + if: runner.os == 'Windows' && matrix.set == 'current' + shell: bash + run: bash tests-integration/jri-optional.sh - uses: actions/checkout@v4 if: matrix.downstream with: diff --git a/configure.win b/configure.win index 841e28a..b2c71a6 100644 --- a/configure.win +++ b/configure.win @@ -22,7 +22,7 @@ echo " JAVA_HOME=$JAVA_HOME" echo "JAVA_HOME:=$JAVA_HOME" > src/Makevars.java -if [ -e src/jri/configure.win ]; then +if [ -e src/jri/configure.win ] && [ "${JRI:-1}" != 0 ]; then echo "=== Building JRI ===" CONFIGURED=1 export CONFIGURED @@ -34,8 +34,7 @@ if [ -e src/jri/configure.win ]; then export RHOME export JAVA_HOME cd src/jri - sh configure.win - make + sh configure.win && make BR=$? cd ../.. if [ $BR = 0 ]; then diff --git a/src/djni.c b/src/djni.c index b6690ff..79fd833 100644 --- a/src/djni.c +++ b/src/djni.c @@ -278,15 +278,22 @@ static int created_vms(HMODULE module) { created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"); return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } -/* Later JVM loads (AWT, Windows keystore) import MSVC DLLs by basename. - Prefer a loaded or system runtime; otherwise retain the selected JDK's - bundled copy for the process lifetime. No persistent search directory or - PATH change is needed. Missing optional copies remain the JDK's concern. */ +/* Keep already loaded runtimes alive; otherwise prefer the selected JDK's + own set, as java.exe does. Only a missing bundled copy falls back to + System32. References last until process exit, without changing PATH or + retaining a DLL search directory. Optional load failures are diagnosed. */ static void load_bundled_msvc(void) { - static const wchar_t *names[] = { L"vcruntime140.dll", L"vcruntime140_1.dll", L"msvcp140.dll" }; + static const wchar_t *names[] = { + L"vcruntime140.dll", L"vcruntime140_1.dll", L"msvcp140.dll", + L"msvcp140_1.dll", L"msvcp140_2.dll", L"msvcp140_atomic_wait.dll" + }; + static HMODULE retained[sizeof(names) / sizeof(names[0])]; wchar_t *path = module_path(jni_dl), *slash, *candidate; size_t i, len; - if (!path) return; + if (!path) { + fprintf(stderr, "rJava: cannot determine the selected JVM's MSVC runtime directory (Windows error %lu).\n", (unsigned long) GetLastError()); + return; + } slash = wcsrchr(path, L'\\'); if (!slash) { free(path); return; } *slash = 0; /* bin\server */ @@ -294,19 +301,35 @@ static void load_bundled_msvc(void) { if (!slash) { free(path); return; } *slash = 0; /* bin */ len = wcslen(path); - candidate = malloc(sizeof(wchar_t) * (len + 32)); + candidate = malloc(sizeof(wchar_t) * (len + 64)); if (candidate) { for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { - if (GetModuleHandleW(names[i])) continue; - if (LoadLibraryExW(names[i], NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)) continue; + DWORD attrs, error; + char *name; + if (retained[i]) continue; + /* Unlike GetModuleHandleW, this owns a reference even if the + component that originally loaded the runtime is unloaded. */ + if (GetModuleHandleExW(0, names[i], &retained[i])) continue; wcscpy(candidate, path); candidate[len] = L'\\'; wcscpy(candidate + len + 1, names[i]); - if (GetFileAttributesW(candidate) != INVALID_FILE_ATTRIBUTES) - LoadLibraryExW(candidate, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + attrs = GetFileAttributesW(candidate); + if (attrs != INVALID_FILE_ATTRIBUTES) + retained[i] = LoadLibraryExW(candidate, NULL, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + else + retained[i] = LoadLibraryExW(names[i], NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (retained[i]) continue; + error = GetLastError(); + /* A JDK need not use every optional runtime in the list. */ + if (attrs == INVALID_FILE_ATTRIBUTES && error == ERROR_MOD_NOT_FOUND) continue; + name = to_utf8(attrs != INVALID_FILE_ATTRIBUTES ? candidate : names[i]); + fprintf(stderr, "rJava: cannot retain MSVC runtime %s (Windows error %lu); later Java native loads may fail.\n", + name ? name : "(unknown path)", (unsigned long) error); + free(name); } free(candidate); - } + } else fprintf(stderr, "rJava: cannot prepare MSVC runtimes: out of memory.\n"); free(path); } static int load_library(const char *path) { @@ -471,15 +494,17 @@ static int same_as_mapped(const void *addr, const char *path) { char *line = NULL; size_t size = 0; unsigned long long a = (uintptr_t) addr, start, end, ino; - unsigned int maj, min; + unsigned int maj = 0, min = 0; + int device_mismatch = 0; struct statfs fs; if (f) { while (getline(&line, &size, f) > 0) if (sscanf(line, "%llx-%llx %*s %*s %x:%x %llu", &start, &end, &maj, &min, &ino) == 5 && a >= start && a < end) { - if (ino) - res = major(s.st_dev) == maj && minor(s.st_dev) == min && - (unsigned long long) s.st_ino == ino; + if (ino) { + device_mismatch = major(s.st_dev) != maj || minor(s.st_dev) != min; + res = !device_mismatch && (unsigned long long) s.st_ino == ino; + } break; } free(line); @@ -487,7 +512,7 @@ static int same_as_mapped(const void *addr, const char *path) { } /* Some overlayfs kernels report a layer file in /proc/self/maps, while stat() reports the overlay identity. A mismatch is ambiguous. */ - if (!res && !statfs(path, &fs) && fs.f_type == 0x794c7630) res = -2; + if (!res && device_mismatch && !statfs(path, &fs) && fs.f_type == 0x794c7630) res = -2; } #endif return res; @@ -789,7 +814,11 @@ int djni_attach(void) { last_error[0] = 0; if (jni_dl) return -1; if ((res = attach_library())) return res; - return load_syms(); + res = load_syms(); +#ifdef _WIN32 + if (!res) load_bundled_msvc(); +#endif + return res; } int djni_unload(void) { if (!jni_dl) return -1; diff --git a/src/djni.h b/src/djni.h index 9a7d8f2..ab4e48c 100644 --- a/src/djni.h +++ b/src/djni.h @@ -16,7 +16,8 @@ the mapped file, whether or not it has created a VM. */ int djni_load(const char *path); /* Selects the only JVM library mapped into the process if it has created a - VM, by its mapped image: no path is interpreted and nothing is loaded. */ + VM, by its mapped image: no path is interpreted and no JVM is loaded. + Windows also retains the selected JDK's MSVC dependency DLLs. */ int djni_attach(void); int djni_unload(void); const char* djni_last_error(void); diff --git a/src/jri/Makevars.win b/src/jri/Makevars.win index fc2cdbe..64047b5 100644 --- a/src/jri/Makevars.win +++ b/src/jri/Makevars.win @@ -6,9 +6,6 @@ JNISO=.dll JAVA_LIBS=-Lwin32 -ljvm JNILD=-shared $(JAVA_LIBS) -L$(RHOME)/src/gnuwin32 -L$(RHOME)/bin$(R_ARCH) -lR -Wl,--kill-at CFLAGS+=-DWin32 -D_JNI_IMPLEMENTATION_ -ifeq ($(R_ARCH),/x64) -CFLAGS+=-DWIN64 -endif JAVA_PROG=$(JAVAHOME)/bin/java JAVA=$(JAVAHOME)/bin/java JAVAC=$(JAVAHOME)/bin/javac diff --git a/src/jri/src/Makefile.all b/src/jri/src/Makefile.all index 3146350..e5c3d45 100644 --- a/src/jri/src/Makefile.all +++ b/src/jri/src/Makefile.all @@ -10,10 +10,12 @@ all: $(TARGETS) JRI.jar: $(JRI_JSRC) $(JNIPREFIX)jri$(JNISO) $(JAVAC) $(JFLAGS) $(JRI_JFLAGS) -d . $(JRI_JSRC) + if [ -f org_rosuda_JRI_Rengine.h ]; then tr -d '\r' < org_rosuda_JRI_Rengine.h > jri-header.tmp && mv jri-header.tmp org_rosuda_JRI_Rengine.h; fi $(JAR) fc $@ org $(JNIPREFIX)jri$(JNISO) org_rosuda_JRI_Rengine.h: org/rosuda/JRI/Rengine.class if [ -n "$(JAVAH)" ]; then $(JAVAH) -d . -classpath . org.rosuda.JRI.Rengine; fi + if [ -f org_rosuda_JRI_Rengine.h ]; then tr -d '\r' < org_rosuda_JRI_Rengine.h > jri-header.tmp && mv jri-header.tmp org_rosuda_JRI_Rengine.h; fi Rcallbacks.o: Rcallbacks.c Rcallbacks.h globals.h rjstring.h org_rosuda_JRI_Rengine.h $(CC) -c -o $@ $< $(CFLAGS) $(CPICF) $(JAVAINC) $(RINC) $(JRI_CPPFLAGS) @@ -44,6 +46,7 @@ win32/libjvm.dll.a: org/rosuda/JRI/Rengine.class org/rosuda/JRI/REXP.class org/rosuda/JRI/Mutex.class: $(JRI_JSRC) $(JAVAC) $(JFLAGS) $(JRI_JFLAGS) -d . $^ + if [ -f org_rosuda_JRI_Rengine.h ]; then tr -d '\r' < org_rosuda_JRI_Rengine.h > jri-header.tmp && mv jri-header.tmp org_rosuda_JRI_Rengine.h; fi clean: rm -rf $(TARGETS) org *.o *~ org_rosuda_JRI_Rengine.h *$(JNISO) *.class *~ diff --git a/src/jri/src/Rdecl.h b/src/jri/src/Rdecl.h index 55d554b..f88b59f 100644 --- a/src/jri/src/Rdecl.h +++ b/src/jri/src/Rdecl.h @@ -6,6 +6,10 @@ #include +#if defined(_WIN64) && !defined(WIN64) +#define WIN64 1 +#endif + #ifndef Win32 #define R_INTERFACE_PTRS 1 #define CSTACK_DEFNS 1 @@ -25,4 +29,8 @@ int R_ReadConsole(char*, unsigned char*, int, int); /* include/Defn.h */ int Rf_initialize_R(int ac, char **av); /* include/Rembedded.h - exists since 2.4.0 */ #endif /* R < 4.6.0 */ +#if defined(Win32) && R_VERSION < R_Version(4,6,0) +extern __declspec(dllimport) int UserBreak; +#endif + #endif /* __RDECL_H__ */ diff --git a/src/jri/src/Rengine.c b/src/jri/src/Rengine.c index 340bc2b..6beb967 100644 --- a/src/jri/src/Rengine.c +++ b/src/jri/src/Rengine.c @@ -49,14 +49,7 @@ LibExtern int R_interrupts_pending; #ifdef Win32 #include -#ifdef _MSC_VER -__declspec(dllimport) int UserBreak; -#else -#ifndef WIN64 -#define UserBreak (*_imp__UserBreak) -#endif -extern int UserBreak; -#endif +/* UserBreak is declared with DLL-import linkage in Rdecl.h / Rembedded.h. */ #else /* for R_runHandlers */ #include diff --git a/src/jri/src/Rinit.c b/src/jri/src/Rinit.c index 05d815d..95d5dc8 100644 --- a/src/jri/src/Rinit.c +++ b/src/jri/src/Rinit.c @@ -124,14 +124,7 @@ extern __declspec(dllimport) uintptr_t R_CStackStart; /* Initial stack address * #define SIGBREAK 21 /* to readers pgrp upon background tty read */ /* one way to allow user interrupts: called in ProcessEvents */ -#ifdef _MSC_VER -__declspec(dllimport) int UserBreak; -#else -#ifndef WIN64 -#define UserBreak (*_imp__UserBreak) -#endif -extern int UserBreak; -#endif +/* UserBreak is declared with DLL-import linkage in Rdecl.h / Rembedded.h. */ /* calls into the R DLL */ extern char *getDLLVersion(void); diff --git a/tests-integration/JriAttach.java b/tests-integration/JriAttach.java index c84ae89..4187798 100644 --- a/tests-integration/JriAttach.java +++ b/tests-integration/JriAttach.java @@ -31,6 +31,28 @@ static void run() { String text = result == null ? "ERROR|no result" : result.asString(); String expected = "OK|attached|" + System.getProperty("java.home") + "|TRUE"; System.out.println("JRI host " + System.getProperty("java.version") + ": " + text); + if (System.getProperty("os.name").startsWith("Windows")) { + final String[] interrupted = new String[1]; + Thread evaluator = new Thread(new Runnable() { + public void run() { + REXP value = engine.eval("tryCatch({Sys.sleep(60); 'MISSED'}, interrupt=function(e) 'INTERRUPTED')"); + interrupted[0] = value == null ? "NULL" : value.asString(); + } + }); + evaluator.setDaemon(true); + evaluator.start(); + try { + Thread.sleep(1000); + engine.rniStop(0); + evaluator.join(10000); + } catch (InterruptedException e) { throw new RuntimeException(e); } + if (evaluator.isAlive() || !"INTERRUPTED".equals(interrupted[0]) || + engine.eval("as.integer(1+1)").asInt() != 2) { + System.err.println("FAIL: JRI interrupt/recovery: " + interrupted[0]); + System.exit(3); + } + System.out.println("PASS jri-interrupt-recovery"); + } engine.end(); if (!expected.equals(text)) { System.err.println("FAIL: expected " + expected); diff --git a/tests-integration/README.md b/tests-integration/README.md index 66c0c02..40583ea 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -109,20 +109,35 @@ is never changed. `dll-search.c` checks the removal with a positive control. Later native loads by the JVM (`zip`, `net`, `nio`, `management`, `sunmscapi`, `awt`, `fontmanager`, `freetype`) also need the JDK's MSVC runtime DLLs to be resolvable by basename. After verifying the JNI entry points, -djni retains `vcruntime140.dll`, `vcruntime140_1.dll` and `msvcp140.dll` when -available: an already loaded runtime is used first, then System32, then the -copy bundled in the selected JDK's `bin`. No persistent search directory or -`PATH` change is made. These references last until process exit, like the -selected JVM itself. This can bind later imports in other native components -to that runtime too; an already loaded or system copy is never replaced. -JDKs that omit a required runtime still need the appropriate redistributable. - -The Windows jobs hide all three System32 runtime DLLs, run JVM startup and -all later native loads for both JDKs, and fail on any error. `msvc.R` also -checks the loaded module paths: the runtimes must come from the selected -JDK, so another installed copy cannot silently mask a missing dependency. -The workflow restores the hidden DLLs in a `finally` block, including on a -failed test. +both explicit selection and host attachment retain the selected runtime's +MSVC DLLs. An already loaded copy is retained with `GetModuleHandleExW`; +it cannot safely be replaced. Otherwise a copy in the selected JDK's `bin` +is preferred, and System32 is used only when that bundled copy is absent. +This follows the normal `java.exe` preference for its own directory and +avoids selecting System32's older `msvcp140` after startup has already loaded +the JDK's `vcruntime140`. References last until process exit. No persistent +search directory or `PATH` change is made. A bundled DLL that exists but +fails to load is diagnosed on stderr rather than silently falling back to +a different copy. Missing optional DLLs are allowed; JDKs that omit a required +runtime still need a suitable redistributable. + +The base VC++ DLLs and bundled `msvcp140_1`, `_2` and `_atomic_wait` satellites +are covered. Already loaded copies retain host-process precedence: this can +bind Java to a host's incompatible older runtime. Use a fresh process with +a compatible runtime in that case; the loader does not replace live DLLs. + +Every Windows job tests normal System32 DLLs, their absence, and genuine +Microsoft VC++ 2015 Update 3 (14.0.24212.0) DLLs temporarily placed in System32. +The old installer is downloaded from Microsoft's distribution site and +pinned by SHA256; only its DLLs are extracted, and file versions must prove +they are older than each selected JDK's bundled copies. A clean-process +control loads the old System32 DLL. `msvc.R` then checks startup, late native +loads after host attachment, and actual loaded module paths for both JDKs. +A separate case releases the original reference to a preloaded MSVC DLL and +checks rJava's retained reference keeps it mapped. Java-host JRI also runs +without system DLLs and with the old copies. Files are restored in `finally`, +including on a failed test. These mutations occur only on disposable Actions +runners. ## Windows JRI builds @@ -132,6 +147,20 @@ explicit default targets so R's included Makeconf cannot select an unrelated rule. Windows uses its own console declaration instead of the Unix-only `Rinterface.h`; the x64 build also enables JRI's existing Win64 branches. +JRI is built by default, and a build failure stops installation. To omit it +explicitly, use `JRI=0 R CMD INSTALL rJava_.tar.gz`; normal rJava/JNI +functionality remains available. `IGNORE=1 R CMD INSTALL ...` instead attempts +JRI and permits installation if that optional build fails. CI forces a real +`javac` failure to check the default failure and `IGNORE=1` fallback, and tests +the explicit `JRI=0` path. A full JDK is needed to build JRI. + +`WIN64` is derived from the compiler's `_WIN64`, independently of `R_ARCH`. +The Windows import declaration for `UserBreak` comes from R's header on R +4.6+ and a compatibility declaration on older R. Generated JNI headers are +normalised to LF, avoiding a Windows-only line-ending check NOTE. The Java +host test interrupts a sleeping R evaluation through `rniStop` and requires +both interrupt delivery and subsequent R evaluation to succeed. + Every Windows runtime pair now runs `.jengine()` from R and a Java host embedding R through JRI. The latter runs with both JDKs as host, with `JAVA_HOME` deliberately naming the other runtime. The test requires rJava @@ -140,20 +169,27 @@ to reject that other runtime and attach to the host's already running JVM. ## Overlayfs kernel checks `overlay.yaml` boots real QEMU/KVM guests; a container alone would reuse the -runner's kernel. Each guest records its actual kernel, mount, mapping device -and inode, and the inode returned by `stat()`. Tests cover a fresh JVM, -a preloaded JVM library, a host-created JVM and replacement of a mapped -library through overlayfs. Each case runs in a fresh R process. - -The matrix includes Ubuntu 22.04's current 5.15 kernel, Ubuntu 24.04's -current kernel (at least 6.8), and the frozen Ubuntu mainline 6.1.0 build -`6.1.0-060100.202303090726` (packages pinned by SHA256). A current distro -kernel can carry fixes, so a release number below 6.8 alone does not predict -its behavior. The distro 5.15 and current-kernel jobs require same-file -reuse to succeed. The frozen mainline job requires the ambiguous overlay -identity to be rejected with the explicit diagnostic. Fresh selection and -attachment to a host-created VM must work on all three. Selecting a -different or replaced library must always fail. +runner's kernel. Ubuntu image snapshots and the frozen mainline kernel are +pinned by SHA256. Reboot readiness checks the boot ID rather than sleeping +for a fixed duration. Each guest records its actual kernel and mounts. + +Each kernel exercises four layouts: all layers on one filesystem, separate +ext4 filesystems with `xino=off`, separate ext4 filesystems with `xino=on`, +and a rootless mount created in an unprivileged user's user namespace. +Tests on privileged mounts run as the unprivileged user too. Each layout +covers a fresh JVM, a preloaded library, a host-created JVM, and replacement +of the mapped file, in separate R processes. Module identities and `stat()` +identities are logged; assertions distinguish a definite different-file +refusal from an unverifiable overlay identity. + +The kernels are Ubuntu 22.04's 5.15, Ubuntu 24.04's 6.8, and frozen mainline +6.1.0 (`6.1.0-060100.202303090726`). Older kernels with the mapping-identity +limitation reject ambiguous same-file reuse, while fresh startup and host +attachment work. Cross-filesystem `xino=off` can report real backing devices; +a device mismatch is conservatively unverifiable, while equal devices with +different inodes are definitely different files. A distro version below +6.8 alone does not predict the behavior, so fixtures assert the expected +behavior for the actual layout and record the kernel. ## Tests @@ -182,8 +218,8 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | every scenario | before anything is mapped, `RgetMappedJVM` is `NULL`: rJava's own JNI wrappers are not taken for a JVM | | `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where `.jloadJVM()` rejects the `JAVA_HOME` runtime and rJava attaches to the host JVM (all three platforms, both host JDKs) | | `dll-search.c` | Windows DLL search state after failed and successful loads | -| `msvc.R` | Windows later native loads without System32 VC++ DLLs; loaded runtime origins | -| `overlay.R` | actual distro 5.15, frozen mainline 6.1.0 and current-kernel overlayfs behavior | +| `msvc.R` | Windows normal/missing/2015 system runtimes, retained references, fresh startup and hosted late loads; loaded runtime origins | +| `overlay.R` | actual 5.15, 6.1.0 and 6.8 kernels; four overlay layouts, unprivileged processes, exact refusal classes | | downstream | rJavaEnv and two minimal consumer packages (current pair only) | Bounded matrix, each on Linux, macOS (arm64) and Windows with R release; the @@ -234,3 +270,8 @@ from the repository root, one process per scenario. other non-HotSpot VMs, Java runtimes without a JDK layout, R versions other than current release, Windows 4.5 and devel in `check.yaml`, and downstream packages beyond the two test consumers. + +Modifying a mapped JVM library in place without changing its device/inode is +outside the identity guarantee, as for other native libraries. Replace runtime +files atomically and use a fresh process after an upgrade. The tests exercise +unlink/replacement, not arbitrary writes to live native-code mappings. diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index ddc0ffd..e7e751b 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -368,6 +368,18 @@ switch(scenario, stopifnot(warned) start(expected) }, + retained = { + stopifnot(windows) + dll <- file.path(dirname(dirname(lib(home_a))), "msvcp140.dll") + dyn.load(dll) + dyn.load(Sys.getenv("RJE_JVMHOST")) + stopifnot(.C("rjava_test_module_loaded", "msvcp140.dll", result = 0L)$result == 1L) + rJava::.jloadJVM(lib(home_a)) + dyn.unload(dll) + stopifnot(.C("rjava_test_module_loaded", "msvcp140.dll", result = 0L)$result == 1L) + start(home_a) + later_loads() + }, laterload = { home <- if (variant == "b") home_b else home_a Sys.setenv(JAVA_HOME = home) @@ -386,7 +398,11 @@ switch(scenario, # jvm.dll of a copy of A's home, renamed as in replaced). .jinit() attaches # to the created VM, but .jloadJVM() rejects that name and B: a path # selects only the file mapped, also when a VM was created. - stopifnot(variant %in% c("", "explicit", "renamed", "replaced"), !(windows && variant == "renamed")) + if (variant == "laterload-b") { + swap <- home_a; home_a <- home_b; home_b <- swap + variant <- "laterload" + } + stopifnot(variant %in% c("", "explicit", "renamed", "replaced", "laterload"), !(windows && variant == "renamed")) helper <- Sys.getenv("RJE_JVMHOST") stopifnot(file.exists(helper)) dyn.load(helper) @@ -427,6 +443,8 @@ switch(scenario, } start(home) stopifnot(state()$state == "attached") + if (variant == "laterload") later_loads() + stopifnot(identical(minimal_path, Sys.getenv("PATH"))) }, jengine = { # R hosts the JVM, then JRI is loaded into it as a later native library. diff --git a/tests-integration/jri-optional.sh b/tests-integration/jri-optional.sh new file mode 100644 index 0000000..fa3b0c5 --- /dev/null +++ b/tests-integration/jri-optional.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +archive="$RUNNER_TEMP/$(basename "$(ls ../rJava_*.tar.gz)")" +work="$RUNNER_TEMP/jri-optional" +mkdir -p "$work"/{strict,ignore,skip} +# A real javac failure must stop the normal install. +if env IGNORE= JRI=1 JRI_JFLAGS=--rjava-intentional-invalid-option R CMD INSTALL \ + --library="$work/strict" "$archive" > "$work/strict.log" 2>&1; then + echo '::error::JRI failure unexpectedly succeeded'; exit 1 +fi +if command -v rg >/dev/null; then + rg -q -- 'rjava-intentional-invalid-option' "$work/strict.log" +else + grep -q -- 'rjava-intentional-invalid-option' "$work/strict.log" +fi +for mode in ignore skip; do + if [[ "$mode" == ignore ]]; then + env IGNORE=1 JRI=1 JRI_JFLAGS=--rjava-intentional-invalid-option R CMD INSTALL \ + --library="$work/$mode" "$archive" > "$work/$mode.log" 2>&1 + else + env IGNORE= JRI=0 R CMD INSTALL --library="$work/$mode" "$archive" > "$work/$mode.log" 2>&1 + fi + env R_LIBS="$work/$mode" JAVA_HOME="$RJE_JAVA_HOME_B" Rscript --vanilla -e \ + 'library(rJava); stopifnot(!file.exists(system.file("jri", "JRI.jar", package="rJava"))); .jinit(); stopifnot(.jcall("java/lang/System", "S", "getProperty", "java.version") != ""); cat("PASS rJava without JRI\n")' +done diff --git a/tests-integration/jvmhost.c b/tests-integration/jvmhost.c index 004d286..3e89db2 100644 --- a/tests-integration/jvmhost.c +++ b/tests-integration/jvmhost.c @@ -31,3 +31,10 @@ void rjava_test_create_jvm(char **path, char **classpath, int *result) { args.ignoreUnrecognized = JNI_TRUE; *result = create(&vm, &env, &args); } + +#ifdef _WIN32 +/* Observe retention after the component that first loaded a runtime lets go. */ +void rjava_test_module_loaded(char **name, int *result) { + *result = GetModuleHandleA(name[0]) != NULL; +} +#endif diff --git a/tests-integration/msvc.R b/tests-integration/msvc.R index 44941b5..b364f0a 100644 --- a/tests-integration/msvc.R +++ b/tests-integration/msvc.R @@ -1,13 +1,19 @@ -# Runs laterload (A or B) with System32 VC++ DLLs hidden by the workflow, -# then proves Windows actually loaded the selected JDK's bundled runtimes. +# Verify selected-JDK runtime origins for fresh startup and native-host attachment. source("tests-integration/deferred.R") -stopifnot(windows, scenario == "laterload") +stopifnot(windows, scenario %in% c("laterload", "hosted", "retained")) ps <- file.path(sysroot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") modules <- system2(ps, c("-NoProfile", "-Command", shQuote(sprintf("(Get-Process -Id %d).Modules | ForEach-Object FileName", Sys.getpid()))), stdout = TRUE) stopifnot(is.null(attr(modules, "status"))) -runtimes <- modules[grepl("[/\\\\](msvcp140|vcruntime140|vcruntime140_1)[.]dll$", modules, ignore.case = TRUE)] -stopifnot(any(tolower(basename(chartr("\\", "/", runtimes))) == "msvcp140.dll")) +runtimes <- modules[grepl("[/\\\\](msvcp140(_1|_2|_atomic_wait)?|vcruntime140(_1)?)[.]dll$", modules, ignore.case = TRUE)] +names <- tolower(basename(chartr("\\", "/", runtimes))) +stopifnot("msvcp140.dll" %in% names) selected <- normalizePath(if (variant == "b") home_b else home_a, winslash = "/") -stopifnot(all(startsWith(tolower(normalizePath(runtimes, winslash = "/")), paste0(tolower(selected), "/")))) +bin <- dirname(dirname(lib(selected))) +for (i in seq_along(runtimes)) { + bundled <- file.path(bin, names[i]) + expected <- if (file.exists(bundled)) bundled else file.path(sysroot, "System32", names[i]) + stopifnot(identical(tolower(normalizePath(runtimes[i], winslash = "/")), + tolower(normalizePath(expected, winslash = "/")))) +} cat("PASS bundled MSVC dependencies:", runtimes, sep = "\n ") diff --git a/tests-integration/overlay-layouts.sh b/tests-integration/overlay-layouts.sh new file mode 100644 index 0000000..7fb6141 --- /dev/null +++ b/tests-integration/overlay-layouts.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Runs inside the kernel VM. Mounts are privileged except rootless-inner; +# every R test runs as the unprivileged ubuntu user or a user-namespace root. +set -euo pipefail +expect=$1 +phase=${2:-outer} +run_cases() { + local layout=$1 home=$2 + findmnt "$home" + for scenario in fresh preloaded hosted replaced; do + if [[ "$phase" == rootless-inner ]]; then + env RJE_JAVA_HOME_A="$home" RJE_OVERLAY_LAYOUT="$layout" RJE_OVERLAY_USERNS=1 \ + Rscript --vanilla tests-integration/overlay.R "$scenario" + else + runuser -u ubuntu -- env RJE_JAVA_HOME_A="$home" RJE_OVERLAY_LAYOUT="$layout" \ + Rscript --vanilla tests-integration/overlay.R "$scenario" + fi + done +} +if [[ "$phase" == rootless-inner ]]; then + work=/home/ubuntu/overlay-rootless + mkdir -p "$work"/{upper,work,merged} + mount -t overlay overlay -o "lowerdir=/opt/overlay-lower,upperdir=$work/upper,workdir=$work/work,userxattr" "$work/merged" + trap 'umount "$work/merged"' EXIT + cat /proc/self/uid_map + run_cases rootless "$work/merged" + exit +fi +export RJE_OVERLAY_EXPECT="$expect" +mkdir -p /opt/overlay-lower /opt/jdk-b /opt/overlay-upper +cp -a "$JAVA_HOME/." /opt/overlay-lower/ +cp -a "$JAVA_HOME/." /opt/jdk-b/ +chown -R ubuntu:ubuntu /opt/overlay-lower +# A second real ext4 filesystem, not merely another directory on the same fs. +truncate -s 1G /opt/overlay-upper.img +mkfs.ext4 -q -F /opt/overlay-upper.img +mount -o loop /opt/overlay-upper.img /opt/overlay-upper +export RJE_JAVA_HOME_B=/opt/jdk-b +for layout in same cross-off cross-on; do + upper=/opt/overlay-upper + options= + if [[ "$layout" == same ]]; then upper=/opt; fi + if [[ "$layout" == cross-off ]]; then options=,xino=off; fi + if [[ "$layout" == cross-on ]]; then options=,xino=on; fi + work="$upper/rjava-$layout" + mkdir -p "$work"/{upper,work,merged} + chown -R ubuntu:ubuntu "$work" + mount -t overlay overlay -o "lowerdir=/opt/overlay-lower,upperdir=$work/upper,workdir=$work/work$options" "$work/merged" + run_cases "$layout" "$work/merged" + umount "$work/merged" +done +# Configure the isolated VM to allow the user namespaces being tested. +if [[ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then + sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 +fi +if [[ -f /proc/sys/kernel/unprivileged_userns_clone ]]; then + sysctl -w kernel.unprivileged_userns_clone=1 +fi +runuser -u ubuntu -- unshare --user --map-root-user --mount --propagation private \ + bash tests-integration/overlay-layouts.sh "$expect" rootless-inner +umount /opt/overlay-upper diff --git a/tests-integration/overlay-vm.sh b/tests-integration/overlay-vm.sh index 5c8c7d3..df36fa6 100644 --- a/tests-integration/overlay-vm.sh +++ b/tests-integration/overlay-vm.sh @@ -6,15 +6,15 @@ expect=$2 work="$RUNNER_TEMP/overlay-$release" mkdir -p "$work" case "$release" in - jammy) version=22.04 ;; - noble) version=24.04 ;; + jammy) version=22.04; snapshot=20261004; image_sha=012d81fade7e8ff4428fc35dfeee711d66d1f47d5ea9c5c8ced96a85b959835c ;; + noble) version=24.04; snapshot=20261017; image_sha=04e8f2461a16a44e78a370284471f29616cdd9baf1893df25807b71f985b6c94 ;; *) exit 2 ;; esac image="ubuntu-$version-server-cloudimg-amd64.img" -base="https://cloud-images.ubuntu.com/releases/$release/release" +base="https://cloud-images.ubuntu.com/releases/$release/release-$snapshot" curl --fail --location --retry 3 "$base/$image" -o "$work/$image" -curl --fail --location --retry 3 "$base/SHA256SUMS" -o "$work/SHA256SUMS" -(cd "$work" && awk -v image="$image" '$2 == image || $2 == "*" image' SHA256SUMS > selected.sha256 && test -s selected.sha256 && sha256sum -c selected.sha256) +printf '%s %s\n' "$image_sha" "$image" > "$work/selected.sha256" +(cd "$work" && sha256sum -c selected.sha256) qemu-img resize "$work/$image" 16G ssh-keygen -t ed25519 -N '' -f "$work/key" cat > "$work/user-data" </dev/null; then return 0; fi + if boot=$(ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 cat /proc/sys/kernel/random/boot_id 2>/dev/null); then + if [[ -n "$boot" && "$boot" != "$previous_boot" ]]; then return 0; fi + fi sleep 5 done cat "$work/serial.log" @@ -63,9 +66,9 @@ ef4cf195e9e6840318e7fe9227d4ddccaf44ce44f0324d5dbd418ecaa72dc2d4 ${packages[1]} SUMS (cd "$work" && sha256sum -c kernel.sha256) scp "${ssh_opts[@]}" -P 2222 "${packages[@]/#/$work/}" ubuntu@127.0.0.1: - ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 'cloud-init status --wait && sudo dpkg -i linux-modules-*.deb linux-image-*.deb && sudo systemd-run --on-active=3s /sbin/reboot' - sleep 15 - wait_for_ssh + previous_boot=$(ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 cat /proc/sys/kernel/random/boot_id) + ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 'set -e; cloud-init status --wait || test "$?" -eq 2; sudo dpkg -i linux-modules-*.deb linux-image-*.deb; sudo systemd-run --on-active=3s /sbin/reboot' + wait_for_ssh "$previous_boot" ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 'test "$(uname -r)" = 6.1.0-060100-generic' fi # Include the checked-out REngine submodule, without Git metadata or VM files. @@ -74,9 +77,9 @@ scp "${ssh_opts[@]}" -P 2222 "$work/source.tar.gz" ubuntu@127.0.0.1:source.tar.g ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 "sudo bash -s -- '$expect'" <<'GUEST' set -euo pipefail export DEBIAN_FRONTEND=noninteractive -cloud-init status --wait +cloud-init status --wait || [[ $? == 2 ]] apt-get update -qq -apt-get install -y --no-install-recommends r-base-dev openjdk-17-jdk-headless libtirpc-dev libicu-dev make g++ unzip zip +apt-get install -y --no-install-recommends r-base-dev openjdk-17-jdk-headless libtirpc-dev libicu-dev e2fsprogs util-linux make g++ unzip zip mkdir /home/ubuntu/source cd /home/ubuntu/source tar -xzf ../source.tar.gz @@ -86,18 +89,10 @@ sh mkdist > /home/ubuntu/build.log 2>&1 || { cat /home/ubuntu/build.log; exit 1; mkdir /opt/rjava-lib R CMD INSTALL --library=/opt/rjava-lib ../rJava_*.tar.gz export R_LIBS=/opt/rjava-lib -mkdir -p /opt/overlay/{lower,upper,work,merged} /opt/jdk-b -cp -a "$JAVA_HOME/." /opt/overlay/lower/ -cp -a "$JAVA_HOME/." /opt/jdk-b/ -mount -t overlay overlay -o lowerdir=/opt/overlay/lower,upperdir=/opt/overlay/upper,workdir=/opt/overlay/work /opt/overlay/merged -findmnt /opt/overlay/merged uname -a -export RJE_JAVA_HOME_A=/opt/overlay/merged RJE_JAVA_HOME_B=/opt/jdk-b RJE_OVERLAY_EXPECT="$1" mkdir /tmp/jvmhost cp tests-integration/jvmhost.c /tmp/jvmhost/ (cd /tmp/jvmhost && PKG_CPPFLAGS=-I/home/ubuntu/source/src R CMD SHLIB jvmhost.c) export RJE_JVMHOST=/tmp/jvmhost/jvmhost.so -for scenario in fresh preloaded hosted replaced; do - Rscript --vanilla tests-integration/overlay.R "$scenario" -done +bash tests-integration/overlay-layouts.sh "$1" GUEST diff --git a/tests-integration/overlay.R b/tests-integration/overlay.R index 9c5c163..aa0063d 100644 --- a/tests-integration/overlay.R +++ b/tests-integration/overlay.R @@ -1,6 +1,15 @@ # Real overlayfs in a VM. Each mode runs in a fresh process. scenario <- commandArgs(TRUE)[1] expect <- Sys.getenv("RJE_OVERLAY_EXPECT") +layout <- Sys.getenv("RJE_OVERLAY_LAYOUT") +stopifnot(layout %in% c("same", "cross-off", "cross-on", "rootless")) +if (Sys.getenv("RJE_OVERLAY_USERNS") == "1") { + ids <- scan("/proc/self/uid_map", quiet = TRUE) + stopifnot(Sys.getuid() == 0L, length(ids) == 3L, ids[1] == 0, ids[2] != 0, ids[3] == 1) +} else stopifnot(Sys.getuid() != 0L) +ambiguous <- "^Cannot verify that .*because the overlayfs mapping identity cannot be verified[.]" +different <- "^Another JVM library is already mapped:" +same_ambiguous <- expect == "legacy" && layout != "cross-off" stopifnot(expect %in% c("legacy", "distro", "current"), Sys.info()[["sysname"]] == "Linux") kernel <- numeric_version(sub("-.*", "", Sys.info()[["release"]])) stopifnot(if (expect == "current") kernel >= "6.8" else kernel < "6.8") @@ -34,21 +43,21 @@ if (scenario == "fresh") { # Record the actual mapping and overlay inode in CI, not just kernel versions. cat(readLines("/proc/self/maps")[grepl("libjvm", readLines("/proc/self/maps"))], sep = "\n") system2("stat", c("-Lc", shQuote("overlay device=%d inode=%i"), shQuote(lib(a)))) - reject(lib(b), "already mapped") + reject(lib(b), different) if (scenario == "replaced") { # Replacing through the overlay triggers copy-up/whiteout behavior. path <- lib(a) stopifnot(unlink(path) == 0L, file.copy(lib(b), path)) - reject(path, "already mapped") + reject(path, if (expect == "legacy" || layout == "cross-off") ambiguous else different) } else { - if (expect == "legacy") reject(lib(a), "overlayfs mapping identity cannot be verified") else { + if (same_ambiguous) reject(lib(a), ambiguous) else { stopifnot(isTRUE(rJava::.jloadJVM(lib(a)))) } if (scenario == "hosted") { Sys.setenv(JAVA_HOME = b) start() # Attach has no pathname comparison, including on old overlayfs. stopifnot(state() == "attached") - } else if (expect != "legacy") start() + } else if (!same_ambiguous) start() } } -cat("PASS overlay", expect, as.character(kernel), scenario, "\n") +cat("PASS overlay", expect, as.character(kernel), layout, scenario, "\n") diff --git a/tests-integration/windows-msvc.ps1 b/tests-integration/windows-msvc.ps1 new file mode 100644 index 0000000..333fb64 --- /dev/null +++ b/tests-integration/windows-msvc.ps1 @@ -0,0 +1,91 @@ +param([ValidateSet('normal', 'missing', 'old')][string]$Mode) +$ErrorActionPreference = 'Stop' +$names = @('vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'msvcp140_atomic_wait.dll') +$system = Join-Path $env:SystemRoot 'System32' +$work = Join-Path $env:RUNNER_TEMP 'msvc-2015' +$hidden = @() +$installed = @() +function Invoke-R([string[]]$RArguments) { + & Rscript --vanilla @RArguments + if ($LASTEXITCODE -ne 0) { throw "R test failed ($Mode): $RArguments" } +} +if ($Mode -eq 'old') { + New-Item -ItemType Directory -Force $work | Out-Null + $exe = Join-Path $work 'vc_redist.x64.exe' + # Microsoft Visual C++ 2015 Update 3, 14.0.24212.0. Never install it globally. + # Official download page: https://www.microsoft.com/en-us/download/details.aspx?id=53587 + Invoke-WebRequest 'https://download.microsoft.com/download/6/d/f/6df3ff94-f7f9-4f0b-838c-a328d1a7d0ee/vc_redist.x64.exe' -OutFile $exe + if ((Get-FileHash $exe -Algorithm SHA256).Hash -ne 'D7257265DBC0635C96DD67DDF938A09ABE0866CB2D4FA05F8B758C8644E724E4') { throw 'VC++ 2015 installer SHA256 mismatch' } + # Extract its pinned Burn payload cabinet, then its Minimum Runtime cabinet. + # The cabinet offsets/length correspond to the exact SHA256 above. + $bytes = [IO.File]::ReadAllBytes($exe) + $cab = New-Object byte[] 14465070 + [Array]::Copy($bytes, 821856, $cab, 0, $cab.Length) + [IO.File]::WriteAllBytes((Join-Path $work 'payload.cab'), $cab) + & 7z x -y "-o$work/payload" "$work/payload.cab" a10 | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Burn payload extraction failed' } + & 7z x -y "-o$work/old" "$work/payload/a10" msvcp140.dll vcruntime140.dll | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Runtime cabinet extraction failed' } + foreach ($name in 'msvcp140.dll', 'vcruntime140.dll') { + $old = Get-Item "$work/old/$name" + if ($old.VersionInfo.FileMajorPart -ne 14 -or $old.VersionInfo.FileMinorPart -ne 0 -or $old.VersionInfo.FileBuildPart -ne 24212) { throw "Wrong old runtime: $name" } + Write-Output "Old fixture $name version $($old.VersionInfo.FileVersion), SHA256 $((Get-FileHash $old).Hash)" + foreach ($home in $env:RJE_JAVA_HOME_A, $env:RJE_JAVA_HOME_B) { + $bundles = Get-ChildItem $home -Recurse -Filter $name + if (!$bundles) { throw "No bundled $name in $home" } + foreach ($bundle in $bundles) { + $v = $bundle.VersionInfo + $version = [version]::new($v.FileMajorPart, $v.FileMinorPart, $v.FileBuildPart, $v.FilePrivatePart) + if ($version -le [version]'14.0.24212.0') { throw "Fixture is not older than $bundle ($version)" } + Write-Output "JDK $bundle version $version" + } + } + } +} +try { + if ($Mode -ne 'normal') { + foreach ($name in $names) { + $path = Join-Path $system $name + if (Test-Path $path) { + takeown /f $path | Out-Null + if ($LASTEXITCODE -ne 0) { throw "takeown failed: $path" } + icacls $path /grant '*S-1-5-32-544:F' | Out-Null + if ($LASTEXITCODE -ne 0) { throw "icacls failed: $path" } + Rename-Item $path "$name.rjava-hidden" + $hidden += $path + } + if (Test-Path $path) { throw "Runtime still visible: $path" } + } + if ($Mode -eq 'old') { + foreach ($name in 'msvcp140.dll', 'vcruntime140.dll') { + $path = Join-Path $system $name + Copy-Item "$work/old/$name" $path + $installed += $path + } + # Positive control: a clean R process really can load the old system DLL. + Invoke-R -RArguments @('-e', 'p <- file.path(Sys.getenv("SystemRoot"), "System32", "msvcp140.dll"); d <- dyn.load(p); stopifnot(tolower(normalizePath(d[["path"]])) == tolower(normalizePath(p))); cat("PASS old System32 runtime control\n")') + } + } + Write-Output "Testing runtime policy: $Mode" + Invoke-R -RArguments @('tests-integration/msvc.R', 'retained') + Invoke-R -RArguments @('tests-integration/msvc.R', 'laterload') + Invoke-R -RArguments @('tests-integration/msvc.R', 'laterload', 'b') + Invoke-R -RArguments @('tests-integration/msvc.R', 'hosted', 'laterload') + Invoke-R -RArguments @('tests-integration/msvc.R', 'hosted', 'laterload-b') + if ($Mode -ne 'normal') { + Invoke-R -RArguments @('tests-integration/jri.R') + $a = $env:RJE_JAVA_HOME_A + $b = $env:RJE_JAVA_HOME_B + try { + $env:RJE_JAVA_HOME_A = $b + $env:RJE_JAVA_HOME_B = $a + Invoke-R -RArguments @('tests-integration/jri.R') + } finally { + $env:RJE_JAVA_HOME_A = $a + $env:RJE_JAVA_HOME_B = $b + } + } +} finally { + foreach ($path in $installed) { Remove-Item $path -Force } + foreach ($path in $hidden) { Rename-Item "$path.rjava-hidden" (Split-Path $path -Leaf) } +} From 81e71f1d41028a4d755134526bc1abfe38f93d07 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 11:26:50 +0200 Subject: [PATCH 40/63] test: make runtime retention and unprivileged overlay fixtures precise --- src/djni.c | 8 ++++++++ tests-integration/deferred.R | 9 ++++++++- tests-integration/overlay.R | 5 +++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/djni.c b/src/djni.c index 79fd833..b69a121 100644 --- a/src/djni.c +++ b/src/djni.c @@ -314,6 +314,14 @@ static void load_bundled_msvc(void) { candidate[len] = L'\\'; wcscpy(candidate + len + 1, names[i]); attrs = GetFileAttributesW(candidate); + error = attrs == INVALID_FILE_ATTRIBUTES ? GetLastError() : 0; + if (error && error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { + name = to_utf8(candidate); + fprintf(stderr, "rJava: cannot inspect bundled MSVC runtime %s (Windows error %lu).\n", + name ? name : "(unknown path)", (unsigned long) error); + free(name); + continue; + } if (attrs != INVALID_FILE_ATTRIBUTES) retained[i] = LoadLibraryExW(candidate, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index e7e751b..b1a2b9c 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -370,12 +370,19 @@ switch(scenario, }, retained = { stopifnot(windows) - dll <- file.path(dirname(dirname(lib(home_a))), "msvcp140.dll") + bin <- dirname(dirname(lib(home_a))) + # Keep the fixture coherent before rJava runs: a plain dyn.load(msvcp) + # otherwise pulls its VCRUNTIME imports from System32. + dependencies <- file.path(bin, c("vcruntime140.dll", "vcruntime140_1.dll")) + dependencies <- dependencies[file.exists(dependencies)] + for (dependency in dependencies) dyn.load(dependency) + dll <- file.path(bin, "msvcp140.dll") dyn.load(dll) dyn.load(Sys.getenv("RJE_JVMHOST")) stopifnot(.C("rjava_test_module_loaded", "msvcp140.dll", result = 0L)$result == 1L) rJava::.jloadJVM(lib(home_a)) dyn.unload(dll) + for (dependency in rev(dependencies)) dyn.unload(dependency) stopifnot(.C("rjava_test_module_loaded", "msvcp140.dll", result = 0L)$result == 1L) start(home_a) later_loads() diff --git a/tests-integration/overlay.R b/tests-integration/overlay.R index aa0063d..92a3002 100644 --- a/tests-integration/overlay.R +++ b/tests-integration/overlay.R @@ -2,11 +2,12 @@ scenario <- commandArgs(TRUE)[1] expect <- Sys.getenv("RJE_OVERLAY_EXPECT") layout <- Sys.getenv("RJE_OVERLAY_LAYOUT") +uid <- as.integer(system2("id", "-u", stdout = TRUE)) stopifnot(layout %in% c("same", "cross-off", "cross-on", "rootless")) if (Sys.getenv("RJE_OVERLAY_USERNS") == "1") { ids <- scan("/proc/self/uid_map", quiet = TRUE) - stopifnot(Sys.getuid() == 0L, length(ids) == 3L, ids[1] == 0, ids[2] != 0, ids[3] == 1) -} else stopifnot(Sys.getuid() != 0L) + stopifnot(uid == 0L, length(ids) == 3L, ids[1] == 0, ids[2] != 0, ids[3] == 1) +} else stopifnot(uid != 0L) ambiguous <- "^Cannot verify that .*because the overlayfs mapping identity cannot be verified[.]" different <- "^Another JVM library is already mapped:" same_ambiguous <- expect == "legacy" && layout != "cross-off" From f159b76d5e0f626ac67fc7fee220a3d8abe6c4d4 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 11:31:44 +0200 Subject: [PATCH 41/63] test: isolate host startup dependencies and verify cross-filesystem refusals --- tests-integration/README.md | 9 ++++++--- tests-integration/jvmhost.c | 23 ++++++++++++++++++++++- tests-integration/overlay.R | 2 +- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/tests-integration/README.md b/tests-integration/README.md index 40583ea..45e0224 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -133,7 +133,9 @@ pinned by SHA256; only its DLLs are extracted, and file versions must prove they are older than each selected JDK's bundled copies. A clean-process control loads the old System32 DLL. `msvc.R` then checks startup, late native loads after host attachment, and actual loaded module paths for both JDKs. -A separate case releases the original reference to a preloaded MSVC DLL and +The native host fixture selects its startup dependencies with a scoped JDK +`bin` search; rJava must resolve late dependencies independently after that +search scope has ended. A separate case releases the original reference to a preloaded MSVC DLL and checks rJava's retained reference keeps it mapped. Java-host JRI also runs without system DLLs and with the old copies. Files are restored in `finally`, including on a failed test. These mutations occur only on disposable Actions @@ -185,8 +187,9 @@ refusal from an unverifiable overlay identity. The kernels are Ubuntu 22.04's 5.15, Ubuntu 24.04's 6.8, and frozen mainline 6.1.0 (`6.1.0-060100.202303090726`). Older kernels with the mapping-identity limitation reject ambiguous same-file reuse, while fresh startup and host -attachment work. Cross-filesystem `xino=off` can report real backing devices; -a device mismatch is conservatively unverifiable, while equal devices with +attachment work. Cross-filesystem `xino=off` also produces a device mismatch on these modern +kernels, so preloaded path selection is conservatively refused there too. A +device mismatch is unverifiable, while equal devices with different inodes are definitely different files. A distro version below 6.8 alone does not predict the behavior, so fixtures assert the expected behavior for the actual layout and record the kernel. diff --git a/tests-integration/jvmhost.c b/tests-integration/jvmhost.c index 3e89db2..9e1f091 100644 --- a/tests-integration/jvmhost.c +++ b/tests-integration/jvmhost.c @@ -3,7 +3,10 @@ global symbols, like the java launcher. Build with PKG_CPPFLAGS=-I/src R CMD SHLIB jvmhost.c */ #ifdef _WIN32 +#define _WIN32_WINNT 0x0602 #include +#include +#include #else #include #endif @@ -16,7 +19,25 @@ void rjava_test_create_jvm(char **path, char **classpath, int *result) { JavaVM *vm; void *env; #ifdef _WIN32 - HMODULE lib = LoadLibraryExA(path[0], NULL, LOAD_WITH_ALTERED_SEARCH_PATH); + /* A real native host must choose its own startup dependencies. Use the + selected JDK's bin only during this load, like java.exe's app directory; + rJava must independently arrange any later MSVC loads after attaching. */ + int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path[0], -1, NULL, 0); + wchar_t *wide = n ? malloc(sizeof(wchar_t) * n) : NULL; + wchar_t *bin = n ? malloc(sizeof(wchar_t) * n) : NULL, *slash; + DLL_DIRECTORY_COOKIE cookie = NULL; + HMODULE lib = NULL; + if (wide && bin && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path[0], -1, wide, n)) { + for (slash = wide; *slash; slash++) if (*slash == L'/') *slash = L'\\'; + wcscpy(bin, wide); + if ((slash = wcsrchr(bin, L'\\'))) { + *slash = 0; + if ((slash = wcsrchr(bin, L'\\'))) { *slash = 0; cookie = AddDllDirectory(bin); } + } + lib = LoadLibraryExW(wide, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (cookie) RemoveDllDirectory(cookie); + } + free(wide); free(bin); if (lib) create = (JNI_CreateJavaVM_fnptr) GetProcAddress(lib, "JNI_CreateJavaVM"); #else void *lib = dlopen(path[0], RTLD_NOW | RTLD_GLOBAL); diff --git a/tests-integration/overlay.R b/tests-integration/overlay.R index 92a3002..d5c8462 100644 --- a/tests-integration/overlay.R +++ b/tests-integration/overlay.R @@ -10,7 +10,7 @@ if (Sys.getenv("RJE_OVERLAY_USERNS") == "1") { } else stopifnot(uid != 0L) ambiguous <- "^Cannot verify that .*because the overlayfs mapping identity cannot be verified[.]" different <- "^Another JVM library is already mapped:" -same_ambiguous <- expect == "legacy" && layout != "cross-off" +same_ambiguous <- expect == "legacy" || layout == "cross-off" stopifnot(expect %in% c("legacy", "distro", "current"), Sys.info()[["sysname"]] == "Linux") kernel <- numeric_version(sub("-.*", "", Sys.info()[["release"]])) stopifnot(if (expect == "current") kernel >= "6.8" else kernel < "6.8") From f4fc341015f4a81e8e54f82e59181b35609b639f Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 11:35:15 +0200 Subject: [PATCH 42/63] test: avoid PowerShell reserved HOME in old-runtime verification --- tests-integration/windows-msvc.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests-integration/windows-msvc.ps1 b/tests-integration/windows-msvc.ps1 index 333fb64..d1b6089 100644 --- a/tests-integration/windows-msvc.ps1 +++ b/tests-integration/windows-msvc.ps1 @@ -30,9 +30,9 @@ if ($Mode -eq 'old') { $old = Get-Item "$work/old/$name" if ($old.VersionInfo.FileMajorPart -ne 14 -or $old.VersionInfo.FileMinorPart -ne 0 -or $old.VersionInfo.FileBuildPart -ne 24212) { throw "Wrong old runtime: $name" } Write-Output "Old fixture $name version $($old.VersionInfo.FileVersion), SHA256 $((Get-FileHash $old).Hash)" - foreach ($home in $env:RJE_JAVA_HOME_A, $env:RJE_JAVA_HOME_B) { - $bundles = Get-ChildItem $home -Recurse -Filter $name - if (!$bundles) { throw "No bundled $name in $home" } + foreach ($javaHome in $env:RJE_JAVA_HOME_A, $env:RJE_JAVA_HOME_B) { + $bundles = Get-ChildItem $javaHome -Recurse -Filter $name + if (!$bundles) { throw "No bundled $name in $javaHome" } foreach ($bundle in $bundles) { $v = $bundle.VersionInfo $version = [version]::new($v.FileMajorPart, $v.FileMinorPart, $v.FileBuildPart, $v.FilePrivatePart) From 3f4b72c70c5a0b8ea3812be5bb91f938f048c8a1 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Sun, 18 Oct 2026 11:37:41 +0200 Subject: [PATCH 43/63] test: use a task-specific overlay path variable --- tests-integration/overlay-layouts.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests-integration/overlay-layouts.sh b/tests-integration/overlay-layouts.sh index 7fb6141..bbd8d39 100644 --- a/tests-integration/overlay-layouts.sh +++ b/tests-integration/overlay-layouts.sh @@ -5,14 +5,14 @@ set -euo pipefail expect=$1 phase=${2:-outer} run_cases() { - local layout=$1 home=$2 - findmnt "$home" + local layout=$1 selected_home=$2 + findmnt "$selected_home" for scenario in fresh preloaded hosted replaced; do if [[ "$phase" == rootless-inner ]]; then - env RJE_JAVA_HOME_A="$home" RJE_OVERLAY_LAYOUT="$layout" RJE_OVERLAY_USERNS=1 \ + env RJE_JAVA_HOME_A="$selected_home" RJE_OVERLAY_LAYOUT="$layout" RJE_OVERLAY_USERNS=1 \ Rscript --vanilla tests-integration/overlay.R "$scenario" else - runuser -u ubuntu -- env RJE_JAVA_HOME_A="$home" RJE_OVERLAY_LAYOUT="$layout" \ + runuser -u ubuntu -- env RJE_JAVA_HOME_A="$selected_home" RJE_OVERLAY_LAYOUT="$layout" \ Rscript --vanilla tests-integration/overlay.R "$scenario" fi done From f53daf4314dcdece7cb4d6d49b27fce04aee561a Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:01:38 +0200 Subject: [PATCH 44/63] test: cover Linux arm64, musl, R 3.6 and the CRAN xlsx package --- .github/workflows/dynamic.yaml | 61 +++++++++++++++++++++- tests-integration/musl.sh | 92 ++++++++++++++++++++++++++++++++++ tests-integration/xlsx.R | 42 ++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100755 tests-integration/musl.sh create mode 100644 tests-integration/xlsx.R diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index f2b7577..ce0d8eb 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -8,11 +8,12 @@ jobs: # Three runtime pairs per platform instead of every OS x vendor x version: # legacy layouts (JDK 8/11), current LTS (17/21, with downstream packages), # and other vendors spanning 11 to 25. The build JDK is always Temurin 17. + # Linux arm64 runs all three: JDK 8 there uses jre/lib/aarch64/server. name: ${{ matrix.os }} ${{ matrix.set }} (${{ matrix.a-dist }} ${{ matrix.a-version }}, ${{ matrix.b-dist }} ${{ matrix.b-version }}) strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] set: [legacy, current, vendor] include: - { set: legacy, a-dist: temurin, a-version: '8', b-dist: temurin, b-version: '11' } @@ -21,6 +22,8 @@ jobs: - { set: current, a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21', downstream: true } - { set: vendor, a-dist: corretto, a-version: '11', b-dist: zulu, b-version: '25' } - { set: r45, os: windows-latest, r: '4.5', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } + # The oldest R that DESCRIPTION accepts. + - { set: r36, os: ubuntu-latest, r: '3.6', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -219,6 +222,36 @@ jobs: working-directory: downstream-source shell: Rscript {0} run: source("tools/test-downstream.R") + - name: Install xlsx test dependencies in a separate library + if: matrix.downstream + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/xlsx-lib" "$RUNNER_TEMP/xlsx-src" + # rJava is already on R_LIBS, so it is not installed as a dependency. + Rscript -e 'lib <- file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-lib") + install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) + stopifnot(!dir.exists(file.path(lib, "rJava"))) + download.packages("xlsx", file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-src"), type = "source")' + { + echo "RJE_RJAVA_LIB=$R_LIBS" + echo "R_LIBS=$R_LIBS${{ runner.os == 'Windows' && ';' || ':' }}$RUNNER_TEMP/xlsx-lib" + } >> "$GITHUB_ENV" + - name: Start Java from a CRAN package's .onLoad (xlsx) + if: matrix.downstream + shell: bash + run: Rscript --vanilla tests-integration/xlsx.R + - name: Check the CRAN xlsx package and its tests with dynamic rJava + if: matrix.downstream + shell: bash + env: + _R_CHECK_FORCE_SUGGESTS_: 'false' + run: | + set -euo pipefail + cd "$RUNNER_TEMP/xlsx-src" + # JDK B is not the build JDK on PATH. NOAWT makes rJava set AWT headless. + JAVA_HOME="$RJE_JAVA_HOME_B" R CMD check --no-manual --ignore-vignettes xlsx_*.tar.gz + cat xlsx.Rcheck/tests/testthat.Rout* | tail -n 20 - uses: actions/upload-artifact@v4 if: always() with: @@ -226,3 +259,29 @@ jobs: path: | ${{ runner.temp }}/rJava_*.tar.gz ${{ runner.temp }}/linkage.txt + musl: + # musl libc with Alpine's R and OpenJDK packages. Docker runs the same + # script on x86_64 and arm64 hosts; the container root is overlayfs on the + # runner's kernel, as in a typical Docker deployment. + name: alpine ${{ matrix.arch }} ${{ matrix.set }} (openjdk ${{ matrix.a }}, openjdk ${{ matrix.b }}) + strategy: + fail-fast: false + matrix: + include: + - { arch: x86_64, runner: ubuntu-24.04, set: legacy, a: '8', b: '11' } + - { arch: x86_64, runner: ubuntu-24.04, set: current, a: '17', b: '21' } + - { arch: aarch64, runner: ubuntu-24.04-arm, set: current, a: '17', b: '21' } + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Check deferred loading, JRI and host attachment on musl + env: + # alpine:3.24.2 + IMAGE: alpine@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 + run: | + docker run --rm -v "$PWD:/src:ro" "$IMAGE" \ + sh -c 'apk add --no-cache bash && exec bash /src/tests-integration/musl.sh "$1" "$2" "$3"' \ + sh '${{ matrix.a }}' '${{ matrix.b }}' '${{ matrix.arch }}' diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh new file mode 100755 index 0000000..abe4a2e --- /dev/null +++ b/tests-integration/musl.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Runs the Linux deferred-loading checks against musl libc, R and OpenJDK +# from Alpine packages, inside an Alpine container: +# docker run --rm -v "$PWD:/src:ro" alpine:3.24 \ +# sh -c 'apk add --no-cache bash && bash /src/tests-integration/musl.sh 17 21 x86_64' +# The arguments are the Alpine OpenJDK versions of JDK A and JDK B, and the +# expected machine. The source is copied first: mkdist writes into the tree +# and its parent. +set -euo pipefail +version_a="$1" +version_b="$2" +[[ "$(uname -m)" == "$3" ]] || { echo "::error::machine is $(uname -m), expected $3"; exit 1; } +apk add --no-cache build-base R R-dev binutils coreutils findutils grep \ + "openjdk$version_a-jdk" "openjdk$version_b-jdk" openjdk17-jdk +export LANG=C.UTF-8 LC_ALL=C.UTF-8 +home() { + local dir + for dir in "/usr/lib/jvm/java-$1-openjdk" "/usr/lib/jvm/java-1.$1-openjdk"; do + if [[ -d "$dir" ]]; then echo "$dir"; return; fi + done + echo "no Alpine OpenJDK $1 home" >&2 + return 1 +} +export RJE_JAVA_HOME_A="$(home "$version_a")" +export RJE_JAVA_HOME_B="$(home "$version_b")" +export RJE_FALLBACK_HOME="$RJE_JAVA_HOME_B" +export JAVA_HOME="$(home 17)" NOAWT=1 +export PATH="$JAVA_HOME/bin:$PATH" + +echo "::group::Environment" +cat /etc/alpine-release +ldd --version 2>&1 | head -n 2 || true +uname -srm +R --version | head -n 1 +# The container root is normally overlayfs on the runner's kernel. +stat -f -c 'JDK filesystem: %T' "$RJE_JAVA_HOME_A" +# deferred.R reduces PATH to /usr/bin:/bin, which must not provide a java. +if [[ -e /usr/bin/java || -e /bin/java ]]; then echo "::error::java on the reduced PATH would mask discovery"; exit 1; fi +echo "::endgroup::" + +for jhome in "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B"; do + echo "== $jhome"; "$jhome/bin/java" -version 2>&1 + find "$jhome/" -name 'libjvm.*' -print + lib="$(find "$jhome/" -name 'libjvm.*' -path '*server*' -print -quit)" + # These must be musl builds, not glibc builds run through a compatibility layer. + readelf -d "$lib" | grep -E 'NEEDED.*libc\.musl-' || { echo "::error::$lib is not linked against musl"; exit 1; } + syms="$(nm -D --defined-only "$lib")" + for sym in JNI_CreateJavaVM JNI_GetCreatedJavaVMs JVM_CurrentTimeMillis; do + if grep -Eq "[ _]$sym(@|\$| )" <<< "$syms"; then echo "exports $sym"; else echo "::error::$lib does not export $sym"; exit 1; fi + done +done + +R CMD javareconf +rm -rf /work && mkdir -p /work/rJava /work/lib /work/jvmhost +cp -a /src/. /work/rJava/ +cd /work/rJava +sh mkdist +R CMD INSTALL --library=/work/lib /work/rJava_*.tar.gz +export R_LIBS=/work/lib + +lib="$(Rscript --vanilla -e 'cat(system.file("libs", package="rJava"))')" +ldd "$lib/rJava.so" +if ldd "$lib/rJava.so" | grep -i libjvm; then echo "::error::rJava.so depends on libjvm"; exit 1; fi + +Rscript --vanilla tests-integration/jni-abi.R "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" +cp tests-integration/jvmhost.c /work/jvmhost/ +(cd /work/jvmhost && PKG_CPPFLAGS="-I/work/rJava/src" R CMD SHLIB jvmhost.c) +export RJE_JVMHOST=/work/jvmhost/jvmhost.so + +# The scenarios of dynamic.yaml that apply to Linux. +run() { echo "::group::$*"; Rscript --vanilla tests-integration/deferred.R "$@"; echo "::endgroup::"; } +for scenario in select retry badinput unload preloaded nonascii fallback laterload hosted decoy; do + run "$scenario" +done +run hosted explicit +run jengine +run preloaded global +run laterload b +run alias link +run alias spelling +run renamed copy +run renamed hardlink +run renamed symlink +run hosted renamed +run replaced deleted +run replaced replaced +run hosted replaced + +echo "::group::JRI host" +Rscript --vanilla tests-integration/jri.R +RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R +echo "::endgroup::" diff --git a/tests-integration/xlsx.R b/tests-integration/xlsx.R new file mode 100644 index 0000000..a2b7eea --- /dev/null +++ b/tests-integration/xlsx.R @@ -0,0 +1,42 @@ +# A CRAN package that starts Java when its namespace loads: xlsx's .onLoad() +# calls rJava::.jpackage(), which runs .jinit(). Run in a fresh process: +# Rscript --vanilla tests-integration/xlsx.R +# RJE_RJAVA_LIB names the library with the deferred-loading rJava build. +home_a <- Sys.getenv("RJE_JAVA_HOME_A") +home_b <- Sys.getenv("RJE_JAVA_HOME_B") +stopifnot(dir.exists(home_a), dir.exists(home_b)) +# A binary rJava installed as a dependency would make this test meaningless. +rjava_lib <- normalizePath(Sys.getenv("RJE_RJAVA_LIB"), winslash = "/") +copies <- .libPaths()[file.exists(file.path(.libPaths(), "rJava", "DESCRIPTION"))] +cat("rJava installed in:", copies, sep = "\n ") +if (!length(copies) || !identical(normalizePath(copies[1], winslash = "/"), rjava_lib)) + stop("the first rJava on the library path is not the one in ", rjava_lib) + +windows <- .Platform$OS.type == "windows" +Sys.setenv(PATH = if (windows) paste(R.home("bin"), file.path(Sys.getenv("SystemRoot"), "System32"), sep = ";") else "/usr/bin:/bin") +ns <- loadNamespace("rJava") +state <- function() get(".jvmState", ns)() +stopifnot(identical(state()$state, "not-loaded"), exists("RgetMappedJVM", ns)) +# JDK B, so that the build JDK (A in the current set) cannot be picked up by accident. +Sys.setenv(JAVA_HOME = home_b) +loadNamespace("xlsx") +stopifnot(state()$initialized) +actual <- normalizePath(rJava::.jcall("java/lang/System", "S", "getProperty", "java.home"), winslash = "/") +expected <- normalizePath(home_b, winslash = "/") +if (!identical(actual, expected) && !identical(dirname(actual), expected)) + stop("java.home is ", actual, ", expected ", expected) +cat("xlsx started", rJava::.jcall("java/lang/System", "S", "getProperty", "java.version"), "from", actual, "\n") + +# Java was selected when xlsx loaded: another runtime cannot be selected later. +other <- get(".find.jvm.library", ns)(home_a)[1] +result <- try(rJava::.jloadJVM(other), silent = TRUE) +stopifnot(inherits(result, "try-error")) +cat("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L), "\n") + +file <- tempfile(fileext = ".xlsx") +data <- data.frame(id = 1:150, value = iris$Sepal.Length, species = as.character(iris$Species)) +xlsx::write.xlsx(data, file, row.names = FALSE) +back <- xlsx::read.xlsx(file, 1, stringsAsFactors = FALSE) +stopifnot(identical(dim(back), dim(data)), all.equal(back$value, data$value), + identical(back$species, data$species)) +cat("xlsx round trip:", nrow(back), "rows\n") From eb5e3504344ba36b4751538a8c710629bd105862 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:12:23 +0200 Subject: [PATCH 45/63] test: fix new coverage fixtures for Alpine, R 3.6 and xlsx --- .github/workflows/dynamic.yaml | 6 ++++-- tests-integration/musl.sh | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index ce0d8eb..4e0c2c4 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -23,7 +23,7 @@ jobs: - { set: vendor, a-dist: corretto, a-version: '11', b-dist: zulu, b-version: '25' } - { set: r45, os: windows-latest, r: '4.5', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } # The oldest R that DESCRIPTION accepts. - - { set: r36, os: ubuntu-latest, r: '3.6', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } + - { set: r36, os: ubuntu-24.04, r: '3.6', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } runs-on: ${{ matrix.os }} timeout-minutes: 45 env: @@ -232,7 +232,9 @@ jobs: Rscript -e 'lib <- file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-lib") install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) stopifnot(!dir.exists(file.path(lib, "rJava"))) - download.packages("xlsx", file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-src"), type = "source")' + src <- download.packages("xlsx", file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-src"), type = "source")[, 2] + install.packages(src, lib = lib, repos = NULL, type = "source") + stopifnot(dir.exists(file.path(lib, "xlsx")))' { echo "RJE_RJAVA_LIB=$R_LIBS" echo "R_LIBS=$R_LIBS${{ runner.os == 'Windows' && ';' || ':' }}$RUNNER_TEMP/xlsx-lib" diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh index abe4a2e..bc13a78 100755 --- a/tests-integration/musl.sh +++ b/tests-integration/musl.sh @@ -34,7 +34,12 @@ uname -srm R --version | head -n 1 # The container root is normally overlayfs on the runner's kernel. stat -f -c 'JDK filesystem: %T' "$RJE_JAVA_HOME_A" -# deferred.R reduces PATH to /usr/bin:/bin, which must not provide a java. +# deferred.R reduces PATH to /usr/bin:/bin so that a system java cannot mask +# discovery. Alpine's java-common links /usr/bin/java to the default JVM; +# remove that link in this disposable container, as the hosted jobs do not +# have one either. +ls -l /usr/bin/java /usr/lib/jvm/default-jvm 2>&1 || true +rm -f /usr/bin/java if [[ -e /usr/bin/java || -e /bin/java ]]; then echo "::error::java on the reduced PATH would mask discovery"; exit 1; fi echo "::endgroup::" From 9e248ec4db20bb1e870d58b9cde787acf796c956 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:23:32 +0200 Subject: [PATCH 46/63] test: check musl linkage without ldd and the CRAN xlsx source package --- .github/workflows/dynamic.yaml | 4 +++- tests-integration/musl.sh | 5 +++-- tests-integration/xlsx.R | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 4e0c2c4..38fca6a 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -232,7 +232,9 @@ jobs: Rscript -e 'lib <- file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-lib") install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) stopifnot(!dir.exists(file.path(lib, "rJava"))) - src <- download.packages("xlsx", file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-src"), type = "source")[, 2] + # The source package from CRAN: the P3M Linux repositories serve binaries. + src <- download.packages("xlsx", file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-src"), + repos = "https://cloud.r-project.org", type = "source")[, 2] install.packages(src, lib = lib, repos = NULL, type = "source") stopifnot(dir.exists(file.path(lib, "xlsx")))' { diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh index bc13a78..eb06dfe 100755 --- a/tests-integration/musl.sh +++ b/tests-integration/musl.sh @@ -64,8 +64,9 @@ R CMD INSTALL --library=/work/lib /work/rJava_*.tar.gz export R_LIBS=/work/lib lib="$(Rscript --vanilla -e 'cat(system.file("libs", package="rJava"))')" -ldd "$lib/rJava.so" -if ldd "$lib/rJava.so" | grep -i libjvm; then echo "::error::rJava.so depends on libjvm"; exit 1; fi +# musl's ldd also resolves symbols, and R's symbols come from the executable. +readelf -d "$lib/rJava.so" | grep NEEDED +if readelf -d "$lib/rJava.so" | grep NEEDED | grep -i libjvm; then echo "::error::rJava.so depends on libjvm"; exit 1; fi Rscript --vanilla tests-integration/jni-abi.R "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" cp tests-integration/jvmhost.c /work/jvmhost/ diff --git a/tests-integration/xlsx.R b/tests-integration/xlsx.R index a2b7eea..d7e121e 100644 --- a/tests-integration/xlsx.R +++ b/tests-integration/xlsx.R @@ -19,7 +19,7 @@ state <- function() get(".jvmState", ns)() stopifnot(identical(state()$state, "not-loaded"), exists("RgetMappedJVM", ns)) # JDK B, so that the build JDK (A in the current set) cannot be picked up by accident. Sys.setenv(JAVA_HOME = home_b) -loadNamespace("xlsx") +invisible(loadNamespace("xlsx")) stopifnot(state()$initialized) actual <- normalizePath(rJava::.jcall("java/lang/System", "S", "getProperty", "java.home"), winslash = "/") expected <- normalizePath(home_b, winslash = "/") From b414fd915aa69b4635a33090f304af8ee6691cdb Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:41:56 +0200 Subject: [PATCH 47/63] fix: refuse a JVM library that the JDK's libraries would not find by name On musl, the libjvm.so dependency of libjava, libjimage and the other JDK libraries is resolved through LD_LIBRARY_PATH as set at process start, not matched to the loaded library's SONAME as glibc does. Selecting a runtime other than the one on that path mapped a second JVM library while starting the VM. Check by name with RTLD_NOLOAD, before creating a VM. --- src/djni.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/djni.c b/src/djni.c index b69a121..29085f2 100644 --- a/src/djni.c +++ b/src/djni.c @@ -802,13 +802,43 @@ static int load_syms(void) { return 0; } +#ifdef __linux__ +/* The JDK's own libraries (libjava, libjimage, ...) depend on libjvm.so by + name, with only their own directory as run path. glibc matches that name + to the loaded JVM library by its SONAME. musl matches it only to a library + it found by searching: LD_LIBRARY_PATH as set when the process started, + then its system path. A JVM library selected by path is then not found, + or another file is, and starting a VM would map a second JVM library. + Resolving the name without loading anything shows what they will get. */ +static int check_jvm_name(const char *path) { + void *named = dlopen("libjvm.so", RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + const char *slash = strrchr(path, '/'); + int dir = slash ? (int) (slash - path) : 1; + if (named) dlclose(named); + dlerror(); + if (named == jni_dl) return 0; + snprintf(last_error, sizeof(last_error), + "The JDK's libraries load libjvm.so by name, which in this process does not resolve to %s. " + "With musl, put %.*s on LD_LIBRARY_PATH before R starts, e.g. with R_JAVA_LD_LIBRARY_PATH. Use a new R process.", + path, dir, slash ? path : "."); + return -4; +} +#endif + /* returns: 0 = success, -1 = already loaded, -2 = load error, -3 = symbols - not found, -4 = another JVM library is already mapped */ + not found, -4 = another JVM library is already mapped, or (Linux) the + JDK's libraries would not bind to the selected one */ int djni_load(const char *path) { int res; last_error[0] = 0; if (jni_dl) return -1; if ((res = load_library(path))) return res; +#ifdef __linux__ + if ((res = check_jvm_name(path))) { + djni_unload(); + return res; + } +#endif res = load_syms(); #ifdef _WIN32 if (!res) load_bundled_msvc(); From 8efb0f02188f8d36a63fb476b685fbe14103db47 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:41:57 +0200 Subject: [PATCH 48/63] test: run musl scenarios with their runtime on the startup library path --- .github/workflows/dynamic.yaml | 11 +------ tests-integration/deferred.R | 14 +++++++++ tests-integration/musl.sh | 54 +++++++++++++++++++++----------- tests-integration/xlsx-install.R | 13 ++++++++ 4 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 tests-integration/xlsx-install.R diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 38fca6a..73476f3 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -227,16 +227,7 @@ jobs: shell: bash run: | set -euo pipefail - mkdir -p "$RUNNER_TEMP/xlsx-lib" "$RUNNER_TEMP/xlsx-src" - # rJava is already on R_LIBS, so it is not installed as a dependency. - Rscript -e 'lib <- file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-lib") - install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) - stopifnot(!dir.exists(file.path(lib, "rJava"))) - # The source package from CRAN: the P3M Linux repositories serve binaries. - src <- download.packages("xlsx", file.path(Sys.getenv("RUNNER_TEMP"), "xlsx-src"), - repos = "https://cloud.r-project.org", type = "source")[, 2] - install.packages(src, lib = lib, repos = NULL, type = "source") - stopifnot(dir.exists(file.path(lib, "xlsx")))' + Rscript tests-integration/xlsx-install.R "$RUNNER_TEMP/xlsx-lib" "$RUNNER_TEMP/xlsx-src" { echo "RJE_RJAVA_LIB=$R_LIBS" echo "R_LIBS=$R_LIBS${{ runner.os == 'Windows' && ';' || ':' }}$RUNNER_TEMP/xlsx-lib" diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index b1a2b9c..6796237 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -343,6 +343,20 @@ switch(scenario, Sys.setenv(JAVA_HOME = home_b) start(home_b) }, + byname = { + # musl resolves the libjvm.so dependency of the JDK's libraries through + # LD_LIBRARY_PATH as set at process start, which the job points at A's. + # Selecting B is refused before a VM exists, so no second JVM library is + # mapped; musl never unmaps B's, which then blocks selecting A. + stopifnot(Sys.info()[["sysname"]] == "Linux") + Sys.setenv(JAVA_HOME = home_b) + fails(rJava::.jinit(), "libjvm.so by name") + maps <- grep("/libjvm[.]so$", readLines("/proc/self/maps"), value = TRUE) + maps <- unique(sub("^\\S+\\s+\\S+\\s+\\S+\\s+\\S+\\s+\\S+\\s+", "", maps)) + log("mapped JVM files:", maps) + stopifnot(length(maps) <= 1L, !lib(home_a) %in% maps) + if (length(maps)) fails(internal(".jloadJVM")(lib(home_a)), "already mapped") + }, nonascii = { # No preload: the loader itself must open a path with spaces and non-ASCII names. Sys.setenv(JAVA_HOME = link_home(home_b)) diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh index eb06dfe..9494871 100755 --- a/tests-integration/musl.sh +++ b/tests-integration/musl.sh @@ -73,24 +73,42 @@ cp tests-integration/jvmhost.c /work/jvmhost/ (cd /work/jvmhost && PKG_CPPFLAGS="-I/work/rJava/src" R CMD SHLIB jvmhost.c) export RJE_JVMHOST=/work/jvmhost/jvmhost.so -# The scenarios of dynamic.yaml that apply to Linux. -run() { echo "::group::$*"; Rscript --vanilla tests-integration/deferred.R "$@"; echo "::endgroup::"; } -for scenario in select retry badinput unload preloaded nonascii fallback laterload hosted decoy; do - run "$scenario" -done -run hosted explicit -run jengine -run preloaded global -run laterload b -run alias link -run alias spelling -run renamed copy -run renamed hardlink -run renamed symlink -run hosted renamed -run replaced deleted -run replaced replaced -run hosted replaced +# musl resolves the JDK libraries' dependency on libjvm.so through +# LD_LIBRARY_PATH as set at process start, not by matching the loaded JVM +# library's SONAME as glibc does. R's etc/ldpaths puts R_JAVA_LD_LIBRARY_PATH +# (default: the server directory of the R CMD javareconf JDK) on it. +echo "::group::Startup library path" +grep -n JAVA /usr/lib/R/etc/ldpaths +Rscript --vanilla -e 'cat("LD_LIBRARY_PATH:", Sys.getenv("LD_LIBRARY_PATH"), "\n")' +echo "::endgroup::" +server() { dirname "$(find "$1/" -name libjvm.so -path '*/server/*' -print -quit)"; } +server_a="$(server "$RJE_JAVA_HOME_A")" +server_b="$(server "$RJE_JAVA_HOME_B")" +# The scenarios of dynamic.yaml that apply to Linux, each with the server +# directory of the runtime it starts (A or B) on the startup library path. +run() { + local jdk="$1"; shift + local dir="$server_a"; [[ "$jdk" == B ]] && dir="$server_b" + echo "::group::$* ($jdk on LD_LIBRARY_PATH)" + R_JAVA_LD_LIBRARY_PATH="$dir" Rscript --vanilla tests-integration/deferred.R "$@" + echo "::endgroup::" +} +for scenario in select retry badinput nonascii fallback decoy; do run B "$scenario"; done +for scenario in unload preloaded laterload hosted jengine; do run A "$scenario"; done +run A hosted explicit +run A preloaded global +run B laterload b +run A alias link +run A alias spelling +run B renamed copy +run A renamed hardlink +run A renamed symlink +run A hosted renamed +run A replaced deleted +run A replaced replaced +run A hosted replaced +# The library path names A's runtime, B is selected: refused before a VM exists. +run A byname echo "::group::JRI host" Rscript --vanilla tests-integration/jri.R diff --git a/tests-integration/xlsx-install.R b/tests-integration/xlsx-install.R new file mode 100644 index 0000000..c36608d --- /dev/null +++ b/tests-integration/xlsx-install.R @@ -0,0 +1,13 @@ +# Installs the CRAN xlsx package and its test dependencies into a separate +# library, leaving the deferred-loading rJava on R_LIBS as the only rJava: +# Rscript tests-integration/xlsx-install.R +args <- commandArgs(TRUE) +lib <- args[1] +dir.create(lib, recursive = TRUE, showWarnings = FALSE) +dir.create(args[2], recursive = TRUE, showWarnings = FALSE) +# rJava is already installed, so it is not installed as a dependency. +install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) +# The source package from CRAN: the P3M Linux repositories serve binaries. +src <- download.packages("xlsx", args[2], repos = "https://cloud.r-project.org", type = "source")[, 2] +install.packages(src, lib = lib, repos = NULL, type = "source") +stopifnot(dir.exists(file.path(lib, "xlsx")), !dir.exists(file.path(lib, "rJava"))) From 4b29da381bede621f77879bc0368aaf375e0d6d1 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:52:15 +0200 Subject: [PATCH 49/63] fix: report a library that is no JVM before checking the JVM library name --- src/djni.c | 8 +++----- tests-integration/xlsx-install.R | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/djni.c b/src/djni.c index 29085f2..3ac35da 100644 --- a/src/djni.c +++ b/src/djni.c @@ -833,13 +833,11 @@ int djni_load(const char *path) { last_error[0] = 0; if (jni_dl) return -1; if ((res = load_library(path))) return res; + res = load_syms(); #ifdef __linux__ - if ((res = check_jvm_name(path))) { - djni_unload(); - return res; - } + /* After load_syms(), so that a library that is no JVM is reported as such. */ + if (!res && (res = check_jvm_name(path))) djni_unload(); #endif - res = load_syms(); #ifdef _WIN32 if (!res) load_bundled_msvc(); #endif diff --git a/tests-integration/xlsx-install.R b/tests-integration/xlsx-install.R index c36608d..b6f743b 100644 --- a/tests-integration/xlsx-install.R +++ b/tests-integration/xlsx-install.R @@ -5,6 +5,9 @@ args <- commandArgs(TRUE) lib <- args[1] dir.create(lib, recursive = TRUE, showWarnings = FALSE) dir.create(args[2], recursive = TRUE, showWarnings = FALSE) +# Under Git bash, R on Windows does not read the profile setup-r writes. +if (is.null(getOption("repos")) || identical(unname(getOption("repos")["CRAN"]), "@CRAN@")) + options(repos = c(CRAN = "https://cloud.r-project.org")) # rJava is already installed, so it is not installed as a dependency. install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) # The source package from CRAN: the P3M Linux repositories serve binaries. From a87157de7da181071be64f456363e71ebdbc7729 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 19:53:27 +0200 Subject: [PATCH 50/63] docs: describe the Linux JVM library name check and the expanded matrix --- tests-integration/README.md | 70 +++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/tests-integration/README.md b/tests-integration/README.md index 45e0224..3f3ad6f 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -83,6 +83,37 @@ not apply the `jvm.dll` name rule, which concerns starting a VM (untested: the JDK cannot start a VM from a renamed `jvm.dll`). The native attach entry point has no exported R wrapper. +## The JVM library name on Linux + +The JDK's own libraries (`libjava`, `libjimage`, `libzip`, ...) depend on +`libjvm.so` by name, with only their own directory (`$ORIGIN`) as run path, +not `server`. glibc matches that name to the loaded JVM library by its +SONAME. musl does not: it matches only a library it found by searching, and +searches `LD_LIBRARY_PATH` as set when the process started, then its system +path. With a JVM library selected by path, the name is then not found, or +names another runtime. R's `etc/ldpaths` puts `R_JAVA_LD_LIBRARY_PATH` on +`LD_LIBRARY_PATH`, by default `lib/server` of `JAVA_HOME` at R startup or of +the `R CMD javareconf` JDK. On Alpine, with JDK 17 configured, `.jinit()` +with `JAVA_HOME` naming another runtime failed while the VM loaded the JDK's +libraries: `libjava.so: JVM_DoPrivileged: symbol not found` with OpenJDK 11, +consistent with binding to JDK 17's `libjvm.so`, which lacks that symbol, +and `Unable to load jimage library` with OpenJDK 21. + +So on Linux, after loading the library and its JNI entry points, djni +resolves `libjvm.so` with `RTLD_NOLOAD`, which loads nothing, and refuses the +selection unless the name resolves to the selected library. The refusal +happens before a VM is created and leaves the state `"not-loaded"`, but musl +never unmaps a library, so it remains mapped and blocks other selections: use +a new R process started with `JAVA_HOME` naming the runtime, or +`R_JAVA_LD_LIBRARY_PATH` naming its server directory (needed for the JDK 8 +layout). On glibc the name resolves through the SONAME, so the check passes +in every glibc scenario above, including renamed and replaced names. +Changing `LD_LIBRARY_PATH` in a running process, as `deferred.R` does, does +not change musl's search path. A Java application +embedding R through JRI is not affected: for this reason the JDK launcher +built for musl sets `LD_LIBRARY_PATH` and re-executes itself +(`RequiresSetenv` in OpenJDK's `java_md.c`), and attaching checks no names. + ## Selected libraries are not unloaded `.junloadJVM()` errors whenever a JVM library is loaded, even before a VM was @@ -215,6 +246,7 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `hosted replaced` | the same replacement after a host created the VM (Windows: renamed in a copy of A's home, as above): `.jloadJVM()` rejects B, the replaced name and the name `RgetMappedJVM` reports, all before `.jinit()` attaches to A | | `decoy` | the JDK's `jli` library (JNI exports on macOS, no `JVM_` exports) is not reported and does not block selecting B | | `nonascii` | loading through such a path without a preload | +| `byname` | musl, with A's server directory on the startup library path: selecting B is refused before a VM exists, A's library is not mapped, and the B library that musl keeps mapped then blocks A | | `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | | `laterload` | the later native loads above | | `hosted`, `hosted explicit` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK; `.jloadJVM()` rejects B, and with `explicit` selects A's own file first | @@ -224,9 +256,12 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `msvc.R` | Windows normal/missing/2015 system runtimes, retained references, fresh startup and hosted late loads; loaded runtime origins | | `overlay.R` | actual 5.15, 6.1.0 and 6.8 kernels; four overlay layouts, unprivileged processes, exact refusal classes | | downstream | rJavaEnv and two minimal consumer packages (current pair only) | +| `xlsx.R`, xlsx check | the CRAN package xlsx, whose `.onLoad()` starts Java through `.jpackage()`: loading it selects `JAVA_HOME` (JDK B, not the build JDK on `PATH`), another runtime is then rejected, and a workbook round trip works; `R CMD check` of the CRAN source package runs its testthat suite (current pair only) | +| `musl.sh` | the Linux scenarios above, `byname` and `jri.R` on Alpine, each scenario with the server directory of the runtime it starts on the startup library path | -Bounded matrix, each on Linux, macOS (arm64) and Windows with R release; the -build JDK is Temurin 17: +Bounded matrix, each on Linux (x86_64 and arm64), macOS (arm64) and Windows +with R release; the build JDK is Temurin 17. On Linux arm64, JDK 8 has the +`jre/lib/aarch64/server` layout: | Set | JDK A | JDK B | |---|---|---| @@ -235,7 +270,21 @@ build JDK is Temurin 17: | vendor | Corretto 11 | Zulu 25 | An additional Windows R 4.5 job runs the same native, JRI and VC++ checks -with Temurin 17/21, including the console declarations for R before 4.6. +with Temurin 17/21, including the console declarations for R before 4.6. An +Ubuntu 24.04 job runs the native and JRI checks with R 3.6.3, the oldest R +that `DESCRIPTION` accepts, and Temurin 17/21. + +The musl jobs run `musl.sh` in Docker from a pinned `alpine:3.24.2` image: +musl 1.2.6, Alpine's R 4.6.0 and its OpenJDK packages. Their libraries must +link `libc.musl`, not a glibc compatibility layer. The JDKs are installed in +the container's overlayfs root on the runner's current kernel, as in a +typical Docker deployment: + +| Set | Machine | JDK A | JDK B | +|---|---|---|---| +| legacy | x86_64 | OpenJDK 8 | OpenJDK 11 | +| current | x86_64 | OpenJDK 17 | OpenJDK 21 | +| current | aarch64 | OpenJDK 17 | OpenJDK 21 | The upstream `check.yaml` matrix (R devel, Zulu 8 and 11, five runner images including Intel macOS) also runs on this branch. @@ -269,10 +318,17 @@ from the repository root, one process per scenario. reuse on the current Ubuntu 5.15 and current-kernel guests. dyld records a library loaded through a symbolic link by its target, so on macOS the `symlink` variant shows the target name. -- Not covered: 32-bit Windows, Windows on ARM, Linux on ARM, musl, OpenJ9 and - other non-HotSpot VMs, Java runtimes without a JDK layout, R versions other - than current release, Windows 4.5 and devel in `check.yaml`, and downstream packages - beyond the two test consumers. +- On musl, a process can start only the runtime whose server directory was on + `LD_LIBRARY_PATH` when it started (above). Other runtimes are refused, not + supported. +- Not covered: 32-bit Windows; Windows on ARM (the linkage, JRI and VC++ + fixtures assume x64, and the old VC++ runtime test downloads x64 DLLs); + OpenJ9 and other non-HotSpot VMs, which the export-based identification + above does not describe; Java runtimes without a JDK layout; musl JDKs + other than Alpine's packages (e.g. Temurin's Alpine builds), and JDK 8 on + musl arm64; Intel macOS in this matrix (only in `check.yaml`); R versions + other than release, Windows 4.5, Linux 3.6.3 and devel in `check.yaml`; + and downstream packages beyond rJavaEnv, the two test consumers and xlsx. Modifying a mapped JVM library in place without changing its device/inode is outside the identity guarantee, as for other native libraries. Replace runtime From a1ba1cef7f38ebd47479980538525db28bd07e23 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:01:42 +0200 Subject: [PATCH 51/63] test: do not run the replaced-file scenarios on musl, and document why --- tests-integration/README.md | 13 ++++++++++++- tests-integration/musl.sh | 6 +++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests-integration/README.md b/tests-integration/README.md index 3f3ad6f..c533086 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -114,6 +114,16 @@ embedding R through JRI is not affected: for this reason the JDK launcher built for musl sets `LD_LIBRARY_PATH` and re-executes itself (`RequiresSetenv` in OpenJDK's `java_md.c`), and attaching checks no names. +musl also resolves `dlopen(name, RTLD_NOLOAD)` for a path by opening that +file. djni references a mapped image, and reads the exports of one whose name +no JVM uses, through that call. So on musl a JVM library mapped under such a +name is no longer identified as a JVM once that file is deleted or replaced. +In `replaced deleted` on Alpine, `.jloadJVM()` of B then mapped B's library, +and only the name check refused it, before any VM was created. The +`replaced` and `hosted replaced` scenarios are therefore not run on musl. A +library under the name `libjvm.so` is identified by that name; deleting or +replacing it is not exercised on musl. + ## Selected libraries are not unloaded `.junloadJVM()` errors whenever a JVM library is loaded, even before a VM was @@ -320,7 +330,8 @@ from the repository root, one process per scenario. macOS the `symlink` variant shows the target name. - On musl, a process can start only the runtime whose server directory was on `LD_LIBRARY_PATH` when it started (above). Other runtimes are refused, not - supported. + supported. A JVM library mapped under a name no JVM uses is not identified + on musl once its file is deleted or replaced. - Not covered: 32-bit Windows; Windows on ARM (the linkage, JRI and VC++ fixtures assume x64, and the old VC++ runtime test downloads x64 DLLs); OpenJ9 and other non-HotSpot VMs, which the export-based identification diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh index 9494871..c5308e8 100755 --- a/tests-integration/musl.sh +++ b/tests-integration/musl.sh @@ -104,9 +104,9 @@ run B renamed copy run A renamed hardlink run A renamed symlink run A hosted renamed -run A replaced deleted -run A replaced replaced -run A hosted replaced +# Not replaced deleted, replaced replaced or hosted replaced: musl references +# a mapped image by reopening its recorded name, so a JVM library under a name +# no JVM uses is not identified once that file is deleted or replaced (README). # The library path names A's runtime, B is selected: refused before a VM exists. run A byname From acb1639180e53464b48583470e7a85b9a0d063ab Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:10:01 +0200 Subject: [PATCH 52/63] test: report the error and state when the JRI host test selects JAVA_HOME --- tests-integration/JriAttach.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests-integration/JriAttach.java b/tests-integration/JriAttach.java index 4187798..e23ecd0 100644 --- a/tests-integration/JriAttach.java +++ b/tests-integration/JriAttach.java @@ -23,7 +23,9 @@ static void run() { REXP result = engine.eval("tryCatch({" + " library(rJava);" + " b <- try(.jloadJVM(rJava:::.find.jvm.library(Sys.getenv('JAVA_HOME'))[1]), silent = TRUE);" + - " if (!inherits(b, 'try-error') || .jvmState()$state != 'not-loaded') stop('JAVA_HOME runtime was selected');" + + " if (!inherits(b, 'try-error') || .jvmState()$state != 'not-loaded')" + + " stop('JAVA_HOME runtime was selected: ', if (inherits(b, 'try-error')) trimws(b) else 'loaded'," + + " '; state ', .jvmState()$state, '; mapped ', paste(unlist(.Call(rJava:::RgetMappedJVM)), collapse = ' '));" + " .jinit();" + " paste('OK', .jvmState()$state, .jcall('java/lang/System', 'S', 'getProperty', 'java.home')," + " !is.jnull(.jengine()), sep = '|')" + From b8e89bd8fc48cdb80496f2e1808cb7c3c1d73c93 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:19:48 +0200 Subject: [PATCH 53/63] test: log the selected path and JVM mappings when the JRI host test fails --- tests-integration/JriAttach.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests-integration/JriAttach.java b/tests-integration/JriAttach.java index e23ecd0..70bd435 100644 --- a/tests-integration/JriAttach.java +++ b/tests-integration/JriAttach.java @@ -22,10 +22,13 @@ static void run() { /* An explicit path must name the mapped file: JAVA_HOME's runtime is rejected. */ REXP result = engine.eval("tryCatch({" + " library(rJava);" + - " b <- try(.jloadJVM(rJava:::.find.jvm.library(Sys.getenv('JAVA_HOME'))[1]), silent = TRUE);" + + " p <- rJava:::.find.jvm.library(Sys.getenv('JAVA_HOME'))[1];" + + " b <- try(.jloadJVM(p), silent = TRUE);" + + " maps <- if (file.exists('/proc/self/maps')) unique(grep('libjvm', readLines('/proc/self/maps'), value = TRUE)) else '';" + " if (!inherits(b, 'try-error') || .jvmState()$state != 'not-loaded')" + - " stop('JAVA_HOME runtime was selected: ', if (inherits(b, 'try-error')) trimws(b) else 'loaded'," + - " '; state ', .jvmState()$state, '; mapped ', paste(unlist(.Call(rJava:::RgetMappedJVM)), collapse = ' '));" + + " stop('JAVA_HOME runtime was selected: ', p, ': ', if (inherits(b, 'try-error')) trimws(b) else 'loaded'," + + " '; state ', .jvmState()$state, '; mapped ', paste(unlist(.Call(rJava:::RgetMappedJVM)), collapse = ' ')," + + " '; maps ', paste(maps, collapse = ' | '));" + " .jinit();" + " paste('OK', .jvmState()$state, .jcall('java/lang/System', 'S', 'getProperty', 'java.home')," + " !is.jnull(.jengine()), sep = '|')" + From 91bbbfde68dcf2777b53a8a54cfb7c7356b32c78 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:25:57 +0200 Subject: [PATCH 54/63] test: swap the JRI host runtimes with env, as dynamic.yaml does --- tests-integration/musl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh index c5308e8..2ee4bbf 100755 --- a/tests-integration/musl.sh +++ b/tests-integration/musl.sh @@ -112,5 +112,5 @@ run A byname echo "::group::JRI host" Rscript --vanilla tests-integration/jri.R -RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R +env RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R echo "::endgroup::" From 6e3a4a6a3e5a758c875ea79fbb93de848be5047b Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:52:09 +0200 Subject: [PATCH 55/63] test: restore musl deleted and replaced alias coverage with either startup path Run replaced deleted and replaced replaced with A's and with B's server directory on the startup library path, and hosted replaced with A's. After each refusal, check that nothing from B's home was mapped. --- tests-integration/deferred.R | 47 +++++++++++++++++++++++++++++------- tests-integration/musl.sh | 15 +++++++++--- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 6796237..1fb887f 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -1,12 +1,15 @@ # Each scenario must run in a fresh R process against the installed source build: # Rscript --vanilla tests-integration/deferred.R [variant] # RJE_JAVA_HOME_A and RJE_JAVA_HOME_B must name two different Java homes. +# RJE_BYNAME (musl only) is A or B: the runtime whose server directory was on +# LD_LIBRARY_PATH when R started, the libjvm.so the JDK's libraries bind to. args <- commandArgs(TRUE) scenario <- args[1] variant <- if (length(args) > 1L) args[2] else "" windows <- .Platform$OS.type == "windows" home_a <- Sys.getenv("RJE_JAVA_HOME_A") home_b <- Sys.getenv("RJE_JAVA_HOME_B") +byname <- Sys.getenv("RJE_BYNAME") stopifnot(dir.exists(home_a), dir.exists(home_b)) log <- function(...) cat(scenario, variant, ":", ..., "\n") @@ -49,6 +52,16 @@ fails <- function(expr, pattern = NULL) { stop("unexpected error: ", result) log("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L)) } +# Linux: a refused selection must not have mapped anything from that home, +# not even a JVM library that is never started. +unmapped <- function(home) { + if (Sys.info()[["sysname"]] != "Linux") return(invisible()) + maps <- grep("^(\\S+\\s+){5}/", readLines("/proc/self/maps"), value = TRUE) + files <- unique(sub("^(\\S+\\s+){5}", "", maps)) + hit <- files[startsWith(files, paste0(normalizePath(home), "/"))] + if (length(hit)) stop("mapped from ", home, ": ", paste(hit, collapse = ", ")) + log("nothing mapped from", home) +} # Map a JVM library as other native code in the process would, outside rJava. preload <- function(path, local = TRUE, bin = dirname(dirname(path))) { old <- Sys.getenv("PATH") @@ -316,19 +329,31 @@ switch(scenario, stopifnot(file.link(lib(home_b), path)) log("replaced", path, "by a hard link of", lib(home_b)) } + # musl takes a reference to a mapped image by opening its recorded name + # again, which no longer opens A's library: the image must still be + # identified as A's JVM before B is mapped. fails(internal(".jloadJVM")(lib(home_b)), "already mapped") fails(internal(".jloadJVM")(path), "already mapped") Sys.setenv(JAVA_HOME = home_b) fails(rJava::.jinit(), "already mapped") - # A's library is the mapped file, whatever is now at the mapped name. - internal(".jloadJVM")(lib(home_a)) - stopifnot(state()$state == "none") - expect_mapped(path, 0L) - # The JVM finds its home from its own file name, which no longer exists - # if deleted. - if (variant == "replaced") { - start(home_a) - expect_mapped(path, 1L) + unmapped(home_b) + if (byname == "B") { + # musl would bind the JDK's libraries to B's libjvm.so: A is refused + # too, before a VM exists, and B is still not mapped. + fails(internal(".jloadJVM")(lib(home_a)), "libjvm.so by name") + unmapped(home_b) + expect_mapped(path, 0L) + } else { + # A's library is the mapped file, whatever is now at the mapped name. + internal(".jloadJVM")(lib(home_a)) + stopifnot(state()$state == "none") + expect_mapped(path, 0L) + # The JVM finds its home from its own file name, which no longer + # exists if deleted. + if (variant == "replaced") { + start(home_a) + expect_mapped(path, 1L) + } } } }, @@ -424,6 +449,9 @@ switch(scenario, variant <- "laterload" } stopifnot(variant %in% c("", "explicit", "renamed", "replaced", "laterload"), !(windows && variant == "renamed")) + # With B's libjvm.so on musl's startup path, the host itself would bind + # A's JDK libraries to B's library, mapping two runtimes before rJava runs. + stopifnot(byname != "B") helper <- Sys.getenv("RJE_JVMHOST") stopifnot(file.exists(helper)) dyn.load(helper) @@ -459,6 +487,7 @@ switch(scenario, fails(rJava::.jloadJVM(recorded), "already mapped") fails(rJava::.jloadJVM(path), "already mapped") } + unmapped(home_b) if (variant == "explicit") { stopifnot(isTRUE(rJava::.jloadJVM(lib(home_a))), state()$state == "detached") } diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh index 2ee4bbf..b658067 100755 --- a/tests-integration/musl.sh +++ b/tests-integration/musl.sh @@ -90,7 +90,7 @@ run() { local jdk="$1"; shift local dir="$server_a"; [[ "$jdk" == B ]] && dir="$server_b" echo "::group::$* ($jdk on LD_LIBRARY_PATH)" - R_JAVA_LD_LIBRARY_PATH="$dir" Rscript --vanilla tests-integration/deferred.R "$@" + R_JAVA_LD_LIBRARY_PATH="$dir" RJE_BYNAME="$jdk" Rscript --vanilla tests-integration/deferred.R "$@" echo "::endgroup::" } for scenario in select retry badinput nonascii fallback decoy; do run B "$scenario"; done @@ -104,9 +104,16 @@ run B renamed copy run A renamed hardlink run A renamed symlink run A hosted renamed -# Not replaced deleted, replaced replaced or hosted replaced: musl references -# a mapped image by reopening its recorded name, so a JVM library under a name -# no JVM uses is not identified once that file is deleted or replaced (README). +# A is mapped under a name that then no longer opens it, so musl cannot take +# a reference by that name. With either runtime on the startup library path, +# B must be refused before it is mapped; A is accepted only when the JDK's +# libraries would bind to it. hosted replaced runs with A only: with B on the +# path, the host's own VM would bind A's JDK libraries to B's libjvm.so. +for jdk in A B; do + run "$jdk" replaced deleted + run "$jdk" replaced replaced +done +run A hosted replaced # The library path names A's runtime, B is selected: refused before a VM exists. run A byname From f44392026e6912f6be1167fca7c150fb4d56a7f3 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:57:55 +0200 Subject: [PATCH 56/63] fix: identify mapped JVM images on Linux from their exports in memory musl takes a handle to a mapped image only by opening its recorded name again, so a JVM library mapped under a name no JVM uses was dropped from discovery once that file was deleted or replaced, and selecting another runtime then mapped it next to the first (reproduced on Alpine: with B's server directory on the startup path, .jloadJVM(B) succeeded). Read JVM_CurrentTimeMillis, JNI_CreateJavaVM and JNI_GetCreatedJavaVMs from the image's dynamic symbol table in memory instead, bounded by its readable segments; an unreadable table fails discovery. Take references only through names that resolve to the image itself, checked with dlinfo(RTLD_DI_LINKMAP): the recorded name, libjvm.so, or for a selection the path naming the mapped file; refuse selection or attachment without one. Also drop trailing whitespace in djni.c and djni.h. --- src/djni.c | 231 +++++++++++++++++++++++++++++++++++++++++------------ src/djni.h | 2 +- 2 files changed, 182 insertions(+), 51 deletions(-) diff --git a/src/djni.c b/src/djni.c index 3ac35da..055dcbf 100644 --- a/src/djni.c +++ b/src/djni.c @@ -532,14 +532,18 @@ static int is_jvm_name(const char *name) { } /* An image mapped into the process: its name as the dynamic loader records it, and where it is mapped, to tell its own definitions from those of its - dependencies. */ + dependencies (macOS) or to read its dynamic symbol table (Linux). */ struct image { char *name; #ifdef __APPLE__ const void *header; #else size_t nseg; - uintptr_t *seg; /* start and end of each PT_LOAD segment */ + uintptr_t *seg; /* start and end of each readable PT_LOAD segment */ +#ifdef __linux__ + uintptr_t base; + const ElfW(Dyn) *dyn; +#endif #endif }; struct images { struct image *v; size_t n, size; int failed; }; @@ -566,11 +570,16 @@ static int phdr_image(struct dl_phdr_info *info, size_t size, void *data) { size_t i, n = 0; if (!info->dlpi_name || !*info->dlpi_name) return 0; /* the executable */ if (!(im = add_image(l, info->dlpi_name))) return 1; - for (i = 0; i < info->dlpi_phnum; i++) if (info->dlpi_phdr[i].p_type == PT_LOAD) n++; + im->base = (uintptr_t) info->dlpi_addr; + for (i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; + if (ph->p_type == PT_LOAD && (ph->p_flags & PF_R)) n++; + if (ph->p_type == PT_DYNAMIC) im->dyn = (const ElfW(Dyn) *) (im->base + (uintptr_t) ph->p_vaddr); + } if (n && !(im->seg = malloc(2 * n * sizeof(uintptr_t)))) { l->failed = 1; return 1; } for (i = 0; i < info->dlpi_phnum; i++) - if (info->dlpi_phdr[i].p_type == PT_LOAD) { - uintptr_t start = (uintptr_t) info->dlpi_addr + (uintptr_t) info->dlpi_phdr[i].p_vaddr; + if (info->dlpi_phdr[i].p_type == PT_LOAD && (info->dlpi_phdr[i].p_flags & PF_R)) { + uintptr_t start = im->base + (uintptr_t) info->dlpi_phdr[i].p_vaddr; im->seg[2 * im->nseg] = start; im->seg[2 * im->nseg++ + 1] = start + (uintptr_t) info->dlpi_phdr[i].p_memsz; } @@ -604,35 +613,132 @@ static int list_images(struct images *l) { #endif return l->failed ? -1 : 0; } -#ifdef RTLD_NOLOAD +#ifdef __APPLE__ /* Whether the image defines sym itself: dlsym() on its handle also finds definitions it inherits from the libraries it depends on. */ static int owns(void *handle, const struct image *im, const char *sym) { void *p = dlsym(handle, sym); - if (!p) return 0; -#ifdef __APPLE__ - { - Dl_info info; - return dladdr(p, &info) && info.dli_fbase == im->header; + Dl_info info; + return p && dladdr(p, &info) && info.dli_fbase == im->header; +} +#elif defined(__linux__) +/* Whether [a, a + len) lies in one readable segment of the image. */ +static int in_image(const struct image *im, uintptr_t a, size_t len) { + size_t i; + for (i = 0; i < im->nseg; i++) + if (a >= im->seg[2 * i] && a < im->seg[2 * i + 1] && len <= im->seg[2 * i + 1] - a) return 1; + return 0; +} +/* glibc relocates the addresses of a dynamic section in place on most + targets, musl never does: an address is used only if it is in the image. */ +static uintptr_t dyn_addr(const struct image *im, uintptr_t v) { + if (in_image(im, v, 1)) return v; + if (in_image(im, im->base + v, 1)) return im->base + v; + return 0; +} +/* The number of entries in the dynamic symbol table, from its hash table as + musl counts them (ldso/dynlink.c), or -1 if the table cannot be read. */ +static long count_syms(const struct image *im, const uint32_t *hash, const uint32_t *gnu) { + const uint32_t *buckets, *chain; + uint32_t i, max = 0; + if (!gnu) return in_image(im, (uintptr_t) hash, 2 * sizeof(*hash)) ? (long) hash[1] : -1; + if (!in_image(im, (uintptr_t) gnu, 4 * sizeof(*gnu))) return -1; + /* The bloom filter words have the size of an address. */ + buckets = (const uint32_t *) ((const ElfW(Addr) *) (gnu + 4) + gnu[2]); + if (!in_image(im, (uintptr_t) buckets, (size_t) gnu[0] * sizeof(*buckets))) return -1; + for (i = 0; i < gnu[0]; i++) if (buckets[i] > max) max = buckets[i]; + if (!max) return 0; + if (max < gnu[1]) return -1; + /* The chain of the last bucket ends with an odd hash value. */ + for (chain = buckets + gnu[0] + (max - gnu[1]); ; chain++, max++) { + if (!in_image(im, (uintptr_t) chain, sizeof(*chain))) return -1; + if (*chain & 1) return (long) max + 1; } -#else - { - uintptr_t a = (uintptr_t) p; - size_t i; - for (i = 0; i < im->nseg; i++) - if (a >= im->seg[2 * i] && a < im->seg[2 * i + 1]) return 1; - return 0; +} +/* Whether the image defines sym, read from its dynamic symbol table in + memory as the dynamic loader reads it. Unlike dlsym(), this needs no + handle: musl takes one by opening the recorded name again, which may since + name another file or none. Returns 1 if defined, setting *fn to its + address if it is a function, 0 if not, -1 if the table cannot be read. */ +static int image_symbol(const struct image *im, const char *sym, uintptr_t *fn) { + const ElfW(Dyn) *d = im->dyn; + const ElfW(Sym) *syms; + const char *strs; + const uint32_t *hash = NULL, *gnu = NULL; + uintptr_t symtab = 0, strtab = 0, strsz = 0; + size_t len = strlen(sym) + 1; + long i, n; + *fn = 0; + if (!d) return 0; /* no dynamic section, so no exports */ + for (;; d++) { + if (!in_image(im, (uintptr_t) d, sizeof(*d))) return -1; + if (d->d_tag == DT_NULL) break; + switch (d->d_tag) { + case DT_SYMTAB: symtab = dyn_addr(im, d->d_un.d_ptr); if (!symtab) return -1; break; + case DT_STRTAB: strtab = dyn_addr(im, d->d_un.d_ptr); if (!strtab) return -1; break; + case DT_STRSZ: strsz = d->d_un.d_val; break; + case DT_HASH: if (!(hash = (const uint32_t *) dyn_addr(im, d->d_un.d_ptr))) return -1; break; + case DT_GNU_HASH: if (!(gnu = (const uint32_t *) dyn_addr(im, d->d_un.d_ptr))) return -1; break; + } } -#endif + /* Without both tables and a hash table, the loader finds no symbol. */ + if (!symtab || !strtab || (!hash && !gnu)) return 0; + if (!in_image(im, strtab, strsz) || (n = count_syms(im, hash, gnu)) < 0 || + !in_image(im, symtab, (size_t) n * sizeof(*syms))) + return -1; + syms = (const ElfW(Sym) *) symtab; + strs = (const char *) strtab; + for (i = 0; i < n; i++) { + const ElfW(Sym) *s = &syms[i]; + if (s->st_shndx == SHN_UNDEF || s->st_name >= strsz || len > strsz - s->st_name || + memcmp(strs + s->st_name, sym, len)) + continue; + /* The low four bits of st_info are the type, in ELF32 and ELF64. */ + if ((s->st_info & 0xf) == STT_FUNC) *fn = im->base + (uintptr_t) s->st_value; + return 1; + } + return 0; } +/* A reference to the image by name, if the name opens it without loading + anything. musl matches a name with a slash by the file now there, which + may be another mapped library, so the handle must be the image's own. */ +static void *reference(const ElfW(Dyn) *dyn, const char *name) { + struct link_map *map; + void *handle = dlopen(name, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + if (handle && (dlinfo(handle, RTLD_DI_LINKMAP, &map) || (const void *) map->l_ld != (const void *) dyn)) { + dlclose(handle); + handle = NULL; + } + dlerror(); /* failed lookups are not errors of the caller */ + return handle; +} +#endif +/* A mapped JVM image: its recorded name, a reference to it if one could be + taken without loading anything, its JNI_GetCreatedJavaVMs, and an address + in it. */ +struct jvm_image { + char *name; + void *handle; + JNI_GetCreatedJavaVMs_fnptr created; + const void *addr; +#ifdef __linux__ + const ElfW(Dyn) *dyn; #endif -struct jvm_image { char *name; void *handle; int owns_created; const void *addr; }; -/* Lists the JVM images mapped into the process, each with a reference if - one can be taken without loading anything. An image is a JVM if it has a - JVM library name, even if malformed, or if it defines the JNI invocation +}; +static void free_jvms(struct jvm_image *v, int n) { + int i; + for (i = 0; i < n; i++) { + if (v[i].handle) dlclose(v[i].handle); + free(v[i].name); + } + free(v); +} +/* Lists the JVM images mapped into the process. An image is a JVM if it has + a JVM library name, even if malformed, or if it defines the JNI invocation exports and a JVM_ export, whatever its name. JNI exports alone do not suffice: the macOS libjli defines them to forward to libjvm. Returns the - number of JVM images, or -1 if the images cannot be listed. */ + number of JVM images, or -1 with last_error set if the images cannot be + listed, or (Linux) the exports of one cannot be read. */ static int find_jvms(struct jvm_image **out) { struct images l = { NULL, 0, 0, 0 }; struct jvm_image *v = NULL; @@ -640,28 +746,54 @@ static int find_jvms(struct jvm_image **out) { int n = 0; *out = NULL; if (list_images(&l) || (l.n && !(v = calloc(l.n, sizeof(*v))))) { + snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process"); free_images(&l); + free(v); return -1; } for (i = 0; i < l.n; i++) { + struct image *im = &l.v[i]; void *handle = NULL; - int jvm = is_jvm_name(l.v[i].name); -#ifdef RTLD_NOLOAD + int jvm = is_jvm_name(im->name); +#ifdef __APPLE__ /* RTLD_NOLOAD returns a reference only if the image is already mapped, and RTLD_LAZY leaves the relocation of other images unchanged. */ - handle = dlopen(l.v[i].name, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + handle = dlopen(im->name, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); if (handle && !jvm) - jvm = owns(handle, &l.v[i], "JVM_CurrentTimeMillis") && owns(handle, &l.v[i], "JNI_CreateJavaVM") && - owns(handle, &l.v[i], "JNI_GetCreatedJavaVMs"); - if (jvm) v[n].owns_created = handle && owns(handle, &l.v[i], "JNI_GetCreatedJavaVMs"); + jvm = owns(handle, im, "JVM_CurrentTimeMillis") && owns(handle, im, "JNI_CreateJavaVM") && + owns(handle, im, "JNI_GetCreatedJavaVMs"); + if (jvm && handle && owns(handle, im, "JNI_GetCreatedJavaVMs")) + *(void **) &v[n].created = dlsym(handle, "JNI_GetCreatedJavaVMs"); +#elif defined(__linux__) + /* Identified by its exports, not by a handle, which musl may be + unable to take; an unreadable table is not taken as no JVM. */ + uintptr_t fn; + int owned = image_symbol(im, "JVM_CurrentTimeMillis", &fn); + if (owned > 0) owned = image_symbol(im, "JNI_CreateJavaVM", &fn); + if (owned > 0 || jvm) { + int created = image_symbol(im, "JNI_GetCreatedJavaVMs", &fn); + if (owned > 0) owned = created; + if (created > 0) v[n].created = (JNI_GetCreatedJavaVMs_fnptr) fn; + } + if (owned < 0 && !jvm) { + snprintf(last_error, sizeof(last_error), "Cannot read the exports of %s, mapped into this process", im->name); + free_jvms(v, n); + free_images(&l); + return -1; + } + jvm = jvm || owned > 0; + /* The recorded name, or the name the JDK's libraries bind to. */ + if (jvm && !(handle = reference(im->dyn, im->name))) + handle = reference(im->dyn, "libjvm.so"); + v[n].dyn = im->dyn; #endif if (jvm) { - v[n].name = l.v[i].name; - l.v[i].name = NULL; + v[n].name = im->name; + im->name = NULL; #ifdef __APPLE__ - v[n].addr = l.v[i].header; + v[n].addr = im->header; #else - v[n].addr = l.v[i].nseg ? (const void *) l.v[i].seg[0] : NULL; + v[n].addr = im->nseg ? (const void *) im->seg[0] : NULL; #endif v[n++].handle = handle; } else if (handle) @@ -672,28 +804,21 @@ static int find_jvms(struct jvm_image **out) { *out = v; return n; } -static void free_jvms(struct jvm_image *v, int n) { - int i; - for (i = 0; i < n; i++) { - if (v[i].handle) dlclose(v[i].handle); - free(v[i].name); - } - free(v); -} /* The number of VMs created by a JVM image, or -1 if unknown. */ static int created_vms(const struct jvm_image *im) { - JNI_GetCreatedJavaVMs_fnptr created; JavaVM *vm; jsize count = 0; - if (!im->owns_created) return -1; - *(void **) &created = dlsym(im->handle, "JNI_GetCreatedJavaVMs"); - return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; + return im->created && im->created(&vm, 1, &count) == JNI_OK ? (int) count : -1; +} +static void use_new_process(void) { + size_t n = strlen(last_error); + snprintf(last_error + n, sizeof(last_error) - n, ". Use a new R process."); } static int load_library(const char *path) { struct jvm_image *jvms; int n = find_jvms(&jvms), res = -4; if (n < 0) { - snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process. Use a new R process."); + use_new_process(); return -4; } if (n > 1) { @@ -703,6 +828,11 @@ static int load_library(const char *path) { /* A path selects the mapped image only if it names the mapped file, also once a VM was created: djni_attach() attaches without a path. */ int same = same_as_mapped(jvms[0].addr, path); +#ifdef __linux__ + /* The path names the mapped file, so it opens the image if the + names find_jvms() tried no longer do. */ + if (same == 1 && !jvms[0].handle) jvms[0].handle = reference(jvms[0].dyn, path); +#endif if (!same) snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", jvms[0].name); else if (same == -2) @@ -735,7 +865,7 @@ static int attach_library(void) { struct jvm_image *jvms; int n = find_jvms(&jvms), res = -4; if (n < 0) { - snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process. Use a new R process."); + use_new_process(); return -4; } if (!n) @@ -745,6 +875,8 @@ static int attach_library(void) { n, jvms[0].name); else if (created_vms(&jvms[0]) <= 0) snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", jvms[0].name); + else if (!jvms[0].handle) + snprintf(last_error, sizeof(last_error), "Cannot reference the JVM library mapped into this process: %s. Use a new R process.", jvms[0].name); else { jni_dl = jvms[0].handle; jvms[0].handle = NULL; @@ -760,7 +892,6 @@ const char *djni_mapped(int *vms) { free(mapped_name); mapped_name = NULL; if ((n = find_jvms(&jvms)) < 0) { - snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process"); *vms = -2; return NULL; } @@ -957,7 +1088,7 @@ static int run_jvm_test(void) { if ((*jvm)->DestroyJavaVM(jvm) != JNI_OK) { fprintf(stderr, "ERROR: cannot destroy VM\n"); return -1; - } + } printf("JVM destroyed.\n"); return 0; } diff --git a/src/djni.h b/src/djni.h index ab4e48c..1919acf 100644 --- a/src/djni.h +++ b/src/djni.h @@ -1,6 +1,6 @@ /* Dynamic JNI Allows to load JVM dynamically into a process. - + (C)2024 Simon Urbanek License: LGPL-2.1 or MIT */ From 9a3c487494ced04d27d4fe4b9daa67c990bd248c Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 20:57:56 +0200 Subject: [PATCH 57/63] docs: describe deferred selection in NEWS and the musl startup-path rule Add a NEWS entry for deferred selection, process-lifetime selection, host attachment and the changed behavior. Qualify the retry wording in ?.jinit for musl, and state in the integration README what discovery and references now guarantee on musl, with the reproduction that led to it. --- NEWS | 29 ++++++++++++++++++ man/jinit.Rd | 13 ++++++-- tests-integration/README.md | 61 +++++++++++++++++++++++++------------ 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/NEWS b/NEWS index 25d3751..a04dc60 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,35 @@ NEWS/ChangeLog for rJava -------------------------- +1.0-19 (not yet released) + o The JVM run-time is no longer linked into rJava or loaded when + the namespace loads. The first .jinit() selects one: a JVM + already created in the process, then java.home (Windows) or + JAVA_HOME, the JavaSoft registry (Windows), + /usr/libexec/java_home (macOS), and java on PATH. Until a + library is loaded, a failed .jinit() can be retried after + correcting the setting. .jloadJVM() selects a library by path; + .jvmState() reports "not-loaded" before any selection. + + o A selected JVM library stays selected for the life of the R + process: .junloadJVM() fails once one is loaded, and another + run-time requires a new R process. A JVM library already mapped + into the process is selected only by a path naming the same + file; any other is refused before it is loaded. + + o If native code in the process (e.g. a Java application that + embeds R through JRI) has created a VM, .jinit() attaches to it + whatever JAVA_HOME names. + + o Changed behavior: the Java home is now chosen when Java is + first used, not when rJava loads, and .onLoad() no longer changes + PATH (Windows) or DYLD_LIBRARY_PATH (macOS). On Linux with musl + (e.g. Alpine), only the run-time whose server directory was on + LD_LIBRARY_PATH when R started can be used; selecting another + is refused, and the refused library stays mapped, so a new R + process is needed (see ?.jinit). + + 1.0-18 2026-04-08 o JRI: fix regression in 1.0-16 breaking older versions of R. diff --git a/man/jinit.Rd b/man/jinit.Rd index 92ac76b..a4d42e7 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -107,9 +107,16 @@ loaded, \code{.jinit} fails and can be called again after correcting the setting. Once a run-time library is loaded, it remains selected for the life of the R process: a different JVM requires a new R - process. If a JVM library is already mapped into the process, a JVM - library path, such as one given to \code{.jloadJVM}, selects it only if - it names that same file, also when a JVM was created in it; attaching to + process. On Linux with musl (e.g. Alpine), the JDK's own libraries + find the JVM library by name only through \code{LD_LIBRARY_PATH} as + set when R started, so only the run-time whose server directory was + on it can be used: start R with \code{JAVA_HOME}, or + \code{R_JAVA_LD_LIBRARY_PATH}, naming the run-time. Selecting another + is refused before a JVM is created, but the refused library stays + mapped, so retrying with any run-time then requires a new R process. + If a JVM library is already mapped into the process, a JVM library + path, such as one given to \code{.jloadJVM}, selects it only if it + names that same file, also when a JVM was created in it; attaching to a created JVM is done by \code{.jinit} without a path. NOTE: \code{.jinit()} can be called implicitly by other functions such diff --git a/tests-integration/README.md b/tests-integration/README.md index c533086..348a796 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -37,9 +37,15 @@ macOS, `EnumProcessModules` on Windows), without loading anything. An image is a JVM library if it has a JVM library name (`libjvm.so`, `libjvm.dylib`, `jvm.dll`), even if malformed, or whatever its name if it defines itself `JNI_CreateJavaVM`, `JNI_GetCreatedJavaVMs` and `JVM_CurrentTimeMillis`. -"Defines itself" excludes definitions inherited from a dependency -(the symbol address lies outside the image) and forwarded Windows exports -(read from the export table in memory). JNI exports alone do not suffice: +"Defines itself" excludes definitions inherited from a dependency and +forwarded Windows exports. The exports are read without a handle where a +handle may be unavailable: on Linux from the image's dynamic symbol table in +memory (located through its `PT_DYNAMIC` segment and hash table, as the +dynamic loader reads it), on Windows from the export table in memory. On +macOS, `dlsym()` on a `RTLD_NOLOAD` handle must return an address inside the +image. On Linux, an image whose table cannot be read inside its own readable +segments makes discovery fail, rather than being taken for no JVM. JNI +exports alone do not suffice: rJava's own wrappers have them, and so does the macOS `libjli`, which forwards them to libjvm. Every HotSpot JDK in the matrix exports the three symbols; CI checks this for each runtime. @@ -74,9 +80,10 @@ even the one `RgetMappedJVM` reports for a created VM, since the file now at that name may be another runtime. `.jinit()` instead attaches through the internal `RattachJVM`, which takes no path: it requires exactly one mapped JVM image, with at least one created VM, and selects that image -through the reference djni holds to it, without loading anything. So it -attaches even if the file at the image's recorded name was deleted or -replaced, or cannot be identified (overlayfs, below). It never selects an +through a reference to it, without loading anything. So it attaches even if +the file at the image's recorded name was deleted or replaced, or cannot be +identified (overlayfs, below), as long as a reference can be taken (below +for musl). It never selects an image without a created VM, never resolves an ambiguity between several images, and fails if the image lacks a JNI entry point. On Windows it does not apply the `jvm.dll` name rule, which concerns starting a VM (untested: @@ -115,14 +122,27 @@ built for musl sets `LD_LIBRARY_PATH` and re-executes itself (`RequiresSetenv` in OpenJDK's `java_md.c`), and attaching checks no names. musl also resolves `dlopen(name, RTLD_NOLOAD)` for a path by opening that -file. djni references a mapped image, and reads the exports of one whose name -no JVM uses, through that call. So on musl a JVM library mapped under such a -name is no longer identified as a JVM once that file is deleted or replaced. -In `replaced deleted` on Alpine, `.jloadJVM()` of B then mapped B's library, -and only the name check refused it, before any VM was created. The -`replaced` and `hosted replaced` scenarios are therefore not run on musl. A -library under the name `libjvm.so` is identified by that name; deleting or -replacing it is not exercised on musl. +file and matching its device and inode (`load_library` in musl's +`ldso/dynlink.c`). A recorded name whose file was deleted or replaced then +yields no reference, or one to another mapped library. Earlier, djni read the +exports of an image through that reference, so on musl a JVM library mapped +under a name no JVM uses was no longer identified once its file was deleted or +replaced: in `replaced deleted` with B's server directory on the startup +library path, `.jloadJVM()` of B mapped B's library next to A's and succeeded, +as the name check then found B; with A's there, the name check refused B, but +only after mapping it. Now the exports are read from memory (above), so the +image is still identified and B is refused before anything is loaded. A +reference is taken only through a name the dynamic loader resolves to the +image itself, verified with `dlinfo(RTLD_DI_LINKMAP)` against the image's +dynamic section: its recorded name, then `libjvm.so` (the name the JDK's +libraries bind to), and for `.jloadJVM()` the path given, which must name the +mapped file. If none resolves to the image, selecting or attaching it is +refused (`Cannot reference the JVM library ...`) before a VM is created. The +`replaced` scenarios run on musl with A's and with B's server directory on +the startup library path, and check that nothing from B's home is mapped +after the refusals; with B's, A is refused too, by the name check. +`hosted replaced` runs with A's only: with B's, the host's own VM would bind +A's JDK libraries to B's `libjvm.so` before rJava is involved. ## Selected libraries are not unloaded @@ -252,8 +272,8 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `alias link/spelling/short` | the same preloaded library through a symbolic link or junction with spaces and non-ASCII characters, an alternative spelling, and an 8.3 name (Windows) | | `renamed copy/hardlink/symlink` | a JVM library mapped under a name no JVM uses: a copy of B rejects A and B's own library; a hard link of A in its directory (and a symbolic link, Linux and macOS) rejects B, then the same file starts A (Windows: is rejected as mapped under another name) | | `hosted renamed` | native code creates the VM from the hard link; it is reported with one VM, B is rejected, and `.jinit()` attaches (Linux and macOS) | -| `replaced deleted/replaced` | the file of the mapped library is deleted, or replaced by a hard link of B's (Windows: a mapped DLL cannot be deleted, so it is renamed in a copy of A's home and B's linked as `jvm.dll`): B, the replaced name and `.jinit()` with `JAVA_HOME` naming B are rejected, A's own file is accepted, and after replacement A starts | -| `hosted replaced` | the same replacement after a host created the VM (Windows: renamed in a copy of A's home, as above): `.jloadJVM()` rejects B, the replaced name and the name `RgetMappedJVM` reports, all before `.jinit()` attaches to A | +| `replaced deleted/replaced` | the file of the mapped library is deleted, or replaced by a hard link of B's (Windows: a mapped DLL cannot be deleted, so it is renamed in a copy of A's home and B's linked as `jvm.dll`): B, the replaced name and `.jinit()` with `JAVA_HOME` naming B are rejected, and on Linux nothing from B's home is then mapped; A's own file is accepted, and after replacement A starts (musl with B's server directory on the startup library path: A is refused by the name check instead, and B is still not mapped) | +| `hosted replaced` | the same replacement after a host created the VM (Windows: renamed in a copy of A's home, as above): `.jloadJVM()` rejects B, the replaced name and the name `RgetMappedJVM` reports, nothing from B's home is mapped (Linux), all before `.jinit()` attaches to A | | `decoy` | the JDK's `jli` library (JNI exports on macOS, no `JVM_` exports) is not reported and does not block selecting B | | `nonascii` | loading through such a path without a preload | | `byname` | musl, with A's server directory on the startup library path: selecting B is refused before a VM exists, A's library is not mapped, and the B library that musl keeps mapped then blocks A | @@ -267,7 +287,7 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `overlay.R` | actual 5.15, 6.1.0 and 6.8 kernels; four overlay layouts, unprivileged processes, exact refusal classes | | downstream | rJavaEnv and two minimal consumer packages (current pair only) | | `xlsx.R`, xlsx check | the CRAN package xlsx, whose `.onLoad()` starts Java through `.jpackage()`: loading it selects `JAVA_HOME` (JDK B, not the build JDK on `PATH`), another runtime is then rejected, and a workbook round trip works; `R CMD check` of the CRAN source package runs its testthat suite (current pair only) | -| `musl.sh` | the Linux scenarios above, `byname` and `jri.R` on Alpine, each scenario with the server directory of the runtime it starts on the startup library path | +| `musl.sh` | the Linux scenarios above, `byname` and `jri.R` on Alpine, each scenario with the server directory of the runtime it starts on the startup library path; `replaced deleted/replaced` also with B's, `hosted replaced` with A's only (above) | Bounded matrix, each on Linux (x86_64 and arm64), macOS (arm64) and Windows with R release; the build JDK is Temurin 17. On Linux arm64, JDK 8 has the @@ -330,8 +350,11 @@ from the repository root, one process per scenario. macOS the `symlink` variant shows the target name. - On musl, a process can start only the runtime whose server directory was on `LD_LIBRARY_PATH` when it started (above). Other runtimes are refused, not - supported. A JVM library mapped under a name no JVM uses is not identified - on musl once its file is deleted or replaced. + supported, and the refused library stays mapped, so retrying in the same + process with another runtime is refused too: start a new R process. A JVM + library mapped under a name that no longer opens it is identified, but can + be selected or attached only through a name that still resolves to it + (above); otherwise rJava refuses it. - Not covered: 32-bit Windows; Windows on ARM (the linkage, JRI and VC++ fixtures assume x64, and the old VC++ runtime test downloads x64 DLLs); OpenJ9 and other non-HotSpot VMs, which the export-based identification From cd6bbe5ef94172ac85e9c44ae8930d0ce4bf40b2 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 20 Oct 2026 21:36:01 +0200 Subject: [PATCH 58/63] fix: read mapped images on Linux only while dl_iterate_phdr() runs glibc holds its loader lock only until dl_iterate_phdr() returns, and dlclose() unmaps under that lock, so another thread may unmap any image, Java or not, right after. find_jvms() read the dynamic symbol tables of every listed image after the call returned, through saved addresses and segment ranges, and could fault on a library unmapped meanwhile. Identify JVM images inside the callback and keep only those, with their dynamic section and first segment as addresses that are compared, not read. JNI_GetCreatedJavaVMs is called only while a reference holds the image mapped, except on musl, which never unmaps an image. Attaching now requires the reference before calling into the image. Add a Linux race scenario: a preloaded fixture interposes dl_iterate_phdr() to unmap an unrelated library right after rJava lists the images, and maps and unmaps it on another thread while discovery runs. Compile the fixture with -Werror and show -Wall -Wextra warnings of djni.c. --- .github/workflows/dynamic.yaml | 17 +++ src/djni.c | 261 ++++++++++++++++++--------------- tests-integration/README.md | 18 ++- tests-integration/deferred.R | 67 +++++++++ tests-integration/dlrace.c | 108 ++++++++++++++ 5 files changed, 349 insertions(+), 122 deletions(-) create mode 100644 tests-integration/dlrace.c diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml index 73476f3..c0f8f7c 100644 --- a/.github/workflows/dynamic.yaml +++ b/.github/workflows/dynamic.yaml @@ -159,6 +159,23 @@ jobs: run replaced deleted run replaced replaced run hosted replaced + - name: Unmap an unrelated library while mapped JVM discovery runs + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + dir="$RUNNER_TEMP/dlrace" + mkdir -p "$dir" + # R's own flags show few warnings; the changed native code is checked with more. + cc="$(R CMD config CC)" + echo "::group::Warnings of src/djni.c with -Wall -Wextra" + $cc -O2 -Wall -Wextra -Isrc -c -o "$dir/djni.o" src/djni.c 2>&1 | tee "$dir/djni-warnings.txt" + echo "$(grep -c 'warning:' "$dir/djni-warnings.txt" || true) warnings" + echo "::endgroup::" + $cc -shared -fPIC -O2 -Wall -Wextra -Werror -o "$dir/dlrace.so" tests-integration/dlrace.c -ldl -lpthread + $cc -shared -fPIC -O2 -Wall -Wextra -Werror -DDLRACE_VICTIM -o "$dir/dlrace-victim.so" tests-integration/dlrace.c + LD_PRELOAD="$dir/dlrace.so" RJE_DLRACE="$dir/dlrace.so" RJE_DLRACE_VICTIM="$dir/dlrace-victim.so" \ + Rscript --vanilla tests-integration/deferred.R race - name: Remove hard links left in the JDK directories if: always() && env.RJE_JAVA_HOME_A != '' shell: bash diff --git a/src/djni.c b/src/djni.c index 055dcbf..e737277 100644 --- a/src/djni.c +++ b/src/djni.c @@ -530,115 +530,38 @@ static int is_jvm_name(const char *name) { base = base ? base + 1 : name; return !strcmp(base, "libjvm.dylib") || !strcmp(base, "libjvm.so"); } -/* An image mapped into the process: its name as the dynamic loader records - it, and where it is mapped, to tell its own definitions from those of its - dependencies (macOS) or to read its dynamic symbol table (Linux). */ -struct image { - char *name; -#ifdef __APPLE__ - const void *header; -#else - size_t nseg; - uintptr_t *seg; /* start and end of each readable PT_LOAD segment */ #ifdef __linux__ +/* An image as dl_iterate_phdr() reports it. Its memory is read only while + the callback runs: glibc holds its loader lock until dl_iterate_phdr() + returns, and dlclose() unmaps an image only while holding that lock + (elf/dl-iteratephdr.c, elf/dl-close.c), so any image, Java or not, may be + unmapped right after. musl never unmaps an image. */ +struct elf { uintptr_t base; + const ElfW(Phdr) *phdr; + size_t phnum; const ElfW(Dyn) *dyn; -#endif -#endif }; -struct images { struct image *v; size_t n, size; int failed; }; -static struct image *add_image(struct images *l, const char *name) { - struct image *im; - if (l->n == l->size) { - size_t size = l->size ? 2 * l->size : 64; - struct image *v = realloc(l->v, size * sizeof(*v)); - if (!v) { l->failed = 1; return NULL; } - l->v = v; - l->size = size; - } - im = &l->v[l->n]; - memset(im, 0, sizeof(*im)); - if (!(im->name = strdup(name))) { l->failed = 1; return NULL; } - l->n++; - return im; -} -#ifdef __linux__ -/* Only records images: dlopen() must not be called from this callback. */ -static int phdr_image(struct dl_phdr_info *info, size_t size, void *data) { - struct images *l = (struct images *) data; - struct image *im; - size_t i, n = 0; - if (!info->dlpi_name || !*info->dlpi_name) return 0; /* the executable */ - if (!(im = add_image(l, info->dlpi_name))) return 1; - im->base = (uintptr_t) info->dlpi_addr; - for (i = 0; i < info->dlpi_phnum; i++) { - const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; - if (ph->p_type == PT_LOAD && (ph->p_flags & PF_R)) n++; - if (ph->p_type == PT_DYNAMIC) im->dyn = (const ElfW(Dyn) *) (im->base + (uintptr_t) ph->p_vaddr); - } - if (n && !(im->seg = malloc(2 * n * sizeof(uintptr_t)))) { l->failed = 1; return 1; } - for (i = 0; i < info->dlpi_phnum; i++) - if (info->dlpi_phdr[i].p_type == PT_LOAD && (info->dlpi_phdr[i].p_flags & PF_R)) { - uintptr_t start = im->base + (uintptr_t) info->dlpi_phdr[i].p_vaddr; - im->seg[2 * im->nseg] = start; - im->seg[2 * im->nseg++ + 1] = start + (uintptr_t) info->dlpi_phdr[i].p_memsz; - } - return 0; -} -#endif -static void free_images(struct images *l) { - size_t i; - for (i = 0; i < l->n; i++) { - free(l->v[i].name); -#ifndef __APPLE__ - free(l->v[i].seg); -#endif - } - free(l->v); -} -/* Returns 0, or -1 if the images cannot be listed. Other systems list none. */ -static int list_images(struct images *l) { -#ifdef __APPLE__ - uint32_t i, n = _dyld_image_count(); - for (i = 0; i < n; i++) { - const char *name = _dyld_get_image_name(i); - const struct mach_header *header = _dyld_get_image_header(i); - struct image *im; - if (!name || !header) continue; - if (!(im = add_image(l, name))) break; - im->header = header; - } -#elif defined(__linux__) - dl_iterate_phdr(phdr_image, l); -#endif - return l->failed ? -1 : 0; -} -#ifdef __APPLE__ -/* Whether the image defines sym itself: dlsym() on its handle also finds - definitions it inherits from the libraries it depends on. */ -static int owns(void *handle, const struct image *im, const char *sym) { - void *p = dlsym(handle, sym); - Dl_info info; - return p && dladdr(p, &info) && info.dli_fbase == im->header; -} -#elif defined(__linux__) /* Whether [a, a + len) lies in one readable segment of the image. */ -static int in_image(const struct image *im, uintptr_t a, size_t len) { +static int in_image(const struct elf *im, uintptr_t a, size_t len) { size_t i; - for (i = 0; i < im->nseg; i++) - if (a >= im->seg[2 * i] && a < im->seg[2 * i + 1] && len <= im->seg[2 * i + 1] - a) return 1; + for (i = 0; i < im->phnum; i++) { + const ElfW(Phdr) *ph = &im->phdr[i]; + uintptr_t start = im->base + (uintptr_t) ph->p_vaddr, end = start + (uintptr_t) ph->p_memsz; + if (ph->p_type == PT_LOAD && (ph->p_flags & PF_R) && a >= start && a < end && len <= end - a) return 1; + } return 0; } /* glibc relocates the addresses of a dynamic section in place on most targets, musl never does: an address is used only if it is in the image. */ -static uintptr_t dyn_addr(const struct image *im, uintptr_t v) { +static uintptr_t dyn_addr(const struct elf *im, uintptr_t v) { if (in_image(im, v, 1)) return v; if (in_image(im, im->base + v, 1)) return im->base + v; return 0; } /* The number of entries in the dynamic symbol table, from its hash table as musl counts them (ldso/dynlink.c), or -1 if the table cannot be read. */ -static long count_syms(const struct image *im, const uint32_t *hash, const uint32_t *gnu) { +static long count_syms(const struct elf *im, const uint32_t *hash, const uint32_t *gnu) { const uint32_t *buckets, *chain; uint32_t i, max = 0; if (!gnu) return in_image(im, (uintptr_t) hash, 2 * sizeof(*hash)) ? (long) hash[1] : -1; @@ -660,7 +583,7 @@ static long count_syms(const struct image *im, const uint32_t *hash, const uint3 handle: musl takes one by opening the recorded name again, which may since name another file or none. Returns 1 if defined, setting *fn to its address if it is a function, 0 if not, -1 if the table cannot be read. */ -static int image_symbol(const struct image *im, const char *sym, uintptr_t *fn) { +static int image_symbol(const struct elf *im, const char *sym, uintptr_t *fn) { const ElfW(Dyn) *d = im->dyn; const ElfW(Sym) *syms; const char *strs; @@ -699,6 +622,108 @@ static int image_symbol(const struct image *im, const char *sym, uintptr_t *fn) } return 0; } +#endif +/* An image mapped into the process: its name as the dynamic loader records + it, and where it is mapped, to tell its own definitions from those of its + dependencies (macOS). On Linux, only JVM images are listed, with what + phdr_image() read of them: their dynamic section, which identifies them + but is not read again, their first readable segment and their + JNI_GetCreatedJavaVMs. */ +struct image { + char *name; +#ifdef __APPLE__ + const void *header; +#elif defined(__linux__) + const ElfW(Dyn) *dyn; + uintptr_t addr, created; +#endif +}; +struct images { struct image *v; size_t n, size; int failed; }; +static struct image *add_image(struct images *l, const char *name) { + struct image *im; + if (l->n == l->size) { + size_t size = l->size ? 2 * l->size : 64; + struct image *v = realloc(l->v, size * sizeof(*v)); + if (!v) { l->failed = 1; return NULL; } + l->v = v; + l->size = size; + } + im = &l->v[l->n]; + memset(im, 0, sizeof(*im)); + if (!(im->name = strdup(name))) { l->failed = 1; return NULL; } + l->n++; + return im; +} +#ifdef __linux__ +/* Records the JVM images, read only while dl_iterate_phdr() runs (struct + elf). An image is identified by its exports, not by a handle, which musl + may be unable to take; an unreadable table is not taken as no JVM. Only + reads memory: dlopen() must not be called from this callback. */ +static int phdr_image(struct dl_phdr_info *info, size_t size, void *data) { + struct images *l = (struct images *) data; + struct elf e = { (uintptr_t) info->dlpi_addr, info->dlpi_phdr, info->dlpi_phnum, NULL }; + struct image *im; + uintptr_t fn, addr = 0, created = 0; + size_t i; + int jvm, owned; + if (!info->dlpi_name || !*info->dlpi_name) return 0; /* the executable */ + for (i = 0; i < e.phnum; i++) { + const ElfW(Phdr) *ph = &e.phdr[i]; + if (ph->p_type == PT_LOAD && (ph->p_flags & PF_R) && !addr) addr = e.base + (uintptr_t) ph->p_vaddr; + if (ph->p_type == PT_DYNAMIC) e.dyn = (const ElfW(Dyn) *) (e.base + (uintptr_t) ph->p_vaddr); + } + jvm = is_jvm_name(info->dlpi_name); + owned = image_symbol(&e, "JVM_CurrentTimeMillis", &fn); + if (owned > 0) owned = image_symbol(&e, "JNI_CreateJavaVM", &fn); + if (owned > 0 || jvm) { + int found = image_symbol(&e, "JNI_GetCreatedJavaVMs", &fn); + if (owned > 0) owned = found; + if (found > 0) created = fn; + } + if (owned < 0 && !jvm) { + snprintf(last_error, sizeof(last_error), "Cannot read the exports of %s, mapped into this process", info->dlpi_name); + l->failed = 2; + return 1; + } + if (!jvm && owned <= 0) return 0; + if (!(im = add_image(l, info->dlpi_name))) return 1; + im->dyn = e.dyn; + im->addr = addr; + im->created = created; + return 0; +} +#endif +static void free_images(struct images *l) { + size_t i; + for (i = 0; i < l->n; i++) free(l->v[i].name); + free(l->v); +} +/* Returns 0, or -1 if the images cannot be listed. Other systems list none. */ +static int list_images(struct images *l) { +#ifdef __APPLE__ + uint32_t i, n = _dyld_image_count(); + for (i = 0; i < n; i++) { + const char *name = _dyld_get_image_name(i); + const struct mach_header *header = _dyld_get_image_header(i); + struct image *im; + if (!name || !header) continue; + if (!(im = add_image(l, name))) break; + im->header = header; + } +#elif defined(__linux__) + dl_iterate_phdr(phdr_image, l); +#endif + return l->failed ? -1 : 0; +} +#ifdef __APPLE__ +/* Whether the image defines sym itself: dlsym() on its handle also finds + definitions it inherits from the libraries it depends on. */ +static int owns(void *handle, const struct image *im, const char *sym) { + void *p = dlsym(handle, sym); + Dl_info info; + return p && dladdr(p, &info) && info.dli_fbase == im->header; +} +#elif defined(__linux__) /* A reference to the image by name, if the name opens it without loading anything. musl matches a name with a slash by the file now there, which may be another mapped library, so the handle must be the image's own. */ @@ -738,7 +763,9 @@ static void free_jvms(struct jvm_image *v, int n) { exports and a JVM_ export, whatever its name. JNI exports alone do not suffice: the macOS libjli defines them to forward to libjvm. Returns the number of JVM images, or -1 with last_error set if the images cannot be - listed, or (Linux) the exports of one cannot be read. */ + listed, or (Linux) the exports of one cannot be read. On Linux, the JVM + images were identified by phdr_image(), and their functions are called + only through a reference that holds them mapped (created_vms()). */ static int find_jvms(struct jvm_image **out) { struct images l = { NULL, 0, 0, 0 }; struct jvm_image *v = NULL; @@ -746,7 +773,7 @@ static int find_jvms(struct jvm_image **out) { int n = 0; *out = NULL; if (list_images(&l) || (l.n && !(v = calloc(l.n, sizeof(*v))))) { - snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process"); + if (l.failed != 2) snprintf(last_error, sizeof(last_error), "Cannot list the libraries mapped into this process"); free_images(&l); free(v); return -1; @@ -765,35 +792,20 @@ static int find_jvms(struct jvm_image **out) { if (jvm && handle && owns(handle, im, "JNI_GetCreatedJavaVMs")) *(void **) &v[n].created = dlsym(handle, "JNI_GetCreatedJavaVMs"); #elif defined(__linux__) - /* Identified by its exports, not by a handle, which musl may be - unable to take; an unreadable table is not taken as no JVM. */ - uintptr_t fn; - int owned = image_symbol(im, "JVM_CurrentTimeMillis", &fn); - if (owned > 0) owned = image_symbol(im, "JNI_CreateJavaVM", &fn); - if (owned > 0 || jvm) { - int created = image_symbol(im, "JNI_GetCreatedJavaVMs", &fn); - if (owned > 0) owned = created; - if (created > 0) v[n].created = (JNI_GetCreatedJavaVMs_fnptr) fn; - } - if (owned < 0 && !jvm) { - snprintf(last_error, sizeof(last_error), "Cannot read the exports of %s, mapped into this process", im->name); - free_jvms(v, n); - free_images(&l); - return -1; - } - jvm = jvm || owned > 0; - /* The recorded name, or the name the JDK's libraries bind to. */ - if (jvm && !(handle = reference(im->dyn, im->name))) - handle = reference(im->dyn, "libjvm.so"); + /* Only JVM images are listed. The recorded name, or the name the + JDK's libraries bind to. */ + jvm = 1; + if (!(handle = reference(im->dyn, im->name))) handle = reference(im->dyn, "libjvm.so"); v[n].dyn = im->dyn; + v[n].created = (JNI_GetCreatedJavaVMs_fnptr) im->created; #endif if (jvm) { v[n].name = im->name; im->name = NULL; #ifdef __APPLE__ v[n].addr = im->header; -#else - v[n].addr = im->nseg ? (const void *) im->seg[0] : NULL; +#elif defined(__linux__) + v[n].addr = (const void *) im->addr; #endif v[n++].handle = handle; } else if (handle) @@ -804,10 +816,17 @@ static int find_jvms(struct jvm_image **out) { *out = v; return n; } -/* The number of VMs created by a JVM image, or -1 if unknown. */ +/* The number of VMs created by a JVM image, or -1 if unknown. Its function + is called only while a reference holds the image mapped. musl, the Linux + C library assumed when neither glibc nor bionic is, never unmaps an image + (its dlclose() only checks the handle), so an address read while listing + the images stays valid without one. */ static int created_vms(const struct jvm_image *im) { JavaVM *vm; jsize count = 0; +#if !defined(__linux__) || defined(__GLIBC__) || defined(__BIONIC__) + if (!im->handle) return -1; +#endif return im->created && im->created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } static void use_new_process(void) { @@ -873,10 +892,10 @@ static int attach_library(void) { else if (n > 1) snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", n, jvms[0].name); - else if (created_vms(&jvms[0]) <= 0) - snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", jvms[0].name); else if (!jvms[0].handle) snprintf(last_error, sizeof(last_error), "Cannot reference the JVM library mapped into this process: %s. Use a new R process.", jvms[0].name); + else if (created_vms(&jvms[0]) <= 0) + snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", jvms[0].name); else { jni_dl = jvms[0].handle; jvms[0].handle = NULL; diff --git a/tests-integration/README.md b/tests-integration/README.md index 348a796..f1074c7 100644 --- a/tests-integration/README.md +++ b/tests-integration/README.md @@ -44,7 +44,16 @@ memory (located through its `PT_DYNAMIC` segment and hash table, as the dynamic loader reads it), on Windows from the export table in memory. On macOS, `dlsym()` on a `RTLD_NOLOAD` handle must return an address inside the image. On Linux, an image whose table cannot be read inside its own readable -segments makes discovery fail, rather than being taken for no JVM. JNI +segments makes discovery fail, rather than being taken for no JVM. Every +image, Java or not, is read only inside the `dl_iterate_phdr` callback: +glibc holds its loader lock until `dl_iterate_phdr` returns, and `dlclose` +unmaps an image only while holding it (`elf/dl-iteratephdr.c`, +`elf/dl-close.c`), so another thread may unmap any image right after. Only +the JVM images are kept: their name, their dynamic section and first segment +as addresses that are compared but not read, and their +`JNI_GetCreatedJavaVMs`, called only while a reference taken afterwards +(below) holds the image mapped. musl never unmaps an image (its `dlclose` +only checks the handle), so there the function is called without one. JNI exports alone do not suffice: rJava's own wrappers have them, and so does the macOS `libjli`, which forwards them to libjvm. Every HotSpot JDK in the matrix exports the three @@ -280,6 +289,7 @@ distribution with `sh mkdist`, installs it in an isolated library, and runs: | `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | | `laterload` | the later native loads above | | `hosted`, `hosted explicit` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK; `.jloadJVM()` rejects B, and with `explicit` selects A's own file first | +| `race` | Linux, with `dlrace.c` preloaded to interpose `dl_iterate_phdr`: an unrelated library is mapped before rJava lists the images and unmapped right after (deterministic), and mapped and unmapped 20000 times on another thread while discovery runs repeatedly (stress, which cannot prove the absence of a race): with nothing mapped, a preloaded A, and a VM a host created from it, discovery reports A with 0 and 1 VMs, B is refused and not mapped, attaching fails without a VM, and `.jinit()` attaches, with one `libjvm.so` mapped at the end. Not run on musl, which never unmaps a library | | every scenario | before anything is mapped, `RgetMappedJVM` is `NULL`: rJava's own JNI wrappers are not taken for a JVM | | `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where `.jloadJVM()` rejects the `JAVA_HOME` runtime and rJava attaches to the host JVM (all three platforms, both host JDKs) | | `dll-search.c` | Windows DLL search state after failed and successful loads | @@ -348,6 +358,12 @@ from the repository root, one process per scenario. reuse on the current Ubuntu 5.15 and current-kernel guests. dyld records a library loaded through a symbolic link by its target, so on macOS the `symlink` variant shows the target name. +- On Linux, a reference to a JVM image is taken after `dl_iterate_phdr` + returns. It is the image's own only by comparing its dynamic section + address (`RTLD_DI_LINKMAP`), so an image unloaded meanwhile and another + loaded under its name with its dynamic section at that same address would + be taken for it. On macOS, `_dyld_get_image_name` can race with a + concurrent unload, a window of the dyld API that djni does not close. - On musl, a process can start only the runtime whose server directory was on `LD_LIBRARY_PATH` when it started (above). Other runtimes are refused, not supported, and the refused library stays mapped, so retrying in the same diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R index 1fb887f..fba2f4c 100644 --- a/tests-integration/deferred.R +++ b/tests-integration/deferred.R @@ -357,6 +357,73 @@ switch(scenario, } } }, + race = { + # Linux, with tests-integration/dlrace.c preloaded: another thread may + # unmap any library, Java or not, once dl_iterate_phdr() returns. The + # fixture unmaps an unrelated library right then (raced), or maps and + # unmaps it on another thread while discovery runs (churned), which + # exercises the race but cannot prove its absence. + fixture <- Sys.getenv("RJE_DLRACE") + victim <- Sys.getenv("RJE_DLRACE_VICTIM") + stopifnot(file.exists(fixture), file.exists(victim), + grepl(fixture, Sys.getenv("LD_PRELOAD"), fixed = TRUE)) + dyn.load(fixture) + counts <- function() .C("dlrace_counts", hits = 0L, unmapped = 0L, armed = 0L) + raced <- function(expr) { + before <- counts() + .C("dlrace_arm", victim) + value <- expr + after <- counts() + stopifnot(after$armed == 0L, after$hits == before$hits + 1L, after$unmapped == before$unmapped + 1L) + invisible(value) + } + churned <- function(check, cycles = 20000L) { + stopifnot(.C("dlrace_churn", victim, cycles, result = 0L)$result == 0L) + calls <- 0L + repeat { + status <- .C("dlrace_churn_status", running = 0L, done = 0L, unmapped = 0L) + if (!status$running) break + check() + calls <- calls + 1L + } + log("discovery ran", calls, "times while another thread mapped and unmapped", + victim, status$done, "times, unmapped after", status$unmapped) + stopifnot(status$done == cycles, status$unmapped == cycles, calls > 0L) + } + attach <- function() .Call(internal("RattachJVM")) + one_jvm <- function() { + maps <- sub("^(\\S+\\s+){5}", "", grep("^(\\S+\\s+){5}/", readLines("/proc/self/maps"), value = TRUE)) + jvms <- unique(maps[basename(maps) == "libjvm.so"]) + if (length(jvms) != 1L) stop("expected one JVM library mapped, found: ", paste(jvms, collapse = ", ")) + } + stopifnot(is.null(raced(mapped_jvm()))) + churned(function() stopifnot(is.null(mapped_jvm()))) + raced(fails(attach(), "No JVM library is mapped")) + # A JVM mapped outside rJava, without and then with a created VM. + path <- lib(home_a) + helper <- Sys.getenv("RJE_JVMHOST") + stopifnot(file.exists(helper)) + dyn.load(helper) + preload(path) + raced(expect_mapped(path, 0L)) + churned(function() stopifnot(identical(mapped_jvm()$vms, 0L))) + raced(fails(attach(), "created no VM")) + raced(fails(internal(".jloadJVM")(lib(home_b)), "already mapped")) + unmapped(home_b) + boot <- file.path(system.file("java", package = "rJava"), "boot") + stopifnot(.C("rjava_test_create_jvm", path, paste0("-Djava.class.path=", boot), result = 0L)$result == 0L) + raced(expect_mapped(path, 1L)) + churned(function() stopifnot(identical(mapped_jvm()$vms, 1L))) + Sys.setenv(JAVA_HOME = home_b) + raced(fails(rJava::.jloadJVM(lib(home_b)), "already mapped")) + unmapped(home_b) + # .jinit() attaches to the created VM, through the same discovery. + raced(start(home_a)) + stopifnot(state()$state == "attached") + churned(function() stopifnot(identical(mapped_jvm()$vms, 1L)), 5000L) + unmapped(home_b) + one_jvm() + }, decoy = { # The Java launcher library (jli) has JNI exports on macOS that forward to # libjvm, but is no JVM: it must neither be reported nor block selection. diff --git a/tests-integration/dlrace.c b/tests-integration/dlrace.c new file mode 100644 index 0000000..c73a011 --- /dev/null +++ b/tests-integration/dlrace.c @@ -0,0 +1,108 @@ +/* Test fixture (Linux): unmaps a library unrelated to Java while djni lists + the mapped images, without a test hook in rJava. Preloaded, it interposes + dl_iterate_phdr(); built with -DDLRACE_VICTIM, it is that library: + cc -shared -fPIC -o dlrace.so dlrace.c -ldl -lpthread + cc -shared -fPIC -DDLRACE_VICTIM -o dlrace-victim.so dlrace.c + glibc holds its loader lock while dl_iterate_phdr() runs and may unmap any + image as soon as it returns. dlrace_arm() makes the next listing by rJava + on the calling thread map the victim before it starts and unmap it right + after it returns, when djni may no longer read it. dlrace_churn() maps and + unmaps the victim on another thread instead, concurrently with listings. */ +#ifdef DLRACE_VICTIM +int rjava_dlrace_victim(void) { return 42; } +#else +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +typedef int (*callback_fn)(struct dl_phdr_info *, size_t, void *); +static int (*real_iterate)(callback_fn, void *); +static char victim[4096]; +static pthread_t armed_thread, churn_thread; +static int armed, hits, unmapped, churning, joined = 1, cycles, churn_unmapped; + +__attribute__((constructor)) static void init(void) { + void *real = dlsym(RTLD_NEXT, "dl_iterate_phdr"); + memcpy(&real_iterate, &real, sizeof(real)); +} +/* Whether the victim is no longer loaded. musl never unloads a library. */ +static int gone(void) { + void *h = dlopen(victim, RTLD_NOW | RTLD_LOCAL | RTLD_NOLOAD); + if (h) dlclose(h); + return !h; +} +static int from_rjava(callback_fn callback) { + Dl_info info; + const char *base; + void *addr; + memcpy(&addr, &callback, sizeof(addr)); + if (!dladdr(addr, &info) || !info.dli_fname) return 0; + base = strrchr(info.dli_fname, '/'); + return !strcmp(base ? base + 1 : info.dli_fname, "rJava.so"); +} +int dl_iterate_phdr(callback_fn callback, void *data) { + void *h = NULL; + int res; + if (__atomic_load_n(&armed, __ATOMIC_ACQUIRE) && pthread_equal(pthread_self(), armed_thread) && + from_rjava(callback)) { + __atomic_store_n(&armed, 0, __ATOMIC_RELEASE); + if (!(h = dlopen(victim, RTLD_NOW | RTLD_LOCAL))) fprintf(stderr, "dlrace: %s\n", dlerror()); + } + res = real_iterate(callback, data); + if (h) { + dlclose(h); + hits++; + unmapped += gone(); + } + return res; +} +void dlrace_arm(char **path) { + snprintf(victim, sizeof(victim), "%s", path[0]); + armed_thread = pthread_self(); + __atomic_store_n(&armed, 1, __ATOMIC_RELEASE); +} +/* hits: listings by rJava that saw the victim, unmapped: those after which + it was no longer loaded, armed: whether a listing is still expected. */ +void dlrace_counts(int *n, int *closed, int *pending) { + *n = hits; + *closed = unmapped; + *pending = __atomic_load_n(&armed, __ATOMIC_ACQUIRE); +} +static void *churn(void *arg) { + int i, n = *(int *) arg; + for (i = 0; i < n; i++) { + void *h = dlopen(victim, RTLD_NOW | RTLD_LOCAL); + if (!h) break; + dlclose(h); + churn_unmapped += gone(); + __atomic_store_n(&cycles, i + 1, __ATOMIC_RELEASE); + } + __atomic_store_n(&churning, 0, __ATOMIC_RELEASE); + return NULL; +} +void dlrace_churn(char **path, int *n, int *result) { + static int count; + if (!joined) { *result = -1; return; } + snprintf(victim, sizeof(victim), "%s", path[0]); + count = *n; + cycles = churn_unmapped = 0; + churning = 1; + if ((*result = pthread_create(&churn_thread, NULL, churn, &count))) churning = 0; + else joined = 0; +} +/* running: whether the thread still runs; once it has stopped, joins it and + reports its cycles and those after which the victim was unloaded. */ +void dlrace_churn_status(int *running, int *done, int *closed) { + *running = __atomic_load_n(&churning, __ATOMIC_ACQUIRE); + *done = __atomic_load_n(&cycles, __ATOMIC_ACQUIRE); + *closed = 0; + if (!*running && !joined) { + pthread_join(churn_thread, NULL); + joined = 1; + } + if (!*running) *closed = churn_unmapped; +} +#endif From 2ddb149c6e4e2dd218a90d07c9bd020b180a8b41 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Fri, 23 Oct 2026 14:10:55 +0200 Subject: [PATCH 59/63] fix: address filesystem identity and deferred-loading compatibility review --- NEWS | 14 +++- R/java-runtime.R | 9 ++- R/jinit.R | 22 +++--- configure | 116 ++--------------------------- configure.ac | 56 +------------- configure.win | 13 ++-- man/jinit.Rd | 29 +++++++- src/Makevars.in | 1 - src/Makevars.win | 1 + src/djni.c | 114 +++++++++------------------- src/djni.h | 3 +- src/init.c | 18 +---- src/jri/Makefile.win | 1 - src/jri/src/Makefile.all | 3 - src/jri/src/Makefile.win | 1 - src/jri/src/Rdecl.h | 14 ---- src/jri/src/Rengine.c | 9 ++- src/jri/src/Rinit.c | 9 ++- src/rJava.h | 4 +- tests-integration/jri-optional.sh | 25 ------- tests-integration/msvc.R | 19 ----- tests-integration/windows-msvc.ps1 | 91 ---------------------- 22 files changed, 131 insertions(+), 441 deletions(-) delete mode 100644 tests-integration/jri-optional.sh delete mode 100644 tests-integration/msvc.R delete mode 100644 tests-integration/windows-msvc.ps1 diff --git a/NEWS b/NEWS index a04dc60..1a4b992 100644 --- a/NEWS +++ b/NEWS @@ -6,7 +6,9 @@ the namespace loads. The first .jinit() selects one: a JVM already created in the process, then java.home (Windows) or JAVA_HOME, the JavaSoft registry (Windows), - /usr/libexec/java_home (macOS), and java on PATH. Until a + /usr/libexec/java_home (macOS), R CMD config JAVA_HOME (Linux), + and java on PATH. Linux thus retains the javareconf JDK as its + default when JAVA_HOME is unset. Until a library is loaded, a failed .jinit() can be retried after correcting the setting. .jloadJVM() selects a library by path; .jvmState() reports "not-loaded" before any selection. @@ -15,7 +17,10 @@ process: .junloadJVM() fails once one is loaded, and another run-time requires a new R process. A JVM library already mapped into the process is selected only by a path naming the same - file; any other is refused before it is loaded. + file; a different or unverifiable file is refused before it is + loaded. Device mismatches on btrfs and overlayfs are not evidence + of a different file: matching inodes also require a loader + reference to the same image, without loading another library. o If native code in the process (e.g. a Java application that embeds R through JRI) has created a VM, .jinit() attaches to it @@ -29,6 +34,11 @@ is refused, and the refused library stays mapped, so a new R process is needed (see ?.jinit). + o JVM loading is now always dynamic. --enable-dynload defaults to + yes; --disable-dynload is rejected instead of silently producing + a dynamic build under --as-needed. RJAVA_FORCE_LOAD is no longer + used, since namespace loading never requires a JVM. + 1.0-18 2026-04-08 o JRI: fix regression in 1.0-16 breaking older versions of R. diff --git a/R/java-runtime.R b/R/java-runtime.R index 0111d0a..0f7f5fc 100644 --- a/R/java-runtime.R +++ b/R/java-runtime.R @@ -1,11 +1,12 @@ -## Deferred JVM discovery. This runs at the first .jinit() or .jloadJVM(), +## Deferred JVM discovery. This runs at the first .jinit(), ## never at namespace load. The Java home is the first of: ## 1. getOption("java.home") (Windows only, as before) or JAVA_HOME; a value ## that is not an existing directory is ignored with a warning, as rJava ## did on Windows and macOS before deferred loading ## 2. on Windows, the CurrentVersion of the JavaSoft registry keys ## 3. on macOS, /usr/libexec/java_home -## 4. the java.home property reported by `java` on PATH +## 4. on Linux, R CMD config JAVA_HOME (the javareconf setting) +## 5. the java.home property reported by `java` on PATH ## An existing Java home without a JVM library is an error, not a fallback. .find.java.runtime <- function() { windows <- .Platform$OS.type == "windows" @@ -20,6 +21,10 @@ if (!nzchar(javahome) && windows) javahome <- .find.java.registry() if (!nzchar(javahome) && file.exists("/usr/libexec/java_home")) javahome <- .java.output("/usr/libexec/java_home") + if (!nzchar(javahome) && identical(Sys.info()[["sysname"]], "Linux")) { + configured <- .java.output(file.path(R.home("bin"), "R"), c("CMD", "config", "JAVA_HOME")) + if (nzchar(configured) && dir.exists(configured)) javahome <- configured + } if (!nzchar(javahome)) javahome <- .java.output("java", c("-cp", shQuote(system.file("java", package="rJava")), "getsp", "java.home")) if (nzchar(javahome) && dir.exists(javahome)) javahome else "" diff --git a/R/jinit.R b/R/jinit.R index 27edb9e..63c5ca7 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -4,7 +4,7 @@ ## ## $Id$ -.check.JVM <- function() +.check.JVM <- function() .Call(RJava_checkJVM) .need.init <- function() .Call(RJava_needs_init) @@ -72,7 +72,7 @@ if (length(classpath)) classpath <- paste(classpath,collapse=path.sep) } - + # merge CLASSPATH environment variable if present cp<-Sys.getenv("CLASSPATH") if (!is.null(cp)) { @@ -81,7 +81,7 @@ else classpath<-paste(classpath,cp,sep=path.sep) } - + # set rJava/java/boot for boostrap (so we can get RJavaClassLoader) boot.classpath <- file.path(.rJava.base.path,"java","boot") @@ -115,7 +115,7 @@ # this should remove any lingering .jclass objects from the global env # left there by previous versions of rJava pj <- grep("^\\.jclass",ls(1,all.names=TRUE),value=TRUE) - if (length(pj)>0) { + if (length(pj)>0) { rm(list=pj,pos=1) if (exists(".jniInitialized",1)) rm(list=".jniInitialized",pos=1) if (!silent) warning("rJava found hidden Java objects in your workspace. Internal objects from previous versions of rJava were deleted. Please note that Java objects cannot be saved in the workspace.") @@ -184,12 +184,12 @@ cpc <- unique(strsplit(classpath, .Platform$path.sep)[[1]]) if (length(cpc)) .jaddClassPath(cpc) } else stop("Unable to create a Java class loader.") - + ##.Call(RJava_new_class_loader, .rJava.base.path, file.path(.rJava.base.path, lib)) ## lock namespace bindings for (x in .delayed.variables) lockBinding(x, .env) - + ## now we need to update the attached namespace (package env) as well m <- match(paste("package", getNamespaceName(.env), sep = ":"), search())[1] if (!is.na(m)) { ## only is it is attached @@ -200,15 +200,15 @@ lockBinding(x, pe) } } - - # FIXME: is this the best place or should this be done + + # FIXME: is this the best place or should this be done # internally right after the RJavaClassLoader is instantiated # init the cached RJavaTools class in the jni side .Call(initRJavaTools) - + # not yet # import( c( "java.lang", "java.util") ) - + invisible(xr) } @@ -252,7 +252,7 @@ ar <- .jcall("java/lang/reflect/Array", "Ljava/lang/Object;", "newInstance", .jclassObject, 1:1) - + for (fn in rcp) { f <- .jnew("java/io/File", fn) url <- .jcall(f, "Ljava/net/URL;", "toURL") diff --git a/configure b/configure index 0ab3e79..1405ec3 100755 --- a/configure +++ b/configure @@ -1359,7 +1359,7 @@ Optional Features: programs to embed R. [auto] --enable-headless enable initialization in headless mode. [auto] --enable-Xrs use -Xrs in Java initialization. [auto] - --enable-dynload load JVM dynamically (without linking). [yes] + --enable-dynload load JVM dynamically (required). [yes] --enable-debug enable debug flags and output. [no] --enable-mem-profile enable memory profiling. [debug] --enable-callbacks enable the support for callbacks from Java into R. @@ -2462,6 +2462,11 @@ else case e in #( esac fi +if test "x${want_dynload}" = xno +then : + as_fn_error $? "--disable-dynload is no longer supported: JVM loading is deferred to .jinit()." "$LINENO" 5 +fi + ## enable debug flags # Check whether --enable-debug was given. @@ -4009,17 +4014,7 @@ printf "%s\n" "#define HAVE_XRS 1" >>confdefs.h fi -use_dynload="$want_dynload" -if test x"$use_dynload" = xauto; then - if test "x$OSNAME" = xDarwin; then - use_dynload=yes - else - use_dynload=no - fi -fi -if test x"$use_dynload" != xyes; then - use_dynload=no -fi +use_dynload=yes { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JVM will be loaded dynamically" >&5 printf %s "checking whether JVM will be loaded dynamically... " >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${use_dynload}" >&5 @@ -4032,103 +4027,6 @@ else USE_DYNLOAD_FALSE= fi - -if test x${use_dynload} = xno; then - -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI programs can be compiled" >&5 -printf %s "checking whether JNI programs can be compiled... " >&6; } -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#include -int main(void) { - jobject o; - JNI_CreateJavaVM(0, 0, 0); - return 0; -} - -_ACEOF -if ac_fn_c_try_link "$LINENO" -then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } -else case e in #( - e) as_fn_error $? "Cannot compile a simple JNI program. See config.log for details. - -Make sure you have Java Development Kit installed and correctly registered in R. -If in doubt, re-run \"R CMD javareconf\" as root. -" "$LINENO" 5 ;; -esac -fi -rm -f core conftest.err conftest.$ac_objext conftest.beam \ - conftest$ac_exeext conftest.$ac_ext - -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JNI programs run" >&5 -printf %s "checking whether JNI programs run... " >&6; } -if test "$cross_compiling" = yes -then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: don't know (cross-compiling)" >&5 -printf "%s\n" "don't know (cross-compiling)" >&6; } -else case e in #( - e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#include -int main(void) { - jsize n; - JNI_GetCreatedJavaVMs(NULL, 0, &n); - return 0; -} - -_ACEOF -if ac_fn_c_try_run "$LINENO" -then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "yes" >&6; } -else case e in #( - e) as_fn_error $? "Unable to run a simple JNI program. Make sure you have configured R with Java support (see R documentation) and check config.log for failure reason." "$LINENO" 5 ;; -esac -fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext ;; -esac -fi - - -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking JNI data types" >&5 -printf %s "checking JNI data types... " >&6; } -if test "$cross_compiling" = yes -then : - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} -as_fn_error $? "cannot run test program while cross compiling -See 'config.log' for more details" "$LINENO" 5; } -else case e in #( - e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#include -int main(void) { - return (sizeof(int)==sizeof(jint) && sizeof(long)==sizeof(long) && sizeof(jbyte)==sizeof(char) && sizeof(jshort)==sizeof(short) && sizeof(jfloat)==sizeof(float) && sizeof(jdouble)==sizeof(double))?0:1; -} - -_ACEOF -if ac_fn_c_try_run "$LINENO" -then : - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ok" >&5 -printf "%s\n" "ok" >&6; } -else case e in #( - e) as_fn_error $? "One or more JNI types differ from the corresponding native type. You may need to use non-standard compiler flags or a different compiler in order to fix this." "$LINENO" 5 ;; -esac -fi -rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ - conftest.$ac_objext conftest.beam conftest.$ac_ext ;; -esac -fi - - -fi ## dynload = no - if test "${want_jri}" = auto; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether JRI should be compiled (autodetect)" >&5 printf %s "checking whether JRI should be compiled (autodetect)... " >&6; } diff --git a/configure.ac b/configure.ac index 6c32cd8..fef8f3c 100644 --- a/configure.ac +++ b/configure.ac @@ -64,9 +64,11 @@ AC_ARG_ENABLE([Xrs], ## enable dynloaded JVM AC_ARG_ENABLE([dynload], -[AS_HELP_STRING([--enable-dynload],[load JVM dynamically (without linking). @<:@yes@:>@])], +[AS_HELP_STRING([--enable-dynload],[load JVM dynamically (required). @<:@yes@:>@])], [want_dynload="${enableval}"], [want_dynload=yes]) +AS_IF([test "x${want_dynload}" = xno], + [AC_MSG_ERROR([--disable-dynload is no longer supported: JVM loading is deferred to .jinit().])]) ## enable debug flags AC_ARG_ENABLE([debug], @@ -238,61 +240,11 @@ if test x"$has_xrs" = xyes; then AC_DEFINE(HAVE_XRS, 1, [Set if the Java parameter -Xrs is supported]) fi -use_dynload="$want_dynload" -if test x"$use_dynload" = xauto; then - if test "x$OSNAME" = xDarwin; then - use_dynload=yes - else - use_dynload=no - fi -fi -if test x"$use_dynload" != xyes; then - use_dynload=no -fi +use_dynload=yes AC_MSG_CHECKING([whether JVM will be loaded dynamically]) AC_MSG_RESULT(${use_dynload}) AM_CONDITIONAL(USE_DYNLOAD, [test "x${use_dynload}" = xyes]) -if test x${use_dynload} = xno; then - -AC_MSG_CHECKING([whether JNI programs can be compiled]) -AC_LINK_IFELSE([AC_LANG_SOURCE([[ -#include -int main(void) { - jobject o; - JNI_CreateJavaVM(0, 0, 0); - return 0; -} - ]])],[AC_MSG_RESULT(yes)], - [AC_MSG_ERROR([Cannot compile a simple JNI program. See config.log for details. - -Make sure you have Java Development Kit installed and correctly registered in R. -If in doubt, re-run "R CMD javareconf" as root. -])]) - -AC_MSG_CHECKING([whether JNI programs run]) -AC_RUN_IFELSE([AC_LANG_SOURCE([ -#include -int main(void) { - jsize n; - JNI_GetCreatedJavaVMs(NULL, 0, &n); - return 0; -} - ])], - [AC_MSG_RESULT([yes])], - [AC_MSG_ERROR([Unable to run a simple JNI program. Make sure you have configured R with Java support (see R documentation) and check config.log for failure reason.])], - [AC_MSG_RESULT([don't know (cross-compiling)])]) - -AC_MSG_CHECKING([JNI data types]) -AC_RUN_IFELSE([AC_LANG_SOURCE([[ -#include -int main(void) { - return (sizeof(int)==sizeof(jint) && sizeof(long)==sizeof(long) && sizeof(jbyte)==sizeof(char) && sizeof(jshort)==sizeof(short) && sizeof(jfloat)==sizeof(float) && sizeof(jdouble)==sizeof(double))?0:1; -} - ]])],[AC_MSG_RESULT([ok])],[AC_MSG_ERROR([One or more JNI types differ from the corresponding native type. You may need to use non-standard compiler flags or a different compiler in order to fix this.])],[]) - -fi ## dynload = no - if test "${want_jri}" = auto; then AC_MSG_CHECKING([whether JRI should be compiled (autodetect)]) AC_MSG_RESULT([${has_R_shlib}]) diff --git a/configure.win b/configure.win index b2c71a6..f72c921 100644 --- a/configure.win +++ b/configure.win @@ -22,7 +22,7 @@ echo " JAVA_HOME=$JAVA_HOME" echo "JAVA_HOME:=$JAVA_HOME" > src/Makevars.java -if [ -e src/jri/configure.win ] && [ "${JRI:-1}" != 0 ]; then +if [ -e jri/configure.win ]; then echo "=== Building JRI ===" CONFIGURED=1 export CONFIGURED @@ -33,20 +33,21 @@ if [ -e src/jri/configure.win ] && [ "${JRI:-1}" != 0 ]; then export R_HOME export RHOME export JAVA_HOME - cd src/jri - sh configure.win && make + cd jri + sh configure.win + make BR=$? - cd ../.. + cd .. if [ $BR = 0 ]; then echo "=== JRI built ===" mkdir -p inst/jri # also copy into R_ARCH is present to avoid overwriting different archs if [ -n "${R_ARCH}" ]; then mkdir -p inst/jri${R_ARCH} - cp src/jri/src/jri.dll inst/jri${R_ARCH}/ + cp jri/src/jri.dll inst/jri${R_ARCH}/ fi # yet still install into JRI in case users get confused - cp -r src/jri/run.bat src/jri/src/jri.dll src/jri/src/JRI.jar src/jri/examples inst/jri/ + cp -r jri/run.bat jri/src/jri.dll jri/src/JRI.jar jri/examples inst/jri/ else echo "**** WARNING: JRI could NOT be built" >&2 if [ -z "$IGNORE" ]; then diff --git a/man/jinit.Rd b/man/jinit.Rd index a4d42e7..e0d5f83 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -1,6 +1,9 @@ \name{jinit} \alias{.jinit} \alias{.jvmState} +\alias{.jloadJVM} +\alias{.junloadJVM} +\alias{.jfindAndLoadJVM} \title{ Initialize Java VM } @@ -14,6 +17,9 @@ .jinit(classpath = NULL, parameters = getOption("java.parameters", NA), ..., silent = FALSE, force.init = FALSE) .jvmState() +.jloadJVM(path, silent = FALSE) +.junloadJVM() +.jfindAndLoadJVM() } \arguments{ \item{classpath}{Any additional classes to include in the Java class @@ -30,7 +36,10 @@ currently. \code{NULL} is equivalent to \code{character()}. } \item{...}{Other optional Java initialization parameters (implementation-dependent).} - \item{silent}{If set to \code{TRUE} no warnings are issued.} + \item{path}{Path to a JVM run-time library for explicit selection.} + \item{silent}{If set to \code{TRUE} no initialization warnings are issued. + For \code{.jloadJVM}, return the native status code instead of signaling + a load error: zero means success and negative values mean failure.} \item{force.init}{If set to \code{TRUE} JVM is re-initialized even if it is already running.} } @@ -101,7 +110,9 @@ otherwise the \code{java.home} option (Windows only) or the \code{JAVA_HOME} environment variable; then, on Windows, the \code{CurrentVersion} of the JavaSoft registry keys; on macOS, - \code{/usr/libexec/java_home}; and finally the \code{java} executable + \code{/usr/libexec/java_home}; on Linux, the existing Java home reported + by \code{R CMD config JAVA_HOME} (set by \code{R CMD javareconf}); + and finally the \code{java} executable found on \code{PATH}. A Java home setting that is not an existing directory is ignored with a warning. If no run-time can be found or loaded, \code{.jinit} fails and can be called again after correcting @@ -119,6 +130,20 @@ names that same file, also when a JVM was created in it; attaching to a created JVM is done by \code{.jinit} without a path. + \code{.jloadJVM} selects a library without starting a VM and returns + \code{TRUE} on success, or a status code when \code{silent = TRUE}. + \code{.jfindAndLoadJVM} performs the automatic selection used by + \code{.jinit}. \code{.junloadJVM} returns \code{TRUE} if no library has + been selected and otherwise signals an error: unloading does not make + replacing a JVM in the same process safe. + + On Linux, filesystem device numbers from \code{stat} and + \code{/proc/self/maps} can differ (e.g. btrfs subvolumes and overlayfs). + Matching inodes then also require the dynamic loader to confirm the + already mapped image without loading anything. If identity cannot be + verified, explicit selection fails; attaching to an existing VM remains + possible without a pathname comparison. + NOTE: \code{.jinit()} can be called implicitly by other functions such as \code{\link{.jpackage}} or \code{\link{J}}. } diff --git a/src/Makevars.in b/src/Makevars.in index f59c622..bcc42e4 100644 --- a/src/Makevars.in +++ b/src/Makevars.in @@ -1,6 +1,5 @@ # we need to add JNI specific stuff here PKG_CPPFLAGS=-I. @JAVA_CPPFLAGS@ -@USE_DYNLOAD_FALSE@PKG_LIBS=@JAVA_LIBS@ JAVA_HOME=@JAVA_HOME@ # make SHLIB believe that we know better what the objects are #OBJECTS=Rglue.o callJNI.o initJNI.o rJava.o jri.o jri_glue.o diff --git a/src/Makevars.win b/src/Makevars.win index db3df09..34b3aab 100644 --- a/src/Makevars.win +++ b/src/Makevars.win @@ -26,3 +26,4 @@ PKG_CPPFLAGS = -D_R_ -DWin32 -Ijvm-w32 -I$(RHOME)/src/include $(JAVA_INCLUDES:%= PKG_LIBS = -Ljvm-w32 $(warning PKG_CPPFLAGS are $(PKG_CPPFLAGS)) + diff --git a/src/djni.c b/src/djni.c index e737277..8154f08 100644 --- a/src/djni.c +++ b/src/djni.c @@ -18,7 +18,6 @@ #elif defined(__linux__) #include #include -#include #endif #endif #include @@ -278,68 +277,6 @@ static int created_vms(HMODULE module) { created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"); return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } -/* Keep already loaded runtimes alive; otherwise prefer the selected JDK's - own set, as java.exe does. Only a missing bundled copy falls back to - System32. References last until process exit, without changing PATH or - retaining a DLL search directory. Optional load failures are diagnosed. */ -static void load_bundled_msvc(void) { - static const wchar_t *names[] = { - L"vcruntime140.dll", L"vcruntime140_1.dll", L"msvcp140.dll", - L"msvcp140_1.dll", L"msvcp140_2.dll", L"msvcp140_atomic_wait.dll" - }; - static HMODULE retained[sizeof(names) / sizeof(names[0])]; - wchar_t *path = module_path(jni_dl), *slash, *candidate; - size_t i, len; - if (!path) { - fprintf(stderr, "rJava: cannot determine the selected JVM's MSVC runtime directory (Windows error %lu).\n", (unsigned long) GetLastError()); - return; - } - slash = wcsrchr(path, L'\\'); - if (!slash) { free(path); return; } - *slash = 0; /* bin\server */ - slash = wcsrchr(path, L'\\'); - if (!slash) { free(path); return; } - *slash = 0; /* bin */ - len = wcslen(path); - candidate = malloc(sizeof(wchar_t) * (len + 64)); - if (candidate) { - for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { - DWORD attrs, error; - char *name; - if (retained[i]) continue; - /* Unlike GetModuleHandleW, this owns a reference even if the - component that originally loaded the runtime is unloaded. */ - if (GetModuleHandleExW(0, names[i], &retained[i])) continue; - wcscpy(candidate, path); - candidate[len] = L'\\'; - wcscpy(candidate + len + 1, names[i]); - attrs = GetFileAttributesW(candidate); - error = attrs == INVALID_FILE_ATTRIBUTES ? GetLastError() : 0; - if (error && error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { - name = to_utf8(candidate); - fprintf(stderr, "rJava: cannot inspect bundled MSVC runtime %s (Windows error %lu).\n", - name ? name : "(unknown path)", (unsigned long) error); - free(name); - continue; - } - if (attrs != INVALID_FILE_ATTRIBUTES) - retained[i] = LoadLibraryExW(candidate, NULL, - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); - else - retained[i] = LoadLibraryExW(names[i], NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - if (retained[i]) continue; - error = GetLastError(); - /* A JDK need not use every optional runtime in the list. */ - if (attrs == INVALID_FILE_ATTRIBUTES && error == ERROR_MOD_NOT_FOUND) continue; - name = to_utf8(attrs != INVALID_FILE_ATTRIBUTES ? candidate : names[i]); - fprintf(stderr, "rJava: cannot retain MSVC runtime %s (Windows error %lu); later Java native loads may fail.\n", - name ? name : "(unknown path)", (unsigned long) error); - free(name); - } - free(candidate); - } else fprintf(stderr, "rJava: cannot prepare MSVC runtimes: out of memory.\n"); - free(path); -} static int load_library(const char *path) { wchar_t *wide = NULL, *full = NULL, *bin = NULL, *slash; struct jvm_image *jvms = NULL; @@ -480,7 +417,8 @@ const char *djni_mapped(int *vms) { the loader recorded is not used: the file there may have been deleted or replaced since, while the mapping keeps the old file. Returns 1 for the same file, 0 for another or no file, -1 if the mapped file is unknown, and - -2 on overlayfs, where a mismatch proves nothing (below). */ + -2 for mismatched devices, where an inode mismatch proves nothing, and + 2 for matching inodes on mismatched devices, requiring a loader check. */ static int same_as_mapped(const void *addr, const char *path) { struct stat s; int res = -1; @@ -503,24 +441,24 @@ static int same_as_mapped(const void *addr, const char *path) { size_t size = 0; unsigned long long a = (uintptr_t) addr, start, end, ino; unsigned int maj = 0, min = 0; - int device_mismatch = 0; - struct statfs fs; + int device_mismatch; if (f) { while (getline(&line, &size, f) > 0) if (sscanf(line, "%llx-%llx %*s %*s %x:%x %llu", &start, &end, &maj, &min, &ino) == 5 && a >= start && a < end) { if (ino) { device_mismatch = major(s.st_dev) != maj || minor(s.st_dev) != min; - res = !device_mismatch && (unsigned long long) s.st_ino == ino; + res = device_mismatch ? ((unsigned long long) s.st_ino == ino ? 2 : -2) : + (unsigned long long) s.st_ino == ino; } break; } free(line); fclose(f); } - /* Some overlayfs kernels report a layer file in /proc/self/maps, - while stat() reports the overlay identity. A mismatch is ambiguous. */ - if (!res && device_mismatch && !statfs(path, &fs) && fs.f_type == 0x794c7630) res = -2; + /* btrfs reports a subvolume device to stat(), but the superblock + device in maps. Overlayfs may report a layer's inode as well. + Neither device mismatch establishes that this is another file. */ } #endif return res; @@ -666,6 +604,7 @@ static int phdr_image(struct dl_phdr_info *info, size_t size, void *data) { uintptr_t fn, addr = 0, created = 0; size_t i; int jvm, owned; + (void) size; if (!info->dlpi_name || !*info->dlpi_name) return 0; /* the executable */ for (i = 0; i < e.phnum; i++) { const ElfW(Phdr) *ph = &e.phdr[i]; @@ -824,10 +763,23 @@ static int find_jvms(struct jvm_image **out) { static int created_vms(const struct jvm_image *im) { JavaVM *vm; jsize count = 0; + JNI_GetCreatedJavaVMs_fnptr created = im->created; #if !defined(__linux__) || defined(__GLIBC__) || defined(__BIONIC__) if (!im->handle) return -1; #endif - return im->created && im->created(&vm, 1, &count) == JNI_OK ? (int) count : -1; +#ifdef __linux__ + /* Resolve from the held image, rather than calling an address saved + before the reference was acquired. A library may have been unmapped + and another mapped at the same address in that interval. */ + if (im->handle) { + Dl_info info; + void *symbol = dlsym(im->handle, "JNI_GetCreatedJavaVMs"); + created = symbol && dladdr(symbol, &info) && info.dli_fbase == im->addr ? + (JNI_GetCreatedJavaVMs_fnptr) symbol : NULL; + dlerror(); + } +#endif + return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } static void use_new_process(void) { size_t n = strlen(last_error); @@ -848,6 +800,18 @@ static int load_library(const char *path) { also once a VM was created: djni_attach() attaches without a path. */ int same = same_as_mapped(jvms[0].addr, path); #ifdef __linux__ + if (same == 2) { + /* A matching inode alone is insufficient across devices. Ask + the loader whether this path already names this exact image; + RTLD_NOLOAD never maps the requested file or its dependencies. + Retain that reference so no second JVM is loaded. */ + void *handle = reference(jvms[0].dyn, path); + if (handle) { + if (jvms[0].handle) dlclose(jvms[0].handle); + jvms[0].handle = handle; + same = 1; + } else same = -2; + } /* The path names the mapped file, so it opens the image if the names find_jvms() tried no longer do. */ if (same == 1 && !jvms[0].handle) jvms[0].handle = reference(jvms[0].dyn, path); @@ -855,7 +819,7 @@ static int load_library(const char *path) { if (!same) snprintf(last_error, sizeof(last_error), "Another JVM library is already mapped: %s. Use a new R process.", jvms[0].name); else if (same == -2) - snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s, because the overlayfs mapping identity cannot be verified. Use a new R process.", + snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s, because the filesystem mapping identity cannot be verified. Use a new R process.", path, jvms[0].name); else if (same < 0) snprintf(last_error, sizeof(last_error), "Cannot verify that %s is the JVM library already mapped: %s. Use a new R process.", path, jvms[0].name); @@ -987,9 +951,6 @@ int djni_load(const char *path) { #ifdef __linux__ /* After load_syms(), so that a library that is no JVM is reported as such. */ if (!res && (res = check_jvm_name(path))) djni_unload(); -#endif -#ifdef _WIN32 - if (!res) load_bundled_msvc(); #endif return res; } @@ -1001,9 +962,6 @@ int djni_attach(void) { if (jni_dl) return -1; if ((res = attach_library())) return res; res = load_syms(); -#ifdef _WIN32 - if (!res) load_bundled_msvc(); -#endif return res; } int djni_unload(void) { diff --git a/src/djni.h b/src/djni.h index 1919acf..ed4e178 100644 --- a/src/djni.h +++ b/src/djni.h @@ -16,8 +16,7 @@ the mapped file, whether or not it has created a VM. */ int djni_load(const char *path); /* Selects the only JVM library mapped into the process if it has created a - VM, by its mapped image: no path is interpreted and no JVM is loaded. - Windows also retains the selected JDK's MSVC dependency DLLs. */ + VM, by its mapped image: no path is interpreted and no JVM is loaded. */ int djni_attach(void); int djni_unload(void); const char* djni_last_error(void); diff --git a/src/init.c b/src/init.c index 8f76143..885a6f3 100644 --- a/src/init.c +++ b/src/init.c @@ -122,7 +122,7 @@ static int initJVM(const char *user_classpath, int opts, char **optv, int hooks, vm_args.version = JNI_VERSION_1_2; if((res = JNI_GetDefaultJavaVMInitArgs(&vm_args)) != JNI_OK) { if (res == JNI_NO_DJNI) /* DJNI has not loaded a run-time yet */ - error("No Java run-time is not loaded."); + error("No Java run-time is loaded."); error("JNI 1.2 or higher is required"); return -1; } @@ -776,22 +776,6 @@ REP SEXP RinitJVM(SEXP par) { #endif } -/* detaches (if only attached) or destroys (if created) JVM */ -HIDE void destroyJVM(void) { - if (!jvm) - return; - if (rJava_JVM_state == JVM_STATE_CREATED || - rJava_JVM_state == JVM_STATE_ATTACHED) { - (*jvm)->DetachCurrentThread(jvm); - if (rJava_JVM_state == JVM_STATE_CREATED) - (*jvm)->DestroyJavaVM(jvm); - jvm = 0; - eenv = 0; - rJava_JVM_state = (rJava_JVM_state == JVM_STATE_CREATED) ? JVM_STATE_DESTROYED : JVM_STATE_DETACHED; - } -} - - /** * Initializes the cached values of classes and methods used internally * These classes and methods are the ones that are in rJava (RJavaTools, ...) diff --git a/src/jri/Makefile.win b/src/jri/Makefile.win index 5ce6cb7..69770f5 100644 --- a/src/jri/Makefile.win +++ b/src/jri/Makefile.win @@ -1,5 +1,4 @@ # Windows-specific part of the Makefile -.DEFAULT_GOAL := windows.all include src/Makefile.wconf include Makevars.win diff --git a/src/jri/src/Makefile.all b/src/jri/src/Makefile.all index e5c3d45..3146350 100644 --- a/src/jri/src/Makefile.all +++ b/src/jri/src/Makefile.all @@ -10,12 +10,10 @@ all: $(TARGETS) JRI.jar: $(JRI_JSRC) $(JNIPREFIX)jri$(JNISO) $(JAVAC) $(JFLAGS) $(JRI_JFLAGS) -d . $(JRI_JSRC) - if [ -f org_rosuda_JRI_Rengine.h ]; then tr -d '\r' < org_rosuda_JRI_Rengine.h > jri-header.tmp && mv jri-header.tmp org_rosuda_JRI_Rengine.h; fi $(JAR) fc $@ org $(JNIPREFIX)jri$(JNISO) org_rosuda_JRI_Rengine.h: org/rosuda/JRI/Rengine.class if [ -n "$(JAVAH)" ]; then $(JAVAH) -d . -classpath . org.rosuda.JRI.Rengine; fi - if [ -f org_rosuda_JRI_Rengine.h ]; then tr -d '\r' < org_rosuda_JRI_Rengine.h > jri-header.tmp && mv jri-header.tmp org_rosuda_JRI_Rengine.h; fi Rcallbacks.o: Rcallbacks.c Rcallbacks.h globals.h rjstring.h org_rosuda_JRI_Rengine.h $(CC) -c -o $@ $< $(CFLAGS) $(CPICF) $(JAVAINC) $(RINC) $(JRI_CPPFLAGS) @@ -46,7 +44,6 @@ win32/libjvm.dll.a: org/rosuda/JRI/Rengine.class org/rosuda/JRI/REXP.class org/rosuda/JRI/Mutex.class: $(JRI_JSRC) $(JAVAC) $(JFLAGS) $(JRI_JFLAGS) -d . $^ - if [ -f org_rosuda_JRI_Rengine.h ]; then tr -d '\r' < org_rosuda_JRI_Rengine.h > jri-header.tmp && mv jri-header.tmp org_rosuda_JRI_Rengine.h; fi clean: rm -rf $(TARGETS) org *.o *~ org_rosuda_JRI_Rengine.h *$(JNISO) *.class *~ diff --git a/src/jri/src/Makefile.win b/src/jri/src/Makefile.win index 678e2fe..a443119 100644 --- a/src/jri/src/Makefile.win +++ b/src/jri/src/Makefile.win @@ -1,5 +1,4 @@ # Windows-specific part of the Makefile -.DEFAULT_GOAL := all include $(R_HOME)/etc$(R_ARCH)/Makeconf include Makefile.wconf diff --git a/src/jri/src/Rdecl.h b/src/jri/src/Rdecl.h index f88b59f..abe9913 100644 --- a/src/jri/src/Rdecl.h +++ b/src/jri/src/Rdecl.h @@ -6,19 +6,9 @@ #include -#if defined(_WIN64) && !defined(WIN64) -#define WIN64 1 -#endif - -#ifndef Win32 #define R_INTERFACE_PTRS 1 #define CSTACK_DEFNS 1 #include /* R_ReadConsole since 4.6.0 */ -#elif R_VERSION >= R_Version(4,6,0) -/* Rinterface.h is Unix-only and is not shipped by Windows R. - Older R uses the declaration in the compatibility block below. */ -int R_ReadConsole(const char *, unsigned char *, int, int); -#endif /* some have been added to R 4.6.0 */ #if R_VERSION >= R_Version(4,6,0) @@ -29,8 +19,4 @@ int R_ReadConsole(char*, unsigned char*, int, int); /* include/Defn.h */ int Rf_initialize_R(int ac, char **av); /* include/Rembedded.h - exists since 2.4.0 */ #endif /* R < 4.6.0 */ -#if defined(Win32) && R_VERSION < R_Version(4,6,0) -extern __declspec(dllimport) int UserBreak; -#endif - #endif /* __RDECL_H__ */ diff --git a/src/jri/src/Rengine.c b/src/jri/src/Rengine.c index 6beb967..340bc2b 100644 --- a/src/jri/src/Rengine.c +++ b/src/jri/src/Rengine.c @@ -49,7 +49,14 @@ LibExtern int R_interrupts_pending; #ifdef Win32 #include -/* UserBreak is declared with DLL-import linkage in Rdecl.h / Rembedded.h. */ +#ifdef _MSC_VER +__declspec(dllimport) int UserBreak; +#else +#ifndef WIN64 +#define UserBreak (*_imp__UserBreak) +#endif +extern int UserBreak; +#endif #else /* for R_runHandlers */ #include diff --git a/src/jri/src/Rinit.c b/src/jri/src/Rinit.c index 95d5dc8..05d815d 100644 --- a/src/jri/src/Rinit.c +++ b/src/jri/src/Rinit.c @@ -124,7 +124,14 @@ extern __declspec(dllimport) uintptr_t R_CStackStart; /* Initial stack address * #define SIGBREAK 21 /* to readers pgrp upon background tty read */ /* one way to allow user interrupts: called in ProcessEvents */ -/* UserBreak is declared with DLL-import linkage in Rdecl.h / Rembedded.h. */ +#ifdef _MSC_VER +__declspec(dllimport) int UserBreak; +#else +#ifndef WIN64 +#define UserBreak (*_imp__UserBreak) +#endif +extern int UserBreak; +#endif /* calls into the R DLL */ extern char *getDLLVersion(void); diff --git a/src/rJava.h b/src/rJava.h index 783f027..1a59a80 100644 --- a/src/rJava.h +++ b/src/rJava.h @@ -177,8 +177,7 @@ extern int rJava_initialized; #define JVM_STATE_CREATED 1 /* JVM was created by us */ #define JVM_STATE_ATTACHED 2 /* we attached to another JVM */ #define JVM_STATE_DEAD 4 /* set when Java exit handler was called */ -#define JVM_STATE_DETACHED 8 /* detached from existing JVM */ -#define JVM_STATE_DESTROYED 9 /* JVM was destroyed */ +#define JVM_STATE_DESTROYED 8 /* JVM was destroyed */ extern int rJava_JVM_state; @@ -206,7 +205,6 @@ extern jmethodID mid_RJavaImport_lookup ; extern jmethodID mid_RJavaImport_exists ; HIDE void init_rJava(void); -HIDE void destroyJVM(void); /* in otables.c */ // turn this for debugging in otables.c diff --git a/tests-integration/jri-optional.sh b/tests-integration/jri-optional.sh deleted file mode 100644 index fa3b0c5..0000000 --- a/tests-integration/jri-optional.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -archive="$RUNNER_TEMP/$(basename "$(ls ../rJava_*.tar.gz)")" -work="$RUNNER_TEMP/jri-optional" -mkdir -p "$work"/{strict,ignore,skip} -# A real javac failure must stop the normal install. -if env IGNORE= JRI=1 JRI_JFLAGS=--rjava-intentional-invalid-option R CMD INSTALL \ - --library="$work/strict" "$archive" > "$work/strict.log" 2>&1; then - echo '::error::JRI failure unexpectedly succeeded'; exit 1 -fi -if command -v rg >/dev/null; then - rg -q -- 'rjava-intentional-invalid-option' "$work/strict.log" -else - grep -q -- 'rjava-intentional-invalid-option' "$work/strict.log" -fi -for mode in ignore skip; do - if [[ "$mode" == ignore ]]; then - env IGNORE=1 JRI=1 JRI_JFLAGS=--rjava-intentional-invalid-option R CMD INSTALL \ - --library="$work/$mode" "$archive" > "$work/$mode.log" 2>&1 - else - env IGNORE= JRI=0 R CMD INSTALL --library="$work/$mode" "$archive" > "$work/$mode.log" 2>&1 - fi - env R_LIBS="$work/$mode" JAVA_HOME="$RJE_JAVA_HOME_B" Rscript --vanilla -e \ - 'library(rJava); stopifnot(!file.exists(system.file("jri", "JRI.jar", package="rJava"))); .jinit(); stopifnot(.jcall("java/lang/System", "S", "getProperty", "java.version") != ""); cat("PASS rJava without JRI\n")' -done diff --git a/tests-integration/msvc.R b/tests-integration/msvc.R deleted file mode 100644 index b364f0a..0000000 --- a/tests-integration/msvc.R +++ /dev/null @@ -1,19 +0,0 @@ -# Verify selected-JDK runtime origins for fresh startup and native-host attachment. -source("tests-integration/deferred.R") -stopifnot(windows, scenario %in% c("laterload", "hosted", "retained")) -ps <- file.path(sysroot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") -modules <- system2(ps, c("-NoProfile", "-Command", - shQuote(sprintf("(Get-Process -Id %d).Modules | ForEach-Object FileName", Sys.getpid()))), stdout = TRUE) -stopifnot(is.null(attr(modules, "status"))) -runtimes <- modules[grepl("[/\\\\](msvcp140(_1|_2|_atomic_wait)?|vcruntime140(_1)?)[.]dll$", modules, ignore.case = TRUE)] -names <- tolower(basename(chartr("\\", "/", runtimes))) -stopifnot("msvcp140.dll" %in% names) -selected <- normalizePath(if (variant == "b") home_b else home_a, winslash = "/") -bin <- dirname(dirname(lib(selected))) -for (i in seq_along(runtimes)) { - bundled <- file.path(bin, names[i]) - expected <- if (file.exists(bundled)) bundled else file.path(sysroot, "System32", names[i]) - stopifnot(identical(tolower(normalizePath(runtimes[i], winslash = "/")), - tolower(normalizePath(expected, winslash = "/")))) -} -cat("PASS bundled MSVC dependencies:", runtimes, sep = "\n ") diff --git a/tests-integration/windows-msvc.ps1 b/tests-integration/windows-msvc.ps1 deleted file mode 100644 index d1b6089..0000000 --- a/tests-integration/windows-msvc.ps1 +++ /dev/null @@ -1,91 +0,0 @@ -param([ValidateSet('normal', 'missing', 'old')][string]$Mode) -$ErrorActionPreference = 'Stop' -$names = @('vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'msvcp140_atomic_wait.dll') -$system = Join-Path $env:SystemRoot 'System32' -$work = Join-Path $env:RUNNER_TEMP 'msvc-2015' -$hidden = @() -$installed = @() -function Invoke-R([string[]]$RArguments) { - & Rscript --vanilla @RArguments - if ($LASTEXITCODE -ne 0) { throw "R test failed ($Mode): $RArguments" } -} -if ($Mode -eq 'old') { - New-Item -ItemType Directory -Force $work | Out-Null - $exe = Join-Path $work 'vc_redist.x64.exe' - # Microsoft Visual C++ 2015 Update 3, 14.0.24212.0. Never install it globally. - # Official download page: https://www.microsoft.com/en-us/download/details.aspx?id=53587 - Invoke-WebRequest 'https://download.microsoft.com/download/6/d/f/6df3ff94-f7f9-4f0b-838c-a328d1a7d0ee/vc_redist.x64.exe' -OutFile $exe - if ((Get-FileHash $exe -Algorithm SHA256).Hash -ne 'D7257265DBC0635C96DD67DDF938A09ABE0866CB2D4FA05F8B758C8644E724E4') { throw 'VC++ 2015 installer SHA256 mismatch' } - # Extract its pinned Burn payload cabinet, then its Minimum Runtime cabinet. - # The cabinet offsets/length correspond to the exact SHA256 above. - $bytes = [IO.File]::ReadAllBytes($exe) - $cab = New-Object byte[] 14465070 - [Array]::Copy($bytes, 821856, $cab, 0, $cab.Length) - [IO.File]::WriteAllBytes((Join-Path $work 'payload.cab'), $cab) - & 7z x -y "-o$work/payload" "$work/payload.cab" a10 | Out-Null - if ($LASTEXITCODE -ne 0) { throw 'Burn payload extraction failed' } - & 7z x -y "-o$work/old" "$work/payload/a10" msvcp140.dll vcruntime140.dll | Out-Null - if ($LASTEXITCODE -ne 0) { throw 'Runtime cabinet extraction failed' } - foreach ($name in 'msvcp140.dll', 'vcruntime140.dll') { - $old = Get-Item "$work/old/$name" - if ($old.VersionInfo.FileMajorPart -ne 14 -or $old.VersionInfo.FileMinorPart -ne 0 -or $old.VersionInfo.FileBuildPart -ne 24212) { throw "Wrong old runtime: $name" } - Write-Output "Old fixture $name version $($old.VersionInfo.FileVersion), SHA256 $((Get-FileHash $old).Hash)" - foreach ($javaHome in $env:RJE_JAVA_HOME_A, $env:RJE_JAVA_HOME_B) { - $bundles = Get-ChildItem $javaHome -Recurse -Filter $name - if (!$bundles) { throw "No bundled $name in $javaHome" } - foreach ($bundle in $bundles) { - $v = $bundle.VersionInfo - $version = [version]::new($v.FileMajorPart, $v.FileMinorPart, $v.FileBuildPart, $v.FilePrivatePart) - if ($version -le [version]'14.0.24212.0') { throw "Fixture is not older than $bundle ($version)" } - Write-Output "JDK $bundle version $version" - } - } - } -} -try { - if ($Mode -ne 'normal') { - foreach ($name in $names) { - $path = Join-Path $system $name - if (Test-Path $path) { - takeown /f $path | Out-Null - if ($LASTEXITCODE -ne 0) { throw "takeown failed: $path" } - icacls $path /grant '*S-1-5-32-544:F' | Out-Null - if ($LASTEXITCODE -ne 0) { throw "icacls failed: $path" } - Rename-Item $path "$name.rjava-hidden" - $hidden += $path - } - if (Test-Path $path) { throw "Runtime still visible: $path" } - } - if ($Mode -eq 'old') { - foreach ($name in 'msvcp140.dll', 'vcruntime140.dll') { - $path = Join-Path $system $name - Copy-Item "$work/old/$name" $path - $installed += $path - } - # Positive control: a clean R process really can load the old system DLL. - Invoke-R -RArguments @('-e', 'p <- file.path(Sys.getenv("SystemRoot"), "System32", "msvcp140.dll"); d <- dyn.load(p); stopifnot(tolower(normalizePath(d[["path"]])) == tolower(normalizePath(p))); cat("PASS old System32 runtime control\n")') - } - } - Write-Output "Testing runtime policy: $Mode" - Invoke-R -RArguments @('tests-integration/msvc.R', 'retained') - Invoke-R -RArguments @('tests-integration/msvc.R', 'laterload') - Invoke-R -RArguments @('tests-integration/msvc.R', 'laterload', 'b') - Invoke-R -RArguments @('tests-integration/msvc.R', 'hosted', 'laterload') - Invoke-R -RArguments @('tests-integration/msvc.R', 'hosted', 'laterload-b') - if ($Mode -ne 'normal') { - Invoke-R -RArguments @('tests-integration/jri.R') - $a = $env:RJE_JAVA_HOME_A - $b = $env:RJE_JAVA_HOME_B - try { - $env:RJE_JAVA_HOME_A = $b - $env:RJE_JAVA_HOME_B = $a - Invoke-R -RArguments @('tests-integration/jri.R') - } finally { - $env:RJE_JAVA_HOME_A = $a - $env:RJE_JAVA_HOME_B = $b - } - } -} finally { - foreach ($path in $installed) { Remove-Item $path -Force } - foreach ($path in $hidden) { Rename-Item "$path.rjava-hidden" (Split-Path $path -Leaf) } -} From fdbe195dc8e9a480345197eede0d0131a912eac5 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Fri, 23 Oct 2026 14:11:09 +0200 Subject: [PATCH 60/63] chore: keep integration infrastructure in the fork testing branch --- .github/workflows/dynamic.yaml | 299 -------------- .github/workflows/overlay.yaml | 32 -- mkdist | 2 +- tests-integration/JriAttach.java | 69 ---- tests-integration/README.md | 386 ------------------ tests-integration/deferred.R | 579 --------------------------- tests-integration/dll-search.c | 62 --- tests-integration/dlrace.c | 108 ----- tests-integration/jni-abi.R | 51 --- tests-integration/jri.R | 30 -- tests-integration/jvmhost.c | 61 --- tests-integration/musl.sh | 123 ------ tests-integration/overlay-layouts.sh | 61 --- tests-integration/overlay-vm.sh | 98 ----- tests-integration/overlay.R | 64 --- tests-integration/xlsx-install.R | 16 - tests-integration/xlsx.R | 42 -- 17 files changed, 1 insertion(+), 2082 deletions(-) delete mode 100644 .github/workflows/dynamic.yaml delete mode 100644 .github/workflows/overlay.yaml delete mode 100644 tests-integration/JriAttach.java delete mode 100644 tests-integration/README.md delete mode 100644 tests-integration/deferred.R delete mode 100644 tests-integration/dll-search.c delete mode 100644 tests-integration/dlrace.c delete mode 100644 tests-integration/jni-abi.R delete mode 100644 tests-integration/jri.R delete mode 100644 tests-integration/jvmhost.c delete mode 100755 tests-integration/musl.sh delete mode 100644 tests-integration/overlay-layouts.sh delete mode 100644 tests-integration/overlay-vm.sh delete mode 100644 tests-integration/overlay.R delete mode 100644 tests-integration/xlsx-install.R delete mode 100644 tests-integration/xlsx.R diff --git a/.github/workflows/dynamic.yaml b/.github/workflows/dynamic.yaml deleted file mode 100644 index c0f8f7c..0000000 --- a/.github/workflows/dynamic.yaml +++ /dev/null @@ -1,299 +0,0 @@ -name: Deferred JVM integration -on: - push: - branches: [feature/dynamic-cross-platform] - workflow_dispatch: -jobs: - dynamic: - # Three runtime pairs per platform instead of every OS x vendor x version: - # legacy layouts (JDK 8/11), current LTS (17/21, with downstream packages), - # and other vendors spanning 11 to 25. The build JDK is always Temurin 17. - # Linux arm64 runs all three: JDK 8 there uses jre/lib/aarch64/server. - name: ${{ matrix.os }} ${{ matrix.set }} (${{ matrix.a-dist }} ${{ matrix.a-version }}, ${{ matrix.b-dist }} ${{ matrix.b-version }}) - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] - set: [legacy, current, vendor] - include: - - { set: legacy, a-dist: temurin, a-version: '8', b-dist: temurin, b-version: '11' } - # Temurin has no JDK 8 for macOS on arm64. - - { set: legacy, os: macos-latest, a-dist: zulu } - - { set: current, a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21', downstream: true } - - { set: vendor, a-dist: corretto, a-version: '11', b-dist: zulu, b-version: '25' } - - { set: r45, os: windows-latest, r: '4.5', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } - # The oldest R that DESCRIPTION accepts. - - { set: r36, os: ubuntu-24.04, r: '3.6', a-dist: temurin, a-version: '17', b-dist: temurin, b-version: '21' } - runs-on: ${{ matrix.os }} - timeout-minutes: 45 - env: - NOAWT: '1' - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: r-lib/actions/setup-r@v2 - with: - r-version: ${{ matrix.r || 'release' }} - rtools-version: '45' - - uses: actions/setup-java@v5 - id: java-a - with: - distribution: ${{ matrix.a-dist }} - java-version: ${{ matrix.a-version }} - - uses: actions/setup-java@v5 - id: java-b - with: - distribution: ${{ matrix.b-dist }} - java-version: ${{ matrix.b-version }} - - uses: actions/setup-java@v5 - id: java-build - with: - distribution: temurin - java-version: '17' - - name: Record runtimes - shell: bash - env: - JAVA_A: ${{ steps.java-a.outputs.path }} - JAVA_B: ${{ steps.java-b.outputs.path }} - run: | - set -euo pipefail - for home in "$JAVA_A" "$JAVA_B"; do - echo "== $home"; "$home/bin/java" -version 2>&1 - # The trailing slash follows a home that is a symbolic link (Linux). - find "$home/" \( -name 'libjvm.*' -o -name jvm.dll \) -print - # djni recognizes a JVM library under any name by these exports. - lib="$(find "$home/" \( -name 'libjvm.*' -o -name jvm.dll \) -path '*server*' -print -quit)" - case "$RUNNER_OS" in - Linux) syms="$(nm -D --defined-only "$lib")" ;; - macOS) syms="$(nm -gU "$lib")" ;; - Windows) syms="$(objdump -p "$lib" | tr -d '\r')" ;; - esac - for sym in JNI_CreateJavaVM JNI_GetCreatedJavaVMs JVM_CurrentTimeMillis; do - if grep -Eq "[ _]$sym(@|\$| )" <<< "$syms"; then echo "exports $sym"; else echo "::error::$lib does not export $sym"; exit 1; fi - done - done - { - echo "RJE_JAVA_HOME_A=$JAVA_A" - echo "RJE_JAVA_HOME_B=$JAVA_B" - echo "RJE_FALLBACK_HOME=$JAVA_B" - } >> "$GITHUB_ENV" - - name: Prepare Windows discovery and path fixtures - if: runner.os == 'Windows' - shell: pwsh - run: | - # Generate 8.3 names for directories created from now on. - fsutil 8dot3name set C: 0 - # Machine-wide JavaSoft keys would take precedence over this user key. - reg query 'HKLM\Software\JavaSoft' /s 2>$null - if ($LASTEXITCODE -eq 0) { Write-Output 'HKLM JavaSoft keys are present' } else { Write-Output 'No HKLM JavaSoft keys' } - reg add 'HKCU\Software\JavaSoft\JDK' /v CurrentVersion /d rjava-test /f - reg add 'HKCU\Software\JavaSoft\JDK\rjava-test' /v JavaHome /d "$env:RJE_JAVA_HOME_B" /f - - name: Configure build Java - if: runner.os != 'Windows' - shell: bash - run: | - echo "export PATH=$PATH" > reconf.sh - echo "export JAVA_HOME=$JAVA_HOME" >> reconf.sh - echo "R CMD javareconf" >> reconf.sh - sudo bash reconf.sh - - name: Generate distribution and install in isolated library - shell: bash - run: | - set -euo pipefail - sh mkdist - mkdir -p "$RUNNER_TEMP/rjava-lib" - archive="$(ls ../rJava_*.tar.gz)" - R CMD INSTALL --library="$RUNNER_TEMP/rjava-lib" "$archive" - cp "$archive" "$RUNNER_TEMP/" - echo "R_LIBS=$RUNNER_TEMP/rjava-lib" >> "$GITHUB_ENV" - - name: Inspect native JVM linkage - shell: bash - run: | - set -euo pipefail - lib="$(Rscript --vanilla -e 'cat(system.file("libs", package="rJava"))')" - if [[ "$RUNNER_OS" == Windows ]]; then - objdump -p "$lib/x64/rJava.dll" > "$RUNNER_TEMP/linkage.txt" - elif [[ "$RUNNER_OS" == macOS ]]; then - otool -L "$lib/rJava.so" > "$RUNNER_TEMP/linkage.txt" - else - ldd "$lib/rJava.so" > "$RUNNER_TEMP/linkage.txt" - fi - cat "$RUNNER_TEMP/linkage.txt" - if grep -Ei '(libjvm|DLL Name: jvm.dll)' "$RUNNER_TEMP/linkage.txt"; then exit 1; fi - - name: Compare src/jni.h with the JDK headers - shell: bash - run: Rscript --vanilla tests-integration/jni-abi.R "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" - - name: Build a native helper that creates a JVM outside rJava - shell: bash - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/jvmhost" - cp tests-integration/jvmhost.c "$RUNNER_TEMP/jvmhost/" - src="$PWD/src" - if [[ "$RUNNER_OS" == Windows ]]; then src="$(cygpath -m "$src")"; fi - (cd "$RUNNER_TEMP/jvmhost" && PKG_CPPFLAGS="-I$src" R CMD SHLIB jvmhost.c) - echo "RJE_JVMHOST=$(ls "$RUNNER_TEMP"/jvmhost/jvmhost.{so,dll} 2>/dev/null)" >> "$GITHUB_ENV" - - name: Check deferred loading in fresh processes - shell: bash - run: | - set -euo pipefail - run() { echo "::group::$*"; Rscript --vanilla tests-integration/deferred.R "$@"; echo "::endgroup::"; } - for scenario in select retry badinput unload preloaded nonascii fallback laterload hosted; do - run "$scenario" - done - run hosted explicit - run jengine - run preloaded global - run laterload b - run alias link - run alias spelling - if [[ "$RUNNER_OS" == Windows ]]; then run alias short; fi - # JVM libraries mapped under names no JVM uses, and a non-JVM library with JNI exports. - run renamed copy - run renamed hardlink - # A renamed jvm.dll cannot start a VM: JDK DLLs import jvm.dll by name. - if [[ "$RUNNER_OS" != Windows ]]; then run renamed symlink; run hosted renamed; fi - run decoy - # The file of a mapped JVM library is deleted, or replaced by B's. - run replaced deleted - run replaced replaced - run hosted replaced - - name: Unmap an unrelated library while mapped JVM discovery runs - if: runner.os == 'Linux' - shell: bash - run: | - set -euo pipefail - dir="$RUNNER_TEMP/dlrace" - mkdir -p "$dir" - # R's own flags show few warnings; the changed native code is checked with more. - cc="$(R CMD config CC)" - echo "::group::Warnings of src/djni.c with -Wall -Wextra" - $cc -O2 -Wall -Wextra -Isrc -c -o "$dir/djni.o" src/djni.c 2>&1 | tee "$dir/djni-warnings.txt" - echo "$(grep -c 'warning:' "$dir/djni-warnings.txt" || true) warnings" - echo "::endgroup::" - $cc -shared -fPIC -O2 -Wall -Wextra -Werror -o "$dir/dlrace.so" tests-integration/dlrace.c -ldl -lpthread - $cc -shared -fPIC -O2 -Wall -Wextra -Werror -DDLRACE_VICTIM -o "$dir/dlrace-victim.so" tests-integration/dlrace.c - LD_PRELOAD="$dir/dlrace.so" RJE_DLRACE="$dir/dlrace.so" RJE_DLRACE_VICTIM="$dir/dlrace-victim.so" \ - Rscript --vanilla tests-integration/deferred.R race - - name: Remove hard links left in the JDK directories - if: always() && env.RJE_JAVA_HOME_A != '' - shell: bash - run: | - # A mapped DLL cannot be deleted, so Windows leaves them behind until here. - find "$RJE_JAVA_HOME_A/" "$RJE_JAVA_HOME_B/" -name 'rjava-alias-*' -print -delete - - name: Embed R in a Java host through JRI - shell: bash - timeout-minutes: 5 - run: | - Rscript --vanilla tests-integration/jri.R - env RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R - - name: Check Windows DLL search state after loads - if: runner.os == 'Windows' - shell: bash - run: | - set -euo pipefail - work="$RUNNER_TEMP/dll-search" - server="$(dirname "$(Rscript --vanilla -e 'cat(rJava:::.find.jvm.library(Sys.getenv("RJE_JAVA_HOME_A"))[1])')")" - mkdir -p "$work/missing/bin/server" "$work/copy/bin/server" - cp "$server/jvm.dll" "$work/copy/bin/server/" - cp "$(dirname "$server")"/*.dll "$work/copy/bin/" - marker="$(dirname "$server")/java.dll" - cp "$marker" "$work/missing/bin/rjava_marker.dll" - cp "$marker" "$work/copy/bin/rjava_marker.dll" - gcc -Isrc -o "$work/dll-search.exe" tests-integration/dll-search.c - w() { cygpath -w "$1"; } - PATH="/c/Windows/System32:/c/Windows" "$work/dll-search.exe" \ - "$(w "$work/missing/bin/server/jvm.dll")" "$(w "$work/copy/bin/server/jvm.dll")" "$(w "$work/missing/bin")" - - name: Verify bundled runtimes with installed system DLLs - if: runner.os == 'Windows' - shell: pwsh - run: ./tests-integration/windows-msvc.ps1 -Mode normal - - name: Verify startup and host attachment without system VC++ DLLs - if: runner.os == 'Windows' - shell: pwsh - run: ./tests-integration/windows-msvc.ps1 -Mode missing - - name: Verify startup and host attachment with VC++ 2015 in System32 - if: runner.os == 'Windows' - shell: pwsh - run: ./tests-integration/windows-msvc.ps1 -Mode old - - name: Check Windows JRI failure and opt-out behavior - if: runner.os == 'Windows' && matrix.set == 'current' - shell: bash - run: bash tests-integration/jri-optional.sh - - uses: actions/checkout@v4 - if: matrix.downstream - with: - repository: e-kotov/rJavaEnv - ref: 693b87c67f8c521388c3fd8627c8afbc29d8766a - path: downstream-source - - uses: r-lib/actions/setup-r-dependencies@v2 - if: matrix.downstream - with: - working-directory: downstream-source - dependencies: '"hard"' - extra-packages: any::callr - install-quarto: false - - name: Test installed rJavaEnv and downstream packages with dynamic rJava - if: matrix.downstream - working-directory: downstream-source - shell: Rscript {0} - run: source("tools/test-downstream.R") - - name: Install xlsx test dependencies in a separate library - if: matrix.downstream - shell: bash - run: | - set -euo pipefail - Rscript tests-integration/xlsx-install.R "$RUNNER_TEMP/xlsx-lib" "$RUNNER_TEMP/xlsx-src" - { - echo "RJE_RJAVA_LIB=$R_LIBS" - echo "R_LIBS=$R_LIBS${{ runner.os == 'Windows' && ';' || ':' }}$RUNNER_TEMP/xlsx-lib" - } >> "$GITHUB_ENV" - - name: Start Java from a CRAN package's .onLoad (xlsx) - if: matrix.downstream - shell: bash - run: Rscript --vanilla tests-integration/xlsx.R - - name: Check the CRAN xlsx package and its tests with dynamic rJava - if: matrix.downstream - shell: bash - env: - _R_CHECK_FORCE_SUGGESTS_: 'false' - run: | - set -euo pipefail - cd "$RUNNER_TEMP/xlsx-src" - # JDK B is not the build JDK on PATH. NOAWT makes rJava set AWT headless. - JAVA_HOME="$RJE_JAVA_HOME_B" R CMD check --no-manual --ignore-vignettes xlsx_*.tar.gz - cat xlsx.Rcheck/tests/testthat.Rout* | tail -n 20 - - uses: actions/upload-artifact@v4 - if: always() - with: - name: dynamic-${{ matrix.os }}-${{ matrix.set }} - path: | - ${{ runner.temp }}/rJava_*.tar.gz - ${{ runner.temp }}/linkage.txt - musl: - # musl libc with Alpine's R and OpenJDK packages. Docker runs the same - # script on x86_64 and arm64 hosts; the container root is overlayfs on the - # runner's kernel, as in a typical Docker deployment. - name: alpine ${{ matrix.arch }} ${{ matrix.set }} (openjdk ${{ matrix.a }}, openjdk ${{ matrix.b }}) - strategy: - fail-fast: false - matrix: - include: - - { arch: x86_64, runner: ubuntu-24.04, set: legacy, a: '8', b: '11' } - - { arch: x86_64, runner: ubuntu-24.04, set: current, a: '17', b: '21' } - - { arch: aarch64, runner: ubuntu-24.04-arm, set: current, a: '17', b: '21' } - runs-on: ${{ matrix.runner }} - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Check deferred loading, JRI and host attachment on musl - env: - # alpine:3.24.2 - IMAGE: alpine@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 - run: | - docker run --rm -v "$PWD:/src:ro" "$IMAGE" \ - sh -c 'apk add --no-cache bash && exec bash /src/tests-integration/musl.sh "$1" "$2" "$3"' \ - sh '${{ matrix.a }}' '${{ matrix.b }}' '${{ matrix.arch }}' diff --git a/.github/workflows/overlay.yaml b/.github/workflows/overlay.yaml deleted file mode 100644 index 042e986..0000000 --- a/.github/workflows/overlay.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Overlayfs kernel compatibility -on: - push: - branches: [feature/dynamic-cross-platform] - workflow_dispatch: -permissions: - contents: read -jobs: - overlay: - name: Overlayfs (${{ matrix.release }}, ${{ matrix.expect }}) - runs-on: ubuntu-24.04 - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - {release: jammy, expect: legacy} - - {release: jammy, expect: distro} - - {release: noble, expect: current} - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Install VM tools - run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends qemu-system-x86 qemu-utils cloud-image-utils - - name: Check a real guest kernel and overlay mount - run: bash tests-integration/overlay-vm.sh '${{ matrix.release }}' '${{ matrix.expect }}' - - uses: actions/upload-artifact@v4 - if: always() - with: - name: overlay-${{ matrix.release }}-${{ matrix.expect }} - path: ${{ runner.temp }}/overlay-${{ matrix.release }}/serial.log diff --git a/mkdist b/mkdist index 4aaed03..503a1be 100644 --- a/mkdist +++ b/mkdist @@ -178,7 +178,7 @@ VER=`sed -n -e 's/.*rJava v\([0-9.-]\{1,\}\).*/\1/p' src/rJava.h` echo "rJava version ${VER}" cat DESCRIPTION| sed "s/Version:.*/Version: ${VER}/" > d mv d DESCRIPTION -rm -rf tests tests-integration +rm -rf tests if test -n "${OPTIMIZED}"; then echo "Creating optimized sources ..." echo '#define ONEFILE 1' > src/all diff --git a/tests-integration/JriAttach.java b/tests-integration/JriAttach.java deleted file mode 100644 index 70bd435..0000000 --- a/tests-integration/JriAttach.java +++ /dev/null @@ -1,69 +0,0 @@ -import org.rosuda.JRI.REXP; -import org.rosuda.JRI.Rengine; - -/* A Java application hosts the JVM and embeds R through JRI. rJava loaded in - that R must attach to the running JVM, not select the JAVA_HOME runtime. */ -public class JriAttach { - public static void main(String[] args) { - try { - run(); - } catch (Throwable e) { - e.printStackTrace(); - } - System.exit(1); /* the R thread is not a daemon, so always exit */ - } - - static void run() { - Rengine engine = new Rengine(new String[] { "--vanilla", "--no-echo" }, false, null); - if (!engine.waitForR()) { - System.err.println("FAIL: cannot start embedded R"); - System.exit(2); - } - /* An explicit path must name the mapped file: JAVA_HOME's runtime is rejected. */ - REXP result = engine.eval("tryCatch({" + - " library(rJava);" + - " p <- rJava:::.find.jvm.library(Sys.getenv('JAVA_HOME'))[1];" + - " b <- try(.jloadJVM(p), silent = TRUE);" + - " maps <- if (file.exists('/proc/self/maps')) unique(grep('libjvm', readLines('/proc/self/maps'), value = TRUE)) else '';" + - " if (!inherits(b, 'try-error') || .jvmState()$state != 'not-loaded')" + - " stop('JAVA_HOME runtime was selected: ', p, ': ', if (inherits(b, 'try-error')) trimws(b) else 'loaded'," + - " '; state ', .jvmState()$state, '; mapped ', paste(unlist(.Call(rJava:::RgetMappedJVM)), collapse = ' ')," + - " '; maps ', paste(maps, collapse = ' | '));" + - " .jinit();" + - " paste('OK', .jvmState()$state, .jcall('java/lang/System', 'S', 'getProperty', 'java.home')," + - " !is.jnull(.jengine()), sep = '|')" + - "}, error = function(e) paste('ERROR', conditionMessage(e), sep = '|'))"); - String text = result == null ? "ERROR|no result" : result.asString(); - String expected = "OK|attached|" + System.getProperty("java.home") + "|TRUE"; - System.out.println("JRI host " + System.getProperty("java.version") + ": " + text); - if (System.getProperty("os.name").startsWith("Windows")) { - final String[] interrupted = new String[1]; - Thread evaluator = new Thread(new Runnable() { - public void run() { - REXP value = engine.eval("tryCatch({Sys.sleep(60); 'MISSED'}, interrupt=function(e) 'INTERRUPTED')"); - interrupted[0] = value == null ? "NULL" : value.asString(); - } - }); - evaluator.setDaemon(true); - evaluator.start(); - try { - Thread.sleep(1000); - engine.rniStop(0); - evaluator.join(10000); - } catch (InterruptedException e) { throw new RuntimeException(e); } - if (evaluator.isAlive() || !"INTERRUPTED".equals(interrupted[0]) || - engine.eval("as.integer(1+1)").asInt() != 2) { - System.err.println("FAIL: JRI interrupt/recovery: " + interrupted[0]); - System.exit(3); - } - System.out.println("PASS jri-interrupt-recovery"); - } - engine.end(); - if (!expected.equals(text)) { - System.err.println("FAIL: expected " + expected); - return; - } - System.out.println("PASS jri-host"); - System.exit(0); - } -} diff --git a/tests-integration/README.md b/tests-integration/README.md deleted file mode 100644 index f1074c7..0000000 --- a/tests-integration/README.md +++ /dev/null @@ -1,386 +0,0 @@ -# Deferred JVM loading experiment - -This branch ports Simon Urbanek's `feature/dynamic` work onto upstream master -(`534ff33`) and includes Egor Kotov's earlier Windows loader work. It is an -experimental fork, not a released rJava API contract. Green CI jobs show that -the cases below work on hosted runners. They do not establish production -safety for every system. - -## Contract - -Loading the rJava namespace, including through another package's Imports, -does not select, load, or initialize Java. The first `.jinit()` selects a -JVM library in this order: - -1. A JVM already created in the process, e.g. by a Java application - embedding R through JRI: rJava attaches to it whatever Java home is - configured. That is the only runtime the process can use, and rJava - linked against the JVM library behaved the same way. -2. `getOption("java.home")` (Windows only, as before) or `JAVA_HOME`. A value - that is not an existing directory is ignored with the upstream warning, - as rJava did on Windows and macOS before deferred loading. -3. Windows: `CurrentVersion` of the JavaSoft registry keys (HKLM, then HKCU). -4. macOS: `/usr/libexec/java_home`. -5. The `java.home` property of `java` on `PATH`. - -Within a Java home, known layouts are checked first (`lib/server`, -`jre/lib//server`, `jre/lib/server`, `bin/server`, `jre/bin/server`), -then a recursive search. An existing Java home without a JVM library is an -error. Every failure before a library is loaded leaves the state -`"not-loaded"`, so the setting can be corrected and `.jinit()` retried. - -If a JVM library is already mapped into the process, a path given to -`.jloadJVM()`, or found from a Java home, selects it only if it names the -mapped file, whether or not that library has created a VM. djni finds mapped JVM libraries among -all images the dynamic loader lists (`dl_iterate_phdr` on Linux, dyld on -macOS, `EnumProcessModules` on Windows), without loading anything. An image -is a JVM library if it has a JVM library name (`libjvm.so`, `libjvm.dylib`, -`jvm.dll`), even if malformed, or whatever its name if it defines itself -`JNI_CreateJavaVM`, `JNI_GetCreatedJavaVMs` and `JVM_CurrentTimeMillis`. -"Defines itself" excludes definitions inherited from a dependency and -forwarded Windows exports. The exports are read without a handle where a -handle may be unavailable: on Linux from the image's dynamic symbol table in -memory (located through its `PT_DYNAMIC` segment and hash table, as the -dynamic loader reads it), on Windows from the export table in memory. On -macOS, `dlsym()` on a `RTLD_NOLOAD` handle must return an address inside the -image. On Linux, an image whose table cannot be read inside its own readable -segments makes discovery fail, rather than being taken for no JVM. Every -image, Java or not, is read only inside the `dl_iterate_phdr` callback: -glibc holds its loader lock until `dl_iterate_phdr` returns, and `dlclose` -unmaps an image only while holding it (`elf/dl-iteratephdr.c`, -`elf/dl-close.c`), so another thread may unmap any image right after. Only -the JVM images are kept: their name, their dynamic section and first segment -as addresses that are compared but not read, and their -`JNI_GetCreatedJavaVMs`, called only while a reference taken afterwards -(below) holds the image mapped. musl never unmaps an image (its `dlclose` -only checks the handle), so there the function is called without one. JNI -exports alone do not suffice: -rJava's own wrappers have them, and so does the macOS `libjli`, which -forwards them to libjvm. Every HotSpot JDK in the matrix exports the three -symbols; CI checks this for each runtime. - -Identity is then the file, not its name: device and inode on Linux and -macOS, volume serial and file ID on Windows. The mapped side is the file the -image was mapped from, not the file now at the name the loader recorded, -which may have been deleted or replaced since: on Linux the mapping's entry -in `/proc/self/maps`, on macOS the vnode of the mapped region -(`proc_pidinfo` with `PROC_PIDREGIONPATHINFO`), on Windows the file -`GetMappedFileNameW` names for the module, opened by its device path. So an -upgrade that replaces the loaded library's file is not taken for the new -runtime, and the same file is still accepted under any of its names. -Symbolic links, junctions, -hard links, 8.3 short names, other spellings and renamed copies of the -loaded library are recognized: the same file is accepted, and djni -references the mapped image rather than loading the alias. On Windows the -same file is rejected if mapped under another module name than `jvm.dll`: -the JDK's DLLs import `jvm.dll` by name, and the loader matches loaded -modules by base name, so starting a VM would load another `jvm.dll` or -fail. A different or -unverifiable file is rejected, and so is any selection while more than one -JVM image is mapped (`RgetMappedJVM` then reports one of them with `vms` -`-1`). If the loaded images cannot be listed, selection fails and -`RgetMappedJVM` signals an error, so callers do not mistake it for "none". - -### Explicit selection and attaching - -Selecting a path and attaching to a created VM are separate operations. -`.jloadJVM(path)` always compares files as above; no name is exempt, not -even the one `RgetMappedJVM` reports for a created VM, since the file now -at that name may be another runtime. `.jinit()` instead attaches through -the internal `RattachJVM`, which takes no path: it requires exactly one -mapped JVM image, with at least one created VM, and selects that image -through a reference to it, without loading anything. So it attaches even if -the file at the image's recorded name was deleted or replaced, or cannot be -identified (overlayfs, below), as long as a reference can be taken (below -for musl). It never selects an -image without a created VM, never resolves an ambiguity between several -images, and fails if the image lacks a JNI entry point. On Windows it does -not apply the `jvm.dll` name rule, which concerns starting a VM (untested: -the JDK cannot start a VM from a renamed `jvm.dll`). The native attach entry -point has no exported R wrapper. - -## The JVM library name on Linux - -The JDK's own libraries (`libjava`, `libjimage`, `libzip`, ...) depend on -`libjvm.so` by name, with only their own directory (`$ORIGIN`) as run path, -not `server`. glibc matches that name to the loaded JVM library by its -SONAME. musl does not: it matches only a library it found by searching, and -searches `LD_LIBRARY_PATH` as set when the process started, then its system -path. With a JVM library selected by path, the name is then not found, or -names another runtime. R's `etc/ldpaths` puts `R_JAVA_LD_LIBRARY_PATH` on -`LD_LIBRARY_PATH`, by default `lib/server` of `JAVA_HOME` at R startup or of -the `R CMD javareconf` JDK. On Alpine, with JDK 17 configured, `.jinit()` -with `JAVA_HOME` naming another runtime failed while the VM loaded the JDK's -libraries: `libjava.so: JVM_DoPrivileged: symbol not found` with OpenJDK 11, -consistent with binding to JDK 17's `libjvm.so`, which lacks that symbol, -and `Unable to load jimage library` with OpenJDK 21. - -So on Linux, after loading the library and its JNI entry points, djni -resolves `libjvm.so` with `RTLD_NOLOAD`, which loads nothing, and refuses the -selection unless the name resolves to the selected library. The refusal -happens before a VM is created and leaves the state `"not-loaded"`, but musl -never unmaps a library, so it remains mapped and blocks other selections: use -a new R process started with `JAVA_HOME` naming the runtime, or -`R_JAVA_LD_LIBRARY_PATH` naming its server directory (needed for the JDK 8 -layout). On glibc the name resolves through the SONAME, so the check passes -in every glibc scenario above, including renamed and replaced names. -Changing `LD_LIBRARY_PATH` in a running process, as `deferred.R` does, does -not change musl's search path. A Java application -embedding R through JRI is not affected: for this reason the JDK launcher -built for musl sets `LD_LIBRARY_PATH` and re-executes itself -(`RequiresSetenv` in OpenJDK's `java_md.c`), and attaching checks no names. - -musl also resolves `dlopen(name, RTLD_NOLOAD)` for a path by opening that -file and matching its device and inode (`load_library` in musl's -`ldso/dynlink.c`). A recorded name whose file was deleted or replaced then -yields no reference, or one to another mapped library. Earlier, djni read the -exports of an image through that reference, so on musl a JVM library mapped -under a name no JVM uses was no longer identified once its file was deleted or -replaced: in `replaced deleted` with B's server directory on the startup -library path, `.jloadJVM()` of B mapped B's library next to A's and succeeded, -as the name check then found B; with A's there, the name check refused B, but -only after mapping it. Now the exports are read from memory (above), so the -image is still identified and B is refused before anything is loaded. A -reference is taken only through a name the dynamic loader resolves to the -image itself, verified with `dlinfo(RTLD_DI_LINKMAP)` against the image's -dynamic section: its recorded name, then `libjvm.so` (the name the JDK's -libraries bind to), and for `.jloadJVM()` the path given, which must name the -mapped file. If none resolves to the image, selecting or attaching it is -refused (`Cannot reference the JVM library ...`) before a VM is created. The -`replaced` scenarios run on musl with A's and with B's server directory on -the startup library path, and check that nothing from B's home is mapped -after the refusals; with B's, A is refused too, by the name check. -`hosted replaced` runs with A's only: with B's, the host's own VM would bind -A's JDK libraries to B's `libjvm.so` before rJava is involved. - -## Selected libraries are not unloaded - -`.junloadJVM()` errors whenever a JVM library is loaded, even before a VM was -created. This is a deliberate safety boundary, separate from the Windows port -(commit "fix: retain JVM library selection until the R process exits"). -`dlclose()`/`FreeLibrary()` success does not mean the library was unmapped: -macOS dyld never unloads images with thread-local variables, and libjvm has -them. In local macOS probes, loading Corretto 17 or Temurin 27, unloading it -before any VM was created, then starting another JDK failed with symbols -resolved against the first library. After `DestroyJavaVM`, creating a VM -from the same library failed (`JNI_ERR`), and switching from Corretto 17 to -Temurin 17 crashed in libjvm. -Use a new R process to run another JVM. `djni_unload()` remains only to -clean up a library that lacks the JNI entry points, and for Simon's -standalone `DJNI_MAIN_TEST` probe. - -## Windows DLL search state - -To load `jvm.dll`, djni adds the JDK `bin` directory (the parent of -`bin\server`, needed for JDK 8's C runtime) with `AddDllDirectory` only for -that `LoadLibraryExW` call, then removes it after success or failure. `PATH` -is never changed. `dll-search.c` checks the removal with a positive control. - -Later native loads by the JVM (`zip`, `net`, `nio`, `management`, -`sunmscapi`, `awt`, `fontmanager`, `freetype`) also need the JDK's MSVC runtime -DLLs to be resolvable by basename. After verifying the JNI entry points, -both explicit selection and host attachment retain the selected runtime's -MSVC DLLs. An already loaded copy is retained with `GetModuleHandleExW`; -it cannot safely be replaced. Otherwise a copy in the selected JDK's `bin` -is preferred, and System32 is used only when that bundled copy is absent. -This follows the normal `java.exe` preference for its own directory and -avoids selecting System32's older `msvcp140` after startup has already loaded -the JDK's `vcruntime140`. References last until process exit. No persistent -search directory or `PATH` change is made. A bundled DLL that exists but -fails to load is diagnosed on stderr rather than silently falling back to -a different copy. Missing optional DLLs are allowed; JDKs that omit a required -runtime still need a suitable redistributable. - -The base VC++ DLLs and bundled `msvcp140_1`, `_2` and `_atomic_wait` satellites -are covered. Already loaded copies retain host-process precedence: this can -bind Java to a host's incompatible older runtime. Use a fresh process with -a compatible runtime in that case; the loader does not replace live DLLs. - -Every Windows job tests normal System32 DLLs, their absence, and genuine -Microsoft VC++ 2015 Update 3 (14.0.24212.0) DLLs temporarily placed in System32. -The old installer is downloaded from Microsoft's distribution site and -pinned by SHA256; only its DLLs are extracted, and file versions must prove -they are older than each selected JDK's bundled copies. A clean-process -control loads the old System32 DLL. `msvc.R` then checks startup, late native -loads after host attachment, and actual loaded module paths for both JDKs. -The native host fixture selects its startup dependencies with a scoped JDK -`bin` search; rJava must resolve late dependencies independently after that -search scope has ended. A separate case releases the original reference to a preloaded MSVC DLL and -checks rJava's retained reference keeps it mapped. Java-host JRI also runs -without system DLLs and with the old copies. Files are restored in `finally`, -including on a failed test. These mutations occur only on disposable Actions -runners. - -## Windows JRI builds - -`configure.win` now builds JRI at its current location, `src/jri`, and -installs `JRI.jar` and `jri.dll` alongside rJava. The Windows Makefiles set -explicit default targets so R's included Makeconf cannot select an unrelated -rule. Windows uses its own console declaration instead of the Unix-only -`Rinterface.h`; the x64 build also enables JRI's existing Win64 branches. - -JRI is built by default, and a build failure stops installation. To omit it -explicitly, use `JRI=0 R CMD INSTALL rJava_.tar.gz`; normal rJava/JNI -functionality remains available. `IGNORE=1 R CMD INSTALL ...` instead attempts -JRI and permits installation if that optional build fails. CI forces a real -`javac` failure to check the default failure and `IGNORE=1` fallback, and tests -the explicit `JRI=0` path. A full JDK is needed to build JRI. - -`WIN64` is derived from the compiler's `_WIN64`, independently of `R_ARCH`. -The Windows import declaration for `UserBreak` comes from R's header on R -4.6+ and a compatibility declaration on older R. Generated JNI headers are -normalised to LF, avoiding a Windows-only line-ending check NOTE. The Java -host test interrupts a sleeping R evaluation through `rniStop` and requires -both interrupt delivery and subsequent R evaluation to succeed. - -Every Windows runtime pair now runs `.jengine()` from R and a Java host -embedding R through JRI. The latter runs with both JDKs as host, with -`JAVA_HOME` deliberately naming the other runtime. The test requires rJava -to reject that other runtime and attach to the host's already running JVM. - -## Overlayfs kernel checks - -`overlay.yaml` boots real QEMU/KVM guests; a container alone would reuse the -runner's kernel. Ubuntu image snapshots and the frozen mainline kernel are -pinned by SHA256. Reboot readiness checks the boot ID rather than sleeping -for a fixed duration. Each guest records its actual kernel and mounts. - -Each kernel exercises four layouts: all layers on one filesystem, separate -ext4 filesystems with `xino=off`, separate ext4 filesystems with `xino=on`, -and a rootless mount created in an unprivileged user's user namespace. -Tests on privileged mounts run as the unprivileged user too. Each layout -covers a fresh JVM, a preloaded library, a host-created JVM, and replacement -of the mapped file, in separate R processes. Module identities and `stat()` -identities are logged; assertions distinguish a definite different-file -refusal from an unverifiable overlay identity. - -The kernels are Ubuntu 22.04's 5.15, Ubuntu 24.04's 6.8, and frozen mainline -6.1.0 (`6.1.0-060100.202303090726`). Older kernels with the mapping-identity -limitation reject ambiguous same-file reuse, while fresh startup and host -attachment work. Cross-filesystem `xino=off` also produces a device mismatch on these modern -kernels, so preloaded path selection is conservatively refused there too. A -device mismatch is unverifiable, while equal devices with -different inodes are definitely different files. A distro version below -6.8 alone does not predict the behavior, so fixtures assert the expected -behavior for the actual layout and record the kernel. - -## Tests - -Every scenario runs in a new R process with `PATH` reduced to system and R -directories and `JAVA_HOME` set explicitly, so the runner's Java setup cannot -mask discovery or dependency failures. `dynamic.yaml` builds a source -distribution with `sh mkdist`, installs it in an isolated library, and runs: - -| Check | What it shows | -|---|---| -| linkage | `rJava.so`/`rJava.dll` has no JVM library dependency | -| `jni-abi.R` | `src/jni.h` type sizes, invocation struct fields and every function table offset match each JDK's `jni.h` | -| `select`, `retry`, `badinput` | selection after namespace load; recovery after an empty home, missing file, library without JNI exports, empty, over-long and relative paths | -| `unload` | unloading or changing a selected, unstarted library fails | -| `preloaded`, `preloaded global` | a library mapped outside rJava (also with global symbols) rejects another runtime and permits the same one; `RattachJVM` fails with nothing mapped and with no created VM | -| `alias link/spelling/short` | the same preloaded library through a symbolic link or junction with spaces and non-ASCII characters, an alternative spelling, and an 8.3 name (Windows) | -| `renamed copy/hardlink/symlink` | a JVM library mapped under a name no JVM uses: a copy of B rejects A and B's own library; a hard link of A in its directory (and a symbolic link, Linux and macOS) rejects B, then the same file starts A (Windows: is rejected as mapped under another name) | -| `hosted renamed` | native code creates the VM from the hard link; it is reported with one VM, B is rejected, and `.jinit()` attaches (Linux and macOS) | -| `replaced deleted/replaced` | the file of the mapped library is deleted, or replaced by a hard link of B's (Windows: a mapped DLL cannot be deleted, so it is renamed in a copy of A's home and B's linked as `jvm.dll`): B, the replaced name and `.jinit()` with `JAVA_HOME` naming B are rejected, and on Linux nothing from B's home is then mapped; A's own file is accepted, and after replacement A starts (musl with B's server directory on the startup library path: A is refused by the name check instead, and B is still not mapped) | -| `hosted replaced` | the same replacement after a host created the VM (Windows: renamed in a copy of A's home, as above): `.jloadJVM()` rejects B, the replaced name and the name `RgetMappedJVM` reports, nothing from B's home is mapped (Linux), all before `.jinit()` attaches to A | -| `decoy` | the JDK's `jli` library (JNI exports on macOS, no `JVM_` exports) is not reported and does not block selecting B | -| `nonascii` | loading through such a path without a preload | -| `byname` | musl, with A's server directory on the startup library path: selecting B is refused before a VM exists, A's library is not mapped, and the B library that musl keeps mapped then blocks A | -| `fallback` | invalid `JAVA_HOME` warns and falls back to an independently computed home (HKCU registry entry on Windows, `java_home` on macOS, `java` on PATH on Linux) | -| `laterload` | the later native loads above | -| `hosted`, `hosted explicit` | native code creates a JVM, then `.jinit()` attaches to it although `JAVA_HOME` names the other JDK; `.jloadJVM()` rejects B, and with `explicit` selects A's own file first | -| `race` | Linux, with `dlrace.c` preloaded to interpose `dl_iterate_phdr`: an unrelated library is mapped before rJava lists the images and unmapped right after (deterministic), and mapped and unmapped 20000 times on another thread while discovery runs repeatedly (stress, which cannot prove the absence of a race): with nothing mapped, a preloaded A, and a VM a host created from it, discovery reports A with 0 and 1 VMs, B is refused and not mapped, attaching fails without a VM, and `.jinit()` attaches, with one `libjvm.so` mapped at the end. Not run on musl, which never unmaps a library | -| every scenario | before anything is mapped, `RgetMappedJVM` is `NULL`: rJava's own JNI wrappers are not taken for a JVM | -| `jengine`, `jri.R` | JRI started from R; a Java host embedding R, where `.jloadJVM()` rejects the `JAVA_HOME` runtime and rJava attaches to the host JVM (all three platforms, both host JDKs) | -| `dll-search.c` | Windows DLL search state after failed and successful loads | -| `msvc.R` | Windows normal/missing/2015 system runtimes, retained references, fresh startup and hosted late loads; loaded runtime origins | -| `overlay.R` | actual 5.15, 6.1.0 and 6.8 kernels; four overlay layouts, unprivileged processes, exact refusal classes | -| downstream | rJavaEnv and two minimal consumer packages (current pair only) | -| `xlsx.R`, xlsx check | the CRAN package xlsx, whose `.onLoad()` starts Java through `.jpackage()`: loading it selects `JAVA_HOME` (JDK B, not the build JDK on `PATH`), another runtime is then rejected, and a workbook round trip works; `R CMD check` of the CRAN source package runs its testthat suite (current pair only) | -| `musl.sh` | the Linux scenarios above, `byname` and `jri.R` on Alpine, each scenario with the server directory of the runtime it starts on the startup library path; `replaced deleted/replaced` also with B's, `hosted replaced` with A's only (above) | - -Bounded matrix, each on Linux (x86_64 and arm64), macOS (arm64) and Windows -with R release; the build JDK is Temurin 17. On Linux arm64, JDK 8 has the -`jre/lib/aarch64/server` layout: - -| Set | JDK A | JDK B | -|---|---|---| -| legacy | Temurin 8 (Zulu 8 on macOS) | Temurin 11 | -| current | Temurin 17 | Temurin 21 | -| vendor | Corretto 11 | Zulu 25 | - -An additional Windows R 4.5 job runs the same native, JRI and VC++ checks -with Temurin 17/21, including the console declarations for R before 4.6. An -Ubuntu 24.04 job runs the native and JRI checks with R 3.6.3, the oldest R -that `DESCRIPTION` accepts, and Temurin 17/21. - -The musl jobs run `musl.sh` in Docker from a pinned `alpine:3.24.2` image: -musl 1.2.6, Alpine's R 4.6.0 and its OpenJDK packages. Their libraries must -link `libc.musl`, not a glibc compatibility layer. The JDKs are installed in -the container's overlayfs root on the runner's current kernel, as in a -typical Docker deployment: - -| Set | Machine | JDK A | JDK B | -|---|---|---|---| -| legacy | x86_64 | OpenJDK 8 | OpenJDK 11 | -| current | x86_64 | OpenJDK 17 | OpenJDK 21 | -| current | aarch64 | OpenJDK 17 | OpenJDK 21 | - -The upstream `check.yaml` matrix (R devel, Zulu 8 and 11, five runner images -including Intel macOS) also runs on this branch. - -To run locally, build with `sh mkdist`, install the tarball in a temporary -library, set `R_LIBS` to it and `RJE_JAVA_HOME_A`/`RJE_JAVA_HOME_B` to two JDK -homes, and run `Rscript --vanilla tests-integration/deferred.R ` -from the repository root, one process per scenario. - -## Limits - -- Namespace loading, JVM library loading and JVM startup are different - boundaries. `.jloadJVM()` fixes the selection even before `.jinit()`. A - package that calls `.jinit()` in `.onLoad()` still selects Java when it - loads. -- Attaching to a JVM created by a host requires rJava's `java/boot` on the - host's class path on JDK 9+, as before: rJava cannot add it to a running - application class loader there. -- Mapped JVM discovery covers Linux, macOS and Windows only; elsewhere djni - lists no mapped libraries, so it cannot reject a conflict. It cannot see libraries - in another `dlmopen()` namespace, and identifies a renamed JVM only by - the exports above (a JVM without `JVM_CurrentTimeMillis` must keep its - usual name). If the mapped file cannot be identified (no `/proc` on - Linux, a failed query), selection is rejected. On overlayfs (e.g. Docker), - kernels without the mapping-identity fix can report the layer file's - device/inode in `/proc/self/maps` while `stat()` reports the overlay's. - `.jloadJVM(path)` then rejects a mapped library as unverifiable, even when - it is the same file, with or without a created VM. A new R process can - select it normally; `.jinit()` attaching to a created VM compares no files. - This is exercised on the frozen mainline 6.1.0 guest, alongside successful - reuse on the current Ubuntu 5.15 and current-kernel guests. dyld - records a library loaded through a symbolic link by its target, so on - macOS the `symlink` variant shows the target name. -- On Linux, a reference to a JVM image is taken after `dl_iterate_phdr` - returns. It is the image's own only by comparing its dynamic section - address (`RTLD_DI_LINKMAP`), so an image unloaded meanwhile and another - loaded under its name with its dynamic section at that same address would - be taken for it. On macOS, `_dyld_get_image_name` can race with a - concurrent unload, a window of the dyld API that djni does not close. -- On musl, a process can start only the runtime whose server directory was on - `LD_LIBRARY_PATH` when it started (above). Other runtimes are refused, not - supported, and the refused library stays mapped, so retrying in the same - process with another runtime is refused too: start a new R process. A JVM - library mapped under a name that no longer opens it is identified, but can - be selected or attached only through a name that still resolves to it - (above); otherwise rJava refuses it. -- Not covered: 32-bit Windows; Windows on ARM (the linkage, JRI and VC++ - fixtures assume x64, and the old VC++ runtime test downloads x64 DLLs); - OpenJ9 and other non-HotSpot VMs, which the export-based identification - above does not describe; Java runtimes without a JDK layout; musl JDKs - other than Alpine's packages (e.g. Temurin's Alpine builds), and JDK 8 on - musl arm64; Intel macOS in this matrix (only in `check.yaml`); R versions - other than release, Windows 4.5, Linux 3.6.3 and devel in `check.yaml`; - and downstream packages beyond rJavaEnv, the two test consumers and xlsx. - -Modifying a mapped JVM library in place without changing its device/inode is -outside the identity guarantee, as for other native libraries. Replace runtime -files atomically and use a fresh process after an upgrade. The tests exercise -unlink/replacement, not arbitrary writes to live native-code mappings. diff --git a/tests-integration/deferred.R b/tests-integration/deferred.R deleted file mode 100644 index fba2f4c..0000000 --- a/tests-integration/deferred.R +++ /dev/null @@ -1,579 +0,0 @@ -# Each scenario must run in a fresh R process against the installed source build: -# Rscript --vanilla tests-integration/deferred.R [variant] -# RJE_JAVA_HOME_A and RJE_JAVA_HOME_B must name two different Java homes. -# RJE_BYNAME (musl only) is A or B: the runtime whose server directory was on -# LD_LIBRARY_PATH when R started, the libjvm.so the JDK's libraries bind to. -args <- commandArgs(TRUE) -scenario <- args[1] -variant <- if (length(args) > 1L) args[2] else "" -windows <- .Platform$OS.type == "windows" -home_a <- Sys.getenv("RJE_JAVA_HOME_A") -home_b <- Sys.getenv("RJE_JAVA_HOME_B") -byname <- Sys.getenv("RJE_BYNAME") -stopifnot(dir.exists(home_a), dir.exists(home_b)) -log <- function(...) cat(scenario, variant, ":", ..., "\n") - -# Runner tool setup must not mask missing discovery or dependency handling: -# no Java directory is on PATH unless a scenario adds one. -sysroot <- Sys.getenv("SystemRoot", "C:\\Windows") -minimal_path <- if (windows) { - paste(normalizePath(c(R.home("bin"), file.path(sysroot, "System32"), sysroot, - file.path(sysroot, "System32", "Wbem")), winslash = "\\"), collapse = ";") -} else "/usr/bin:/bin" -Sys.setenv(PATH = minimal_path, JAVA_HOME = file.path(tempdir(), "missing-jdk")) -Sys.unsetenv(c("CLASSPATH", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS")) -options(java.parameters = "-Djava.awt.headless=true") -ns <- loadNamespace("rJava") -internal <- function(name) get(name, ns) -state <- function() internal(".jvmState")() -mapped_jvm <- function() .Call(internal("RgetMappedJVM")) -# rJava's own JNI wrappers (exported by rJava.dll) must not count as a JVM. -stopifnot(identical(state()$state, "not-loaded"), !state()$initialized, - identical(minimal_path, Sys.getenv("PATH")), is.null(mapped_jvm())) - -lib <- function(home) { - paths <- internal(".find.jvm.library")(home) - stopifnot(length(paths) >= 1L, grepl("/server/", paths[1], fixed = TRUE)) - normalizePath(paths[1], winslash = "/") -} -relative <- function(home) substring(internal(".find.jvm.library")(home)[1], nchar(home) + 2L) -same_home <- function(actual, expected) { - # JDK 8 reports its jre subdirectory as java.home. - actual <- normalizePath(actual, winslash = "/") - expected <- normalizePath(expected, winslash = "/") - identical(actual, expected) || identical(dirname(actual), expected) -} -fails <- function(expr, pattern = NULL) { - path <- Sys.getenv("PATH") - result <- suppressWarnings(try(expr, silent = TRUE)) - stopifnot(inherits(result, "try-error"), state()$state == "not-loaded", - identical(path, Sys.getenv("PATH"))) - if (!is.null(pattern) && !grepl(pattern, result)) - stop("unexpected error: ", result) - log("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L)) -} -# Linux: a refused selection must not have mapped anything from that home, -# not even a JVM library that is never started. -unmapped <- function(home) { - if (Sys.info()[["sysname"]] != "Linux") return(invisible()) - maps <- grep("^(\\S+\\s+){5}/", readLines("/proc/self/maps"), value = TRUE) - files <- unique(sub("^(\\S+\\s+){5}", "", maps)) - hit <- files[startsWith(files, paste0(normalizePath(home), "/"))] - if (length(hit)) stop("mapped from ", home, ": ", paste(hit, collapse = ", ")) - log("nothing mapped from", home) -} -# Map a JVM library as other native code in the process would, outside rJava. -preload <- function(path, local = TRUE, bin = dirname(dirname(path))) { - old <- Sys.getenv("PATH") - # JDK 8 jvm.dll needs the C runtime from its bin directory. - if (windows) Sys.setenv(PATH = paste(normalizePath(bin, winslash = "\\"), old, sep = ";")) - dyn.load(path, local = local, now = TRUE) - Sys.setenv(PATH = old) -} -# Another name for the JVM library of a home, with a basename no JVM uses: -# a hard link in its own directory, from which the JVM still finds its home. -# A mapped DLL cannot be deleted, so Windows removes earlier links first. -alias_lib <- function(home) { - real <- lib(home) - unlink(Sys.glob(file.path(dirname(real), "rjava-alias-*"))) - alias <- file.path(dirname(real), paste0("rjava-alias-", Sys.getpid(), ".", tools::file_ext(real))) - stopifnot(file.link(real, alias)) - reg.finalizer(globalenv(), function(e) unlink(alias), onexit = TRUE) - alias -} -# The library must be recognized by its exports, not by its name. -expect_mapped <- function(path, vms) { - mapped <- mapped_jvm() - if (is.null(mapped)) stop("no mapped JVM library was found; expected ", path) - log("mapped", mapped$path, "with", mapped$vms, "VMs") - stopifnot(identical(mapped$vms, vms)) - if (!is.null(path) && !identical(basename(mapped$path), basename(path))) - stop("mapped library is ", mapped$path, ", expected ", path) -} -# A directory name with spaces and non-ASCII characters when the locale allows it. -odd_dir <- function() { - name <- if (isTRUE(l10n_info()[["UTF-8"]])) "rJava t\u00ebst \u00fc \u65e5\u672c" else "rJava test dir" - dir <- file.path(if (windows) "C:/rjava-paths" else tempdir(), name) - dir.create(dir, recursive = TRUE, showWarnings = FALSE) - stopifnot(dir.exists(dir)) - dir -} -# A second name for a Java home: a junction on Windows, a symbolic link elsewhere. -link_home <- function(home, dir = odd_dir(), name = "jdk link") { - link <- file.path(dir, name) - unlink(link) - ok <- if (windows) Sys.junction(normalizePath(home), link) else file.symlink(normalizePath(home), link) - stopifnot(isTRUE(ok), dir.exists(link)) - link -} -start <- function(expected) { - rJava::.jinit() - stopifnot(state()$initialized, state()$state %in% c("created", "attached")) - actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") - if (!same_home(actual, expected)) stop("java.home is ", actual, ", expected ", expected) - # Exercise JNI signatures, references, arrays and exceptions, beyond initialization. - obj <- rJava::.jnew("java/lang/String", "deferred") - stopifnot(rJava::.jcall(obj, "I", "length") == 8L, - identical(rJava::.jevalArray(rJava::.jarray(1:3)), 1:3), - identical(rJava::.jcall("java/lang/Long", "J", "parseLong", "-4294967297"), -4294967297), - identical(rJava::.jcall("java/lang/Math", "D", "scalb", 1.5, 2L), 6)) - thrown <- try(rJava::.jcall("java/lang/Integer", "I", "parseInt", "x"), silent = TRUE) - stopifnot(inherits(thrown, "try-error"), grepl("NumberFormatException", thrown)) - failed <- try(internal(".junloadJVM")(), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$initialized) - version <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.version") - log("started", version, "at", actual) - invisible(actual) -} -# Native libraries the JVM loads after startup, as real workloads would. -later_loads <- function() { - J <- function(...) rJava::.jcall(...) - deflater <- rJava::.jnew("java/util/zip/Deflater") - J(deflater, "V", "setInput", rJava::.jarray(as.raw(1:100))) - J(deflater, "V", "finish") - stopifnot(J(deflater, "I", "deflate", rJava::.jarray(raw(200))) > 0L) - address <- J("java/net/InetAddress", "Ljava/net/InetAddress;", "getByName", "127.0.0.1") - socket <- rJava::.jnew("java/net/ServerSocket", 0L, 1L, address) - stopifnot(J(socket, "I", "getLocalPort") > 0L) - J(socket, "V", "close") - digest <- J("java/security/MessageDigest", "Ljava/security/MessageDigest;", "getInstance", "SHA-256") - stopifnot(length(J(digest, "[B", "digest", rJava::.jarray(as.raw(1:3)))) == 32L) - bean <- J("java/lang/management/ManagementFactory", "Ljava/lang/management/RuntimeMXBean;", "getRuntimeMXBean") - stopifnot(J(bean, "J", "getUptime") >= 0) - if (windows) { - store <- J("java/security/KeyStore", "Ljava/security/KeyStore;", "getInstance", "Windows-MY") - J(store, "V", "load", rJava::.jnull("java/security/KeyStore$LoadStoreParameter")) - } - image <- rJava::.jnew("java/awt/image/BufferedImage", 40L, 20L, 1L) - graphics <- J(image, "Ljava/awt/Graphics2D;", "createGraphics") - J(graphics, "V", "drawString", "rJava", 2L, 15L) - out <- rJava::.jnew("java/io/ByteArrayOutputStream") - stopifnot(J("javax/imageio/ImageIO", "Z", "write", rJava::.jcast(image, "java/awt/image/RenderedImage"), - "png", rJava::.jcast(out, "java/io/OutputStream")), J(out, "I", "size") > 0L) - log("later native loads passed") - if (windows) { - ps <- file.path(sysroot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") - modules <- system2(ps, c("-NoProfile", "-Command", - shQuote(sprintf("(Get-Process -Id %d).Modules | ForEach-Object FileName", Sys.getpid()))), stdout = TRUE) - keep <- grepl("(java|jdk|jre|zulu|corretto|temurin|vcruntime|msvc|ucrtbase)", modules, ignore.case = TRUE) - cat("Loaded modules:", modules[keep], sep = "\n ") - } -} - -switch(scenario, - select = { - Sys.setenv(JAVA_HOME = home_b) - start(home_b) - stopifnot(identical(minimal_path, Sys.getenv("PATH"))) - }, - retry = { - empty <- file.path(tempdir(), "home without jvm") - dir.create(empty) - Sys.setenv(JAVA_HOME = empty) - fails(rJava::.jinit(), "Cannot find JVM library") - fails(internal(".jloadJVM")(file.path(empty, "server", basename(lib(home_a))))) - # A native library with no JNI exports must fail cleanly and permit retry. - loadNamespace("stats") - fails(internal(".jloadJVM")(getLoadedDLLs()[["stats"]][["path"]]), "entry points") - Sys.setenv(JAVA_HOME = home_b) - start(home_b) - }, - badinput = { - load <- internal(".jloadJVM") - fails(load("")) - fails(load(c(lib(home_a), lib(home_b))), "Invalid path") - fails(load(home_a)) - fails(load(strrep("a", 40000L))) - # A relative path that only exceeds the Windows path limit once completed. - fails(load(paste0(strrep("b", 32700L), "/jvm.dll"))) - fails(load(file.path(tempdir(), strrep("c", 300L), "server", basename(lib(home_a))))) - Sys.setenv(JAVA_HOME = home_b) - start(home_b) - }, - unload = { - internal(".jloadJVM")(lib(home_a)) - stopifnot(state()$state == "none", !state()$initialized) - failed <- try(internal(".junloadJVM")(), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$state == "none") - failed <- try(internal(".jloadJVM")(lib(home_b)), silent = TRUE) - stopifnot(inherits(failed, "try-error"), state()$state == "none") - Sys.setenv(JAVA_HOME = home_a) - start(home_a) - }, - preloaded = { - # A JVM library mapped outside this loader rejects a different runtime and - # permits the same one, without starting a VM. With global symbols, its - # JNI exports must not interpose on rJava's own loader wrappers. - stopifnot(variant %in% c("", "global")) - # The internal attach used by .jinit() selects only a created VM. - attach <- function() .Call(internal("RattachJVM")) - fails(attach(), "No JVM library is mapped") - preload(lib(home_a), local = variant != "global") - mapped <- .Call(internal("RgetMappedJVM")) - stopifnot(!is.null(mapped), identical(mapped$vms, 0L)) - fails(attach(), "created no VM") - Sys.setenv(JAVA_HOME = home_b) - fails(rJava::.jinit(), "already mapped") - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - Sys.setenv(JAVA_HOME = home_a) - start(home_a) - }, - alias = { - # The mapped library must be recognized by file identity, not by name. - real <- lib(home_a) - path <- switch(variant, - link = file.path(link_home(home_a), relative(home_a)), - short = { - stopifnot(windows) - long <- file.path(link_home(home_a, name = "jdk long directory name"), relative(home_a)) - short <- utils::shortPathName(long) - if (identical(tolower(gsub("\\\\", "/", short)), tolower(long))) stop("no 8.3 name was generated for ", long) - short - }, - spelling = if (windows) { - chartr("/", "\\", toupper(file.path(dirname(dirname(real)), ".", "server", "..", "server", basename(real)))) - } else file.path(dirname(dirname(real)), ".", "server", "..", "server//", basename(real)), - stop("unknown alias variant")) - log("alias", path, "of", real) - preload(real) - internal(".jloadJVM")(path) - stopifnot(state()$state == "none") - start(home_a) - }, - renamed = { - # A JVM library mapped under a name no JVM uses, by a copy, hard link or - # symbolic link, rejects another runtime; the same file is still accepted. - if (variant == "copy") { - # A copy of B is another file than B's own library, and than A's. - copy <- file.path(odd_dir(), paste0("renamed runtime.", tools::file_ext(lib(home_b)))) - stopifnot(file.copy(lib(home_b), copy, overwrite = TRUE)) - preload(copy, bin = dirname(dirname(lib(home_b)))) - expect_mapped(copy, 0L) - Sys.setenv(JAVA_HOME = home_a) - fails(rJava::.jinit(), "already mapped") - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - expect_mapped(copy, 0L) - } else { - path <- switch(variant, - hardlink = alias_lib(home_a), - symlink = { - stopifnot(!windows) - link <- file.path(odd_dir(), paste0("renamed link.", tools::file_ext(lib(home_a)))) - unlink(link) - stopifnot(file.symlink(lib(home_a), link)) - link - }, - stop("unknown renamed variant")) - log("renamed", path, "of", lib(home_a)) - preload(path, bin = dirname(dirname(lib(home_a)))) - # dyld may record a symbolic link by its target, so only a hard link - # is certain to be mapped under the unusual name. - expect_mapped(if (variant == "hardlink") path, 0L) - Sys.setenv(JAVA_HOME = home_b) - fails(rJava::.jinit(), "already mapped") - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - Sys.setenv(JAVA_HOME = home_a) - if (windows) { - # The JDK's DLLs import jvm.dll by name, which this module lacks. - fails(rJava::.jinit(), "under another name") - } else { - start(home_a) - expect_mapped(if (variant == "hardlink") path, 1L) - } - } - }, - replaced = { - # The file the mapped JVM library was loaded from is deleted, or replaced - # by B's library under the same name. Identity is that of the mapped file, - # not of the file now at its name: B must be rejected, A still accepted. - stopifnot(variant %in% c("deleted", "replaced")) - if (windows) { - # A mapped DLL cannot be deleted, but may be renamed. JDK DLLs import - # jvm.dll by name, so replace jvm.dll in a copy of A's home. - copy <- file.path("C:/rjava-paths", paste0("replaced-", Sys.getpid())) - dir.create(copy, recursive = TRUE) - stopifnot(file.copy(normalizePath(home_a), copy, recursive = TRUE)) - home <- file.path(copy, basename(normalizePath(home_a))) - path <- file.path(home, relative(home_a)) - preload(path) - expect_mapped(path, 0L) - stopifnot(unlink(path) != 0L || file.exists(path)) - log("Windows refused deleting the mapped", path) - if (variant == "deleted") { - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - internal(".jloadJVM")(path) - stopifnot(state()$state == "none") - start(home) - } else { - moved <- file.path(dirname(path), "jvm-mapped.dll") - stopifnot(file.rename(path, moved), file.link(lib(home_b), path)) - log("renamed the mapped DLL to", moved, "and linked B's in its place") - expect_mapped(NULL, 0L) - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - fails(internal(".jloadJVM")(path), "already mapped") - Sys.setenv(JAVA_HOME = home_b) - fails(rJava::.jinit(), "already mapped") - internal(".jloadJVM")(moved) - stopifnot(state()$state == "none") - start(home) - expect_mapped(NULL, 1L) - } - } else { - path <- alias_lib(home_a) - preload(path) - expect_mapped(path, 0L) - stopifnot(unlink(path) == 0L, !file.exists(path)) - if (variant == "replaced") { - # A hard link, so the old check (the file now at the mapped name is B's - # library) accepted B and kept A's handle. - stopifnot(file.link(lib(home_b), path)) - log("replaced", path, "by a hard link of", lib(home_b)) - } - # musl takes a reference to a mapped image by opening its recorded name - # again, which no longer opens A's library: the image must still be - # identified as A's JVM before B is mapped. - fails(internal(".jloadJVM")(lib(home_b)), "already mapped") - fails(internal(".jloadJVM")(path), "already mapped") - Sys.setenv(JAVA_HOME = home_b) - fails(rJava::.jinit(), "already mapped") - unmapped(home_b) - if (byname == "B") { - # musl would bind the JDK's libraries to B's libjvm.so: A is refused - # too, before a VM exists, and B is still not mapped. - fails(internal(".jloadJVM")(lib(home_a)), "libjvm.so by name") - unmapped(home_b) - expect_mapped(path, 0L) - } else { - # A's library is the mapped file, whatever is now at the mapped name. - internal(".jloadJVM")(lib(home_a)) - stopifnot(state()$state == "none") - expect_mapped(path, 0L) - # The JVM finds its home from its own file name, which no longer - # exists if deleted. - if (variant == "replaced") { - start(home_a) - expect_mapped(path, 1L) - } - } - } - }, - race = { - # Linux, with tests-integration/dlrace.c preloaded: another thread may - # unmap any library, Java or not, once dl_iterate_phdr() returns. The - # fixture unmaps an unrelated library right then (raced), or maps and - # unmaps it on another thread while discovery runs (churned), which - # exercises the race but cannot prove its absence. - fixture <- Sys.getenv("RJE_DLRACE") - victim <- Sys.getenv("RJE_DLRACE_VICTIM") - stopifnot(file.exists(fixture), file.exists(victim), - grepl(fixture, Sys.getenv("LD_PRELOAD"), fixed = TRUE)) - dyn.load(fixture) - counts <- function() .C("dlrace_counts", hits = 0L, unmapped = 0L, armed = 0L) - raced <- function(expr) { - before <- counts() - .C("dlrace_arm", victim) - value <- expr - after <- counts() - stopifnot(after$armed == 0L, after$hits == before$hits + 1L, after$unmapped == before$unmapped + 1L) - invisible(value) - } - churned <- function(check, cycles = 20000L) { - stopifnot(.C("dlrace_churn", victim, cycles, result = 0L)$result == 0L) - calls <- 0L - repeat { - status <- .C("dlrace_churn_status", running = 0L, done = 0L, unmapped = 0L) - if (!status$running) break - check() - calls <- calls + 1L - } - log("discovery ran", calls, "times while another thread mapped and unmapped", - victim, status$done, "times, unmapped after", status$unmapped) - stopifnot(status$done == cycles, status$unmapped == cycles, calls > 0L) - } - attach <- function() .Call(internal("RattachJVM")) - one_jvm <- function() { - maps <- sub("^(\\S+\\s+){5}", "", grep("^(\\S+\\s+){5}/", readLines("/proc/self/maps"), value = TRUE)) - jvms <- unique(maps[basename(maps) == "libjvm.so"]) - if (length(jvms) != 1L) stop("expected one JVM library mapped, found: ", paste(jvms, collapse = ", ")) - } - stopifnot(is.null(raced(mapped_jvm()))) - churned(function() stopifnot(is.null(mapped_jvm()))) - raced(fails(attach(), "No JVM library is mapped")) - # A JVM mapped outside rJava, without and then with a created VM. - path <- lib(home_a) - helper <- Sys.getenv("RJE_JVMHOST") - stopifnot(file.exists(helper)) - dyn.load(helper) - preload(path) - raced(expect_mapped(path, 0L)) - churned(function() stopifnot(identical(mapped_jvm()$vms, 0L))) - raced(fails(attach(), "created no VM")) - raced(fails(internal(".jloadJVM")(lib(home_b)), "already mapped")) - unmapped(home_b) - boot <- file.path(system.file("java", package = "rJava"), "boot") - stopifnot(.C("rjava_test_create_jvm", path, paste0("-Djava.class.path=", boot), result = 0L)$result == 0L) - raced(expect_mapped(path, 1L)) - churned(function() stopifnot(identical(mapped_jvm()$vms, 1L))) - Sys.setenv(JAVA_HOME = home_b) - raced(fails(rJava::.jloadJVM(lib(home_b)), "already mapped")) - unmapped(home_b) - # .jinit() attaches to the created VM, through the same discovery. - raced(start(home_a)) - stopifnot(state()$state == "attached") - churned(function() stopifnot(identical(mapped_jvm()$vms, 1L)), 5000L) - unmapped(home_b) - one_jvm() - }, - decoy = { - # The Java launcher library (jli) has JNI exports on macOS that forward to - # libjvm, but is no JVM: it must neither be reported nor block selection. - jli <- list.files(home_a, "^(lib)?jli[.](so|dylib|dll)$", recursive = TRUE, full.names = TRUE) - stopifnot(length(jli) >= 1L) - log("decoy", jli[1]) - preload(jli[1], bin = dirname(jli[1])) - stopifnot(is.null(mapped_jvm())) - Sys.setenv(JAVA_HOME = home_b) - start(home_b) - }, - byname = { - # musl resolves the libjvm.so dependency of the JDK's libraries through - # LD_LIBRARY_PATH as set at process start, which the job points at A's. - # Selecting B is refused before a VM exists, so no second JVM library is - # mapped; musl never unmaps B's, which then blocks selecting A. - stopifnot(Sys.info()[["sysname"]] == "Linux") - Sys.setenv(JAVA_HOME = home_b) - fails(rJava::.jinit(), "libjvm.so by name") - maps <- grep("/libjvm[.]so$", readLines("/proc/self/maps"), value = TRUE) - maps <- unique(sub("^\\S+\\s+\\S+\\s+\\S+\\s+\\S+\\s+\\S+\\s+", "", maps)) - log("mapped JVM files:", maps) - stopifnot(length(maps) <= 1L, !lib(home_a) %in% maps) - if (length(maps)) fails(internal(".jloadJVM")(lib(home_a)), "already mapped") - }, - nonascii = { - # No preload: the loader itself must open a path with spaces and non-ASCII names. - Sys.setenv(JAVA_HOME = link_home(home_b)) - start(home_b) - }, - fallback = { - # An invalid explicit home is ignored with a warning, as before deferred - # loading, and discovery falls back. Expected homes are computed outside rJava. - # Windows: the CI job registers RJE_FALLBACK_HOME under HKCU\Software\JavaSoft. - # macOS: /usr/libexec/java_home if it finds a runtime. Otherwise: java on PATH. - expected <- Sys.getenv("RJE_FALLBACK_HOME") - java_home <- if (file.exists("/usr/libexec/java_home")) - suppressWarnings(system2("/usr/libexec/java_home", stdout = TRUE, stderr = FALSE)) - if (length(java_home) == 1L && is.null(attr(java_home, "status")) && dir.exists(java_home)) { - expected <- java_home - } else if (!windows) Sys.setenv(PATH = paste(file.path(expected, "bin"), minimal_path, sep = ":")) - stopifnot(dir.exists(expected)) - log("expecting fallback to", expected) - warned <- FALSE - withCallingHandlers(rJava::.jinit(), warning = function(w) { - if (grepl("INVALID", conditionMessage(w))) { warned <<- TRUE; invokeRestart("muffleWarning") } - }) - stopifnot(warned) - start(expected) - }, - retained = { - stopifnot(windows) - bin <- dirname(dirname(lib(home_a))) - # Keep the fixture coherent before rJava runs: a plain dyn.load(msvcp) - # otherwise pulls its VCRUNTIME imports from System32. - dependencies <- file.path(bin, c("vcruntime140.dll", "vcruntime140_1.dll")) - dependencies <- dependencies[file.exists(dependencies)] - for (dependency in dependencies) dyn.load(dependency) - dll <- file.path(bin, "msvcp140.dll") - dyn.load(dll) - dyn.load(Sys.getenv("RJE_JVMHOST")) - stopifnot(.C("rjava_test_module_loaded", "msvcp140.dll", result = 0L)$result == 1L) - rJava::.jloadJVM(lib(home_a)) - dyn.unload(dll) - for (dependency in rev(dependencies)) dyn.unload(dependency) - stopifnot(.C("rjava_test_module_loaded", "msvcp140.dll", result = 0L)$result == 1L) - start(home_a) - later_loads() - }, - laterload = { - home <- if (variant == "b") home_b else home_a - Sys.setenv(JAVA_HOME = home) - start(home) - later_loads() - stopifnot(identical(minimal_path, Sys.getenv("PATH"))) - }, - hosted = { - # Native code in this process created a JVM before rJava was used. rJava - # must attach to it, although JAVA_HOME names another runtime. A host - # provides rJava's class loader on its class path (JDK 9+ forbids adding it). - # Variant explicit: .jloadJVM() selects A's own file before .jinit(). - # Variant renamed: the host loads the runtime under a name no JVM uses. - # Not on Windows, where the JDK's DLLs then cannot import jvm.dll by name. - # Variant replaced: B's library then replaces that name (on Windows, the - # jvm.dll of a copy of A's home, renamed as in replaced). .jinit() attaches - # to the created VM, but .jloadJVM() rejects that name and B: a path - # selects only the file mapped, also when a VM was created. - if (variant == "laterload-b") { - swap <- home_a; home_a <- home_b; home_b <- swap - variant <- "laterload" - } - stopifnot(variant %in% c("", "explicit", "renamed", "replaced", "laterload"), !(windows && variant == "renamed")) - # With B's libjvm.so on musl's startup path, the host itself would bind - # A's JDK libraries to B's library, mapping two runtimes before rJava runs. - stopifnot(byname != "B") - helper <- Sys.getenv("RJE_JVMHOST") - stopifnot(file.exists(helper)) - dyn.load(helper) - boot <- file.path(system.file("java", package = "rJava"), "boot") - home <- home_a - if (windows && variant == "replaced") { - copy <- file.path("C:/rjava-paths", paste0("hosted-", Sys.getpid())) - dir.create(copy, recursive = TRUE) - stopifnot(file.copy(normalizePath(home_a), copy, recursive = TRUE)) - home <- file.path(copy, basename(normalizePath(home_a))) - path <- file.path(home, relative(home_a)) - } else path <- if (variant %in% c("renamed", "replaced")) alias_lib(home_a) else lib(home_a) - old <- Sys.getenv("PATH") - if (windows) Sys.setenv(PATH = paste(normalizePath(dirname(dirname(path)), winslash = "\\"), old, sep = ";")) - created <- .C("rjava_test_create_jvm", path, paste0("-Djava.class.path=", boot), result = 0L)$result - Sys.setenv(PATH = old) - stopifnot(created == 0L) - expect_mapped(path, 1L) - # The name djni_mapped() reports, which .jinit() once passed to .jloadJVM(). - recorded <- mapped_jvm()$path - if (variant == "replaced") { - if (windows) { - moved <- file.path(dirname(path), "jvm-mapped.dll") - stopifnot(file.rename(path, moved), file.link(lib(home_b), path)) - } else stopifnot(unlink(path) == 0L, file.link(lib(home_b), path)) - log("replaced", path, "by a hard link of", lib(home_b)) - expect_mapped(if (!windows) path, 1L) - } - stopifnot(state()$state == "not-loaded") - Sys.setenv(JAVA_HOME = home_b) - fails(rJava::.jloadJVM(lib(home_b)), "already mapped") - if (variant == "replaced") { - fails(rJava::.jloadJVM(recorded), "already mapped") - fails(rJava::.jloadJVM(path), "already mapped") - } - unmapped(home_b) - if (variant == "explicit") { - stopifnot(isTRUE(rJava::.jloadJVM(lib(home_a))), state()$state == "detached") - } - start(home) - stopifnot(state()$state == "attached") - if (variant == "laterload") later_loads() - stopifnot(identical(minimal_path, Sys.getenv("PATH"))) - }, - jengine = { - # R hosts the JVM, then JRI is loaded into it as a later native library. - jri <- system.file("jri", package = "rJava") - if (windows && dir.exists(file.path(jri, "x64"))) jri <- file.path(jri, "x64") - options(java.parameters = c("-Djava.awt.headless=true", paste0("-Djava.library.path=", jri))) - Sys.setenv(JAVA_HOME = home_a) - start(home_a) - engine <- rJava::.jengine(TRUE) - stopifnot(!rJava::is.jnull(engine)) - stopifnot(identical(rJava::.jcall(engine, "Z", "isStandAlone"), FALSE)) - log("JRI engine started inside R") - }, - stop("unknown scenario: ", scenario)) -cat("PASS", scenario, variant, "\n") diff --git a/tests-integration/dll-search.c b/tests-integration/dll-search.c deleted file mode 100644 index 01d5351..0000000 --- a/tests-integration/dll-search.c +++ /dev/null @@ -1,62 +0,0 @@ -/* Windows only: the JVM bin directory may be added to the DLL search path - only while djni_load() loads jvm.dll, and must be removed again after both - failed and successful loads. Build and run with a stripped PATH: - gcc -Isrc -o dll-search.exe tests-integration/dll-search.c - dll-search.exe - Both bin directories contain rjava_marker.dll, which is nowhere else. */ -#include "../src/djni.c" - -static int failures; - -static void check(int ok, const char *what) { - printf("%s: %s\n", ok ? "ok" : "FAIL", what); - if (!ok) failures++; -} -/* Searches only directories added with AddDllDirectory. */ -static int marker_visible(void) { - HMODULE h = LoadLibraryExW(L"rjava_marker.dll", NULL, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_SEARCH_USER_DIRS); - if (h) FreeLibrary(h); - return h != NULL; -} - -int main(int argc, char **argv) { - DLL_DIRECTORY_COOKIE cookie; - wchar_t *bin; - char *path; - const char *mapped; - int vms, res; - if (argc != 4) { fprintf(stderr, "usage: dll-search \n"); return 2; } - check(!marker_visible(), "marker is not visible initially"); - bin = to_wide(argv[3]); - cookie = bin ? AddDllDirectory(bin) : NULL; - check(cookie && marker_visible(), "control: an added directory is searched"); - if (cookie) RemoveDllDirectory(cookie); - check(!marker_visible(), "control: a removed directory is not searched"); - - res = djni_load(argv[1]); - printf("missing library: %d %s\n", res, djni_last_error() ? djni_last_error() : ""); - check(res == -2 && !djni_loaded(), "a missing jvm.dll fails cleanly"); - check(!marker_visible(), "bin is not searched after a failed load"); - - /* Relative input that only exceeds the path limit once completed. */ - path = malloc(32800); - memset(path, 'b', 32700); - strcpy(path + 32700, "\\server\\jvm.dll"); - res = djni_load(path); - printf("over-long path: %d %.120s\n", res, djni_last_error() ? djni_last_error() : ""); - check(res == -2 && !djni_loaded(), "an over-long path fails cleanly"); - free(path); - - check(!djni_mapped(&vms), "no jvm.dll is mapped before loading"); - res = djni_load(argv[2]); - printf("copied library: %d %s\n", res, djni_last_error() ? djni_last_error() : ""); - check(res == 0 && djni_loaded(), "a jvm.dll with dependencies in bin loads"); - check(!marker_visible(), "bin is not searched after a successful load"); - mapped = djni_mapped(&vms); - printf("mapped: %s, %d VMs\n", mapped ? mapped : "(none)", vms); - check(mapped && vms == 0, "the loaded jvm.dll is reported without a VM"); - free(bin); - if (failures) { printf("FAIL dll-search: %d failures\n", failures); return 1; } - printf("PASS dll-search\n"); - return 0; -} diff --git a/tests-integration/dlrace.c b/tests-integration/dlrace.c deleted file mode 100644 index c73a011..0000000 --- a/tests-integration/dlrace.c +++ /dev/null @@ -1,108 +0,0 @@ -/* Test fixture (Linux): unmaps a library unrelated to Java while djni lists - the mapped images, without a test hook in rJava. Preloaded, it interposes - dl_iterate_phdr(); built with -DDLRACE_VICTIM, it is that library: - cc -shared -fPIC -o dlrace.so dlrace.c -ldl -lpthread - cc -shared -fPIC -DDLRACE_VICTIM -o dlrace-victim.so dlrace.c - glibc holds its loader lock while dl_iterate_phdr() runs and may unmap any - image as soon as it returns. dlrace_arm() makes the next listing by rJava - on the calling thread map the victim before it starts and unmap it right - after it returns, when djni may no longer read it. dlrace_churn() maps and - unmaps the victim on another thread instead, concurrently with listings. */ -#ifdef DLRACE_VICTIM -int rjava_dlrace_victim(void) { return 42; } -#else -#define _GNU_SOURCE -#include -#include -#include -#include -#include - -typedef int (*callback_fn)(struct dl_phdr_info *, size_t, void *); -static int (*real_iterate)(callback_fn, void *); -static char victim[4096]; -static pthread_t armed_thread, churn_thread; -static int armed, hits, unmapped, churning, joined = 1, cycles, churn_unmapped; - -__attribute__((constructor)) static void init(void) { - void *real = dlsym(RTLD_NEXT, "dl_iterate_phdr"); - memcpy(&real_iterate, &real, sizeof(real)); -} -/* Whether the victim is no longer loaded. musl never unloads a library. */ -static int gone(void) { - void *h = dlopen(victim, RTLD_NOW | RTLD_LOCAL | RTLD_NOLOAD); - if (h) dlclose(h); - return !h; -} -static int from_rjava(callback_fn callback) { - Dl_info info; - const char *base; - void *addr; - memcpy(&addr, &callback, sizeof(addr)); - if (!dladdr(addr, &info) || !info.dli_fname) return 0; - base = strrchr(info.dli_fname, '/'); - return !strcmp(base ? base + 1 : info.dli_fname, "rJava.so"); -} -int dl_iterate_phdr(callback_fn callback, void *data) { - void *h = NULL; - int res; - if (__atomic_load_n(&armed, __ATOMIC_ACQUIRE) && pthread_equal(pthread_self(), armed_thread) && - from_rjava(callback)) { - __atomic_store_n(&armed, 0, __ATOMIC_RELEASE); - if (!(h = dlopen(victim, RTLD_NOW | RTLD_LOCAL))) fprintf(stderr, "dlrace: %s\n", dlerror()); - } - res = real_iterate(callback, data); - if (h) { - dlclose(h); - hits++; - unmapped += gone(); - } - return res; -} -void dlrace_arm(char **path) { - snprintf(victim, sizeof(victim), "%s", path[0]); - armed_thread = pthread_self(); - __atomic_store_n(&armed, 1, __ATOMIC_RELEASE); -} -/* hits: listings by rJava that saw the victim, unmapped: those after which - it was no longer loaded, armed: whether a listing is still expected. */ -void dlrace_counts(int *n, int *closed, int *pending) { - *n = hits; - *closed = unmapped; - *pending = __atomic_load_n(&armed, __ATOMIC_ACQUIRE); -} -static void *churn(void *arg) { - int i, n = *(int *) arg; - for (i = 0; i < n; i++) { - void *h = dlopen(victim, RTLD_NOW | RTLD_LOCAL); - if (!h) break; - dlclose(h); - churn_unmapped += gone(); - __atomic_store_n(&cycles, i + 1, __ATOMIC_RELEASE); - } - __atomic_store_n(&churning, 0, __ATOMIC_RELEASE); - return NULL; -} -void dlrace_churn(char **path, int *n, int *result) { - static int count; - if (!joined) { *result = -1; return; } - snprintf(victim, sizeof(victim), "%s", path[0]); - count = *n; - cycles = churn_unmapped = 0; - churning = 1; - if ((*result = pthread_create(&churn_thread, NULL, churn, &count))) churning = 0; - else joined = 0; -} -/* running: whether the thread still runs; once it has stopped, joins it and - reports its cycles and those after which the victim was unloaded. */ -void dlrace_churn_status(int *running, int *done, int *closed) { - *running = __atomic_load_n(&churning, __ATOMIC_ACQUIRE); - *done = __atomic_load_n(&cycles, __ATOMIC_ACQUIRE); - *closed = 0; - if (!*running && !joined) { - pthread_join(churn_thread, NULL); - joined = 1; - } - if (!*running) *closed = churn_unmapped; -} -#endif diff --git a/tests-integration/jni-abi.R b/tests-integration/jni-abi.R deleted file mode 100644 index ed24c21..0000000 --- a/tests-integration/jni-abi.R +++ /dev/null @@ -1,51 +0,0 @@ -# rJava compiles against its own src/jni.h, not the JDK headers. Check that its -# types and function tables have the layout of each JDK's jni.h: -# Rscript --vanilla tests-integration/jni-abi.R [ ...] -homes <- commandArgs(TRUE) -stopifnot(length(homes) >= 1L, all(dir.exists(homes))) -windows <- .Platform$OS.type == "windows" -quote <- function(x) shQuote(x, type = if (windows) "cmd" else "sh") -header <- readLines(file.path("src", "jni.h")) -members <- function(struct) { - start <- grep(paste0("^struct ", struct, " \\{"), header) - end <- start + grep("^\\};", header[-seq_len(start)])[1] - body <- header[start:end] - c(sub(".*void \\*(reserved[0-9]+);.*", "\\1", grep("void \\*reserved[0-9]+;", body, value = TRUE)), - sub(".*\\(JNICALL \\*([A-Za-z0-9_]+)\\).*", "\\1", grep("JNICALL \\*", body, value = TRUE))) -} -offsets <- function(struct) sprintf(' printf("%s.%%s %%lu\\n", "%s", (unsigned long) offsetof(struct %s, %s));', - struct, members(struct), struct, members(struct)) -types <- c("jbyte", "jboolean", "jchar", "jshort", "jint", "jlong", "jfloat", "jdouble", "jsize", - "jvalue", "jobject", "jfieldID", "jmethodID", "JavaVMOption", "JavaVMInitArgs", "JavaVMAttachArgs") -fields <- c("JavaVMInitArgs, version", "JavaVMInitArgs, nOptions", "JavaVMInitArgs, options", - "JavaVMInitArgs, ignoreUnrecognized", "JavaVMOption, extraInfo", "JavaVMAttachArgs, group") -probe <- c("#include ", "#include ", "#include ", "int main(void) {", - sprintf(' printf("sizeof %s %%lu\\n", (unsigned long) sizeof(%s));', types, types), - sprintf(' printf("offsetof %s %%lu\\n", (unsigned long) offsetof(%s));', fields, fields), - offsets("JNIInvokeInterface_"), offsets("JNINativeInterface_"), - ' printf("JNI_VERSION_1_2 %x\\n", JNI_VERSION_1_2);', " return 0;", "}") -dir <- tempfile("jni-abi-") -dir.create(dir) -src <- file.path(dir, "probe.c") -writeLines(probe, src) -cc <- system2(file.path(R.home("bin"), "R"), c("CMD", "config", "CC"), stdout = TRUE) -run <- function(name, includes) { - exe <- file.path(dir, paste0("probe-", name, if (windows) ".exe" else "")) - command <- paste(cc, paste0("-I", quote(includes), collapse = " "), "-o", quote(exe), quote(src)) - if (system(command) != 0L) stop("cannot compile the probe with ", paste(includes, collapse = ", ")) - system2(exe, stdout = TRUE) -} -# A directory holding only rJava's header, so that no JDK header can be picked up. -own <- file.path(dir, "own") -dir.create(own) -stopifnot(file.copy(file.path("src", "jni.h"), own)) -expected <- run("own", own) -stopifnot(length(expected) == length(types) + length(fields) + 1L + - length(members("JNIInvokeInterface_")) + length(members("JNINativeInterface_"))) -for (home in homes) { - include <- file.path(home, "include") - actual <- run(basename(tempfile("jdk")), c(include, list.dirs(include, recursive = FALSE))) - different <- setdiff(expected, actual) - if (length(different)) stop("JNI layout differs from ", home, ":\n", paste(different, collapse = "\n")) - cat("JNI layout of src/jni.h matches", home, "(", length(expected), "entries )\n") -} diff --git a/tests-integration/jri.R b/tests-integration/jri.R deleted file mode 100644 index 32d94d8..0000000 --- a/tests-integration/jri.R +++ /dev/null @@ -1,30 +0,0 @@ -# Runs a Java host (JDK A) embedding R through JRI, with JAVA_HOME naming JDK B: -# Rscript --vanilla tests-integration/jri.R -windows <- .Platform$OS.type == "windows" -home_a <- Sys.getenv("RJE_JAVA_HOME_A") -home_b <- Sys.getenv("RJE_JAVA_HOME_B") -stopifnot(dir.exists(home_a), dir.exists(home_b)) -jri <- system.file("jri", package = "rJava") -jar <- file.path(jri, "JRI.jar") -native <- if (windows && dir.exists(file.path(jri, "x64"))) file.path(jri, "x64") else jri -stopifnot(file.exists(jar), length(list.files(native, "jri[.](dll|so|jnilib)$")) == 1L) -classes <- file.path(tempdir(), "classes") -dir.create(classes) -exe <- function(home, tool) file.path(home, "bin", if (windows) paste0(tool, ".exe") else tool) -source_file <- file.path(getwd(), "tests-integration", "JriAttach.java") -stopifnot(system2(exe(home_a, "javac"), c("-cp", shQuote(jar), "-d", shQuote(classes), shQuote(source_file))) == 0L) -# The embedded R needs its own libraries, but no Java directory, on the search path. -sysroot <- Sys.getenv("SystemRoot", "C:\\Windows") -Sys.setenv( - JAVA_HOME = home_b, - R_HOME = R.home(), - R_LIBS = paste(.libPaths(), collapse = .Platform$path.sep), - PATH = if (windows) paste(normalizePath(c(R.home("bin"), file.path(sysroot, "System32"), sysroot), - winslash = "\\"), collapse = ";") else "/usr/bin:/bin") -if (Sys.info()[["sysname"]] == "Linux") Sys.setenv(LD_LIBRARY_PATH = file.path(R.home(), "lib")) -# A Java host provides rJava's class loader on its class path: JDK 9+ does not -# permit rJava to append it to a running application class loader. -boot <- file.path(system.file("java", package = "rJava"), "boot") -status <- system2(exe(home_a, "java"), c(paste0("-Djava.library.path=", shQuote(native)), - "-cp", shQuote(paste(jar, boot, classes, sep = .Platform$path.sep)), "JriAttach")) -stopifnot(status == 0L) diff --git a/tests-integration/jvmhost.c b/tests-integration/jvmhost.c deleted file mode 100644 index 9e1f091..0000000 --- a/tests-integration/jvmhost.c +++ /dev/null @@ -1,61 +0,0 @@ -/* Test helper: creates a JVM in the R process outside rJava, as a native - application embedding both Java and R would. POSIX loads the library with - global symbols, like the java launcher. Build with - PKG_CPPFLAGS=-I/src R CMD SHLIB jvmhost.c */ -#ifdef _WIN32 -#define _WIN32_WINNT 0x0602 -#include -#include -#include -#else -#include -#endif -#include "jni.h" - -void rjava_test_create_jvm(char **path, char **classpath, int *result) { - JNI_CreateJavaVM_fnptr create = 0; - JavaVMOption options[2]; - JavaVMInitArgs args; - JavaVM *vm; - void *env; -#ifdef _WIN32 - /* A real native host must choose its own startup dependencies. Use the - selected JDK's bin only during this load, like java.exe's app directory; - rJava must independently arrange any later MSVC loads after attaching. */ - int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path[0], -1, NULL, 0); - wchar_t *wide = n ? malloc(sizeof(wchar_t) * n) : NULL; - wchar_t *bin = n ? malloc(sizeof(wchar_t) * n) : NULL, *slash; - DLL_DIRECTORY_COOKIE cookie = NULL; - HMODULE lib = NULL; - if (wide && bin && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path[0], -1, wide, n)) { - for (slash = wide; *slash; slash++) if (*slash == L'/') *slash = L'\\'; - wcscpy(bin, wide); - if ((slash = wcsrchr(bin, L'\\'))) { - *slash = 0; - if ((slash = wcsrchr(bin, L'\\'))) { *slash = 0; cookie = AddDllDirectory(bin); } - } - lib = LoadLibraryExW(wide, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); - if (cookie) RemoveDllDirectory(cookie); - } - free(wide); free(bin); - if (lib) create = (JNI_CreateJavaVM_fnptr) GetProcAddress(lib, "JNI_CreateJavaVM"); -#else - void *lib = dlopen(path[0], RTLD_NOW | RTLD_GLOBAL); - if (lib) *(void **) &create = dlsym(lib, "JNI_CreateJavaVM"); -#endif - if (!create) { *result = -100; return; } - options[0].optionString = classpath[0]; - options[1].optionString = "-Djava.awt.headless=true"; - args.version = JNI_VERSION_1_2; - args.nOptions = 2; - args.options = options; - args.ignoreUnrecognized = JNI_TRUE; - *result = create(&vm, &env, &args); -} - -#ifdef _WIN32 -/* Observe retention after the component that first loaded a runtime lets go. */ -void rjava_test_module_loaded(char **name, int *result) { - *result = GetModuleHandleA(name[0]) != NULL; -} -#endif diff --git a/tests-integration/musl.sh b/tests-integration/musl.sh deleted file mode 100755 index b658067..0000000 --- a/tests-integration/musl.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -# Runs the Linux deferred-loading checks against musl libc, R and OpenJDK -# from Alpine packages, inside an Alpine container: -# docker run --rm -v "$PWD:/src:ro" alpine:3.24 \ -# sh -c 'apk add --no-cache bash && bash /src/tests-integration/musl.sh 17 21 x86_64' -# The arguments are the Alpine OpenJDK versions of JDK A and JDK B, and the -# expected machine. The source is copied first: mkdist writes into the tree -# and its parent. -set -euo pipefail -version_a="$1" -version_b="$2" -[[ "$(uname -m)" == "$3" ]] || { echo "::error::machine is $(uname -m), expected $3"; exit 1; } -apk add --no-cache build-base R R-dev binutils coreutils findutils grep \ - "openjdk$version_a-jdk" "openjdk$version_b-jdk" openjdk17-jdk -export LANG=C.UTF-8 LC_ALL=C.UTF-8 -home() { - local dir - for dir in "/usr/lib/jvm/java-$1-openjdk" "/usr/lib/jvm/java-1.$1-openjdk"; do - if [[ -d "$dir" ]]; then echo "$dir"; return; fi - done - echo "no Alpine OpenJDK $1 home" >&2 - return 1 -} -export RJE_JAVA_HOME_A="$(home "$version_a")" -export RJE_JAVA_HOME_B="$(home "$version_b")" -export RJE_FALLBACK_HOME="$RJE_JAVA_HOME_B" -export JAVA_HOME="$(home 17)" NOAWT=1 -export PATH="$JAVA_HOME/bin:$PATH" - -echo "::group::Environment" -cat /etc/alpine-release -ldd --version 2>&1 | head -n 2 || true -uname -srm -R --version | head -n 1 -# The container root is normally overlayfs on the runner's kernel. -stat -f -c 'JDK filesystem: %T' "$RJE_JAVA_HOME_A" -# deferred.R reduces PATH to /usr/bin:/bin so that a system java cannot mask -# discovery. Alpine's java-common links /usr/bin/java to the default JVM; -# remove that link in this disposable container, as the hosted jobs do not -# have one either. -ls -l /usr/bin/java /usr/lib/jvm/default-jvm 2>&1 || true -rm -f /usr/bin/java -if [[ -e /usr/bin/java || -e /bin/java ]]; then echo "::error::java on the reduced PATH would mask discovery"; exit 1; fi -echo "::endgroup::" - -for jhome in "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B"; do - echo "== $jhome"; "$jhome/bin/java" -version 2>&1 - find "$jhome/" -name 'libjvm.*' -print - lib="$(find "$jhome/" -name 'libjvm.*' -path '*server*' -print -quit)" - # These must be musl builds, not glibc builds run through a compatibility layer. - readelf -d "$lib" | grep -E 'NEEDED.*libc\.musl-' || { echo "::error::$lib is not linked against musl"; exit 1; } - syms="$(nm -D --defined-only "$lib")" - for sym in JNI_CreateJavaVM JNI_GetCreatedJavaVMs JVM_CurrentTimeMillis; do - if grep -Eq "[ _]$sym(@|\$| )" <<< "$syms"; then echo "exports $sym"; else echo "::error::$lib does not export $sym"; exit 1; fi - done -done - -R CMD javareconf -rm -rf /work && mkdir -p /work/rJava /work/lib /work/jvmhost -cp -a /src/. /work/rJava/ -cd /work/rJava -sh mkdist -R CMD INSTALL --library=/work/lib /work/rJava_*.tar.gz -export R_LIBS=/work/lib - -lib="$(Rscript --vanilla -e 'cat(system.file("libs", package="rJava"))')" -# musl's ldd also resolves symbols, and R's symbols come from the executable. -readelf -d "$lib/rJava.so" | grep NEEDED -if readelf -d "$lib/rJava.so" | grep NEEDED | grep -i libjvm; then echo "::error::rJava.so depends on libjvm"; exit 1; fi - -Rscript --vanilla tests-integration/jni-abi.R "$RJE_JAVA_HOME_A" "$RJE_JAVA_HOME_B" -cp tests-integration/jvmhost.c /work/jvmhost/ -(cd /work/jvmhost && PKG_CPPFLAGS="-I/work/rJava/src" R CMD SHLIB jvmhost.c) -export RJE_JVMHOST=/work/jvmhost/jvmhost.so - -# musl resolves the JDK libraries' dependency on libjvm.so through -# LD_LIBRARY_PATH as set at process start, not by matching the loaded JVM -# library's SONAME as glibc does. R's etc/ldpaths puts R_JAVA_LD_LIBRARY_PATH -# (default: the server directory of the R CMD javareconf JDK) on it. -echo "::group::Startup library path" -grep -n JAVA /usr/lib/R/etc/ldpaths -Rscript --vanilla -e 'cat("LD_LIBRARY_PATH:", Sys.getenv("LD_LIBRARY_PATH"), "\n")' -echo "::endgroup::" -server() { dirname "$(find "$1/" -name libjvm.so -path '*/server/*' -print -quit)"; } -server_a="$(server "$RJE_JAVA_HOME_A")" -server_b="$(server "$RJE_JAVA_HOME_B")" -# The scenarios of dynamic.yaml that apply to Linux, each with the server -# directory of the runtime it starts (A or B) on the startup library path. -run() { - local jdk="$1"; shift - local dir="$server_a"; [[ "$jdk" == B ]] && dir="$server_b" - echo "::group::$* ($jdk on LD_LIBRARY_PATH)" - R_JAVA_LD_LIBRARY_PATH="$dir" RJE_BYNAME="$jdk" Rscript --vanilla tests-integration/deferred.R "$@" - echo "::endgroup::" -} -for scenario in select retry badinput nonascii fallback decoy; do run B "$scenario"; done -for scenario in unload preloaded laterload hosted jengine; do run A "$scenario"; done -run A hosted explicit -run A preloaded global -run B laterload b -run A alias link -run A alias spelling -run B renamed copy -run A renamed hardlink -run A renamed symlink -run A hosted renamed -# A is mapped under a name that then no longer opens it, so musl cannot take -# a reference by that name. With either runtime on the startup library path, -# B must be refused before it is mapped; A is accepted only when the JDK's -# libraries would bind to it. hosted replaced runs with A only: with B on the -# path, the host's own VM would bind A's JDK libraries to B's libjvm.so. -for jdk in A B; do - run "$jdk" replaced deleted - run "$jdk" replaced replaced -done -run A hosted replaced -# The library path names A's runtime, B is selected: refused before a VM exists. -run A byname - -echo "::group::JRI host" -Rscript --vanilla tests-integration/jri.R -env RJE_JAVA_HOME_A="$RJE_JAVA_HOME_B" RJE_JAVA_HOME_B="$RJE_JAVA_HOME_A" Rscript --vanilla tests-integration/jri.R -echo "::endgroup::" diff --git a/tests-integration/overlay-layouts.sh b/tests-integration/overlay-layouts.sh deleted file mode 100644 index bbd8d39..0000000 --- a/tests-integration/overlay-layouts.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Runs inside the kernel VM. Mounts are privileged except rootless-inner; -# every R test runs as the unprivileged ubuntu user or a user-namespace root. -set -euo pipefail -expect=$1 -phase=${2:-outer} -run_cases() { - local layout=$1 selected_home=$2 - findmnt "$selected_home" - for scenario in fresh preloaded hosted replaced; do - if [[ "$phase" == rootless-inner ]]; then - env RJE_JAVA_HOME_A="$selected_home" RJE_OVERLAY_LAYOUT="$layout" RJE_OVERLAY_USERNS=1 \ - Rscript --vanilla tests-integration/overlay.R "$scenario" - else - runuser -u ubuntu -- env RJE_JAVA_HOME_A="$selected_home" RJE_OVERLAY_LAYOUT="$layout" \ - Rscript --vanilla tests-integration/overlay.R "$scenario" - fi - done -} -if [[ "$phase" == rootless-inner ]]; then - work=/home/ubuntu/overlay-rootless - mkdir -p "$work"/{upper,work,merged} - mount -t overlay overlay -o "lowerdir=/opt/overlay-lower,upperdir=$work/upper,workdir=$work/work,userxattr" "$work/merged" - trap 'umount "$work/merged"' EXIT - cat /proc/self/uid_map - run_cases rootless "$work/merged" - exit -fi -export RJE_OVERLAY_EXPECT="$expect" -mkdir -p /opt/overlay-lower /opt/jdk-b /opt/overlay-upper -cp -a "$JAVA_HOME/." /opt/overlay-lower/ -cp -a "$JAVA_HOME/." /opt/jdk-b/ -chown -R ubuntu:ubuntu /opt/overlay-lower -# A second real ext4 filesystem, not merely another directory on the same fs. -truncate -s 1G /opt/overlay-upper.img -mkfs.ext4 -q -F /opt/overlay-upper.img -mount -o loop /opt/overlay-upper.img /opt/overlay-upper -export RJE_JAVA_HOME_B=/opt/jdk-b -for layout in same cross-off cross-on; do - upper=/opt/overlay-upper - options= - if [[ "$layout" == same ]]; then upper=/opt; fi - if [[ "$layout" == cross-off ]]; then options=,xino=off; fi - if [[ "$layout" == cross-on ]]; then options=,xino=on; fi - work="$upper/rjava-$layout" - mkdir -p "$work"/{upper,work,merged} - chown -R ubuntu:ubuntu "$work" - mount -t overlay overlay -o "lowerdir=/opt/overlay-lower,upperdir=$work/upper,workdir=$work/work$options" "$work/merged" - run_cases "$layout" "$work/merged" - umount "$work/merged" -done -# Configure the isolated VM to allow the user namespaces being tested. -if [[ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then - sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 -fi -if [[ -f /proc/sys/kernel/unprivileged_userns_clone ]]; then - sysctl -w kernel.unprivileged_userns_clone=1 -fi -runuser -u ubuntu -- unshare --user --map-root-user --mount --propagation private \ - bash tests-integration/overlay-layouts.sh "$expect" rootless-inner -umount /opt/overlay-upper diff --git a/tests-integration/overlay-vm.sh b/tests-integration/overlay-vm.sh deleted file mode 100644 index df36fa6..0000000 --- a/tests-integration/overlay-vm.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -# Boot a real kernel: containers would reuse the Actions runner's kernel. -set -euo pipefail -release=$1 -expect=$2 -work="$RUNNER_TEMP/overlay-$release" -mkdir -p "$work" -case "$release" in - jammy) version=22.04; snapshot=20261004; image_sha=012d81fade7e8ff4428fc35dfeee711d66d1f47d5ea9c5c8ced96a85b959835c ;; - noble) version=24.04; snapshot=20261017; image_sha=04e8f2461a16a44e78a370284471f29616cdd9baf1893df25807b71f985b6c94 ;; - *) exit 2 ;; -esac -image="ubuntu-$version-server-cloudimg-amd64.img" -base="https://cloud-images.ubuntu.com/releases/$release/release-$snapshot" -curl --fail --location --retry 3 "$base/$image" -o "$work/$image" -printf '%s %s\n' "$image_sha" "$image" > "$work/selected.sha256" -(cd "$work" && sha256sum -c selected.sha256) -qemu-img resize "$work/$image" 16G -ssh-keygen -t ed25519 -N '' -f "$work/key" -cat > "$work/user-data" < "$work/meta-data" -cloud-localds "$work/seed.img" "$work/user-data" "$work/meta-data" -sudo qemu-system-x86_64 -accel kvm -cpu host -smp 2 -m 3072 \ - -drive "file=$work/$image,if=virtio,format=qcow2" \ - -drive "file=$work/seed.img,if=virtio,format=raw" \ - -netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=net0 \ - -display none -serial "file:$work/serial.log" -daemonize -pidfile "$work/qemu.pid" -sudo chmod a+r "$work/qemu.pid" "$work/serial.log" -trap 'sudo kill "$(cat "$work/qemu.pid")" || true' EXIT -ssh_opts=(-i "$work/key" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5) -wait_for_ssh() { - local previous_boot=${1:-} boot - for _attempt in {1..60}; do - if boot=$(ssh "${ssh_opts[@]}" -p 2222 ubuntu@127.0.0.1 cat /proc/sys/kernel/random/boot_id 2>/dev/null); then - if [[ -n "$boot" && "$boot" != "$previous_boot" ]]; then return 0; fi - fi - sleep 5 - done - cat "$work/serial.log" - return 1 -} -wait_for_ssh -if [[ "$expect" == legacy ]]; then - # Current distro 5.15 kernels can carry overlayfs fixes. Also boot a frozen - # pre-6.8 mainline build, so the ambiguous-identity case is really exercised. - mainline=https://kernel.ubuntu.com/mainline/v6.1/amd64 - kernel_version=6.1.0-060100 - kernel_build=6.1.0-060100.202303090726 - packages=("linux-image-unsigned-${kernel_version}-generic_${kernel_build}_amd64.deb" - "linux-modules-${kernel_version}-generic_${kernel_build}_amd64.deb") - for package in "${packages[@]}"; do - curl --fail --location --retry 3 "$mainline/$package" -o "$work/$package" - done - # SHA256 values from the release's CHECKSUMS, pinned with the package names. - cat > "$work/kernel.sha256" < /home/ubuntu/build.log 2>&1 || { cat /home/ubuntu/build.log; exit 1; } -mkdir /opt/rjava-lib -R CMD INSTALL --library=/opt/rjava-lib ../rJava_*.tar.gz -export R_LIBS=/opt/rjava-lib -uname -a -mkdir /tmp/jvmhost -cp tests-integration/jvmhost.c /tmp/jvmhost/ -(cd /tmp/jvmhost && PKG_CPPFLAGS=-I/home/ubuntu/source/src R CMD SHLIB jvmhost.c) -export RJE_JVMHOST=/tmp/jvmhost/jvmhost.so -bash tests-integration/overlay-layouts.sh "$1" -GUEST diff --git a/tests-integration/overlay.R b/tests-integration/overlay.R deleted file mode 100644 index d5c8462..0000000 --- a/tests-integration/overlay.R +++ /dev/null @@ -1,64 +0,0 @@ -# Real overlayfs in a VM. Each mode runs in a fresh process. -scenario <- commandArgs(TRUE)[1] -expect <- Sys.getenv("RJE_OVERLAY_EXPECT") -layout <- Sys.getenv("RJE_OVERLAY_LAYOUT") -uid <- as.integer(system2("id", "-u", stdout = TRUE)) -stopifnot(layout %in% c("same", "cross-off", "cross-on", "rootless")) -if (Sys.getenv("RJE_OVERLAY_USERNS") == "1") { - ids <- scan("/proc/self/uid_map", quiet = TRUE) - stopifnot(uid == 0L, length(ids) == 3L, ids[1] == 0, ids[2] != 0, ids[3] == 1) -} else stopifnot(uid != 0L) -ambiguous <- "^Cannot verify that .*because the overlayfs mapping identity cannot be verified[.]" -different <- "^Another JVM library is already mapped:" -same_ambiguous <- expect == "legacy" || layout == "cross-off" -stopifnot(expect %in% c("legacy", "distro", "current"), Sys.info()[["sysname"]] == "Linux") -kernel <- numeric_version(sub("-.*", "", Sys.info()[["release"]])) -stopifnot(if (expect == "current") kernel >= "6.8" else kernel < "6.8") -a <- Sys.getenv("RJE_JAVA_HOME_A") -b <- Sys.getenv("RJE_JAVA_HOME_B") -Sys.setenv(JAVA_HOME = a, PATH = "/usr/bin:/bin") -ns <- loadNamespace("rJava") -lib <- function(home) get(".find.jvm.library", ns)(home)[1] -state <- function() rJava::.jvmState()$state -mapped <- function() .Call(get("RgetMappedJVM", ns)) -reject <- function(path, pattern) { - error <- tryCatch(rJava::.jloadJVM(path), error = identity) - stopifnot(inherits(error, "error"), grepl(pattern, conditionMessage(error)), state() == "not-loaded") - cat("Expected refusal:", conditionMessage(error), "\n") -} -start <- function() { - rJava::.jinit() - actual <- rJava::.jcall("java/lang/System", "S", "getProperty", "java.home") - stopifnot(identical(normalizePath(actual), normalizePath(a))) -} -stopifnot(is.null(mapped()), state() == "not-loaded") -if (scenario == "fresh") { - start() # New selection works even on an older overlayfs kernel. -} else { - if (scenario == "hosted") { - dyn.load(Sys.getenv("RJE_JVMHOST")) - boot <- file.path(system.file("java", package = "rJava"), "boot") - stopifnot(.C("rjava_test_create_jvm", lib(a), paste0("-Djava.class.path=", boot), result = 0L)$result == 0L) - } else dyn.load(lib(a), local = TRUE) - stopifnot(!is.null(mapped()), mapped()$vms == if (scenario == "hosted") 1L else 0L) - # Record the actual mapping and overlay inode in CI, not just kernel versions. - cat(readLines("/proc/self/maps")[grepl("libjvm", readLines("/proc/self/maps"))], sep = "\n") - system2("stat", c("-Lc", shQuote("overlay device=%d inode=%i"), shQuote(lib(a)))) - reject(lib(b), different) - if (scenario == "replaced") { - # Replacing through the overlay triggers copy-up/whiteout behavior. - path <- lib(a) - stopifnot(unlink(path) == 0L, file.copy(lib(b), path)) - reject(path, if (expect == "legacy" || layout == "cross-off") ambiguous else different) - } else { - if (same_ambiguous) reject(lib(a), ambiguous) else { - stopifnot(isTRUE(rJava::.jloadJVM(lib(a)))) - } - if (scenario == "hosted") { - Sys.setenv(JAVA_HOME = b) - start() # Attach has no pathname comparison, including on old overlayfs. - stopifnot(state() == "attached") - } else if (!same_ambiguous) start() - } -} -cat("PASS overlay", expect, as.character(kernel), layout, scenario, "\n") diff --git a/tests-integration/xlsx-install.R b/tests-integration/xlsx-install.R deleted file mode 100644 index b6f743b..0000000 --- a/tests-integration/xlsx-install.R +++ /dev/null @@ -1,16 +0,0 @@ -# Installs the CRAN xlsx package and its test dependencies into a separate -# library, leaving the deferred-loading rJava on R_LIBS as the only rJava: -# Rscript tests-integration/xlsx-install.R -args <- commandArgs(TRUE) -lib <- args[1] -dir.create(lib, recursive = TRUE, showWarnings = FALSE) -dir.create(args[2], recursive = TRUE, showWarnings = FALSE) -# Under Git bash, R on Windows does not read the profile setup-r writes. -if (is.null(getOption("repos")) || identical(unname(getOption("repos")["CRAN"]), "@CRAN@")) - options(repos = c(CRAN = "https://cloud.r-project.org")) -# rJava is already installed, so it is not installed as a dependency. -install.packages(c("xlsxjars", "testthat", "rprojroot", "tibble", "knitr"), lib = lib) -# The source package from CRAN: the P3M Linux repositories serve binaries. -src <- download.packages("xlsx", args[2], repos = "https://cloud.r-project.org", type = "source")[, 2] -install.packages(src, lib = lib, repos = NULL, type = "source") -stopifnot(dir.exists(file.path(lib, "xlsx")), !dir.exists(file.path(lib, "rJava"))) diff --git a/tests-integration/xlsx.R b/tests-integration/xlsx.R deleted file mode 100644 index d7e121e..0000000 --- a/tests-integration/xlsx.R +++ /dev/null @@ -1,42 +0,0 @@ -# A CRAN package that starts Java when its namespace loads: xlsx's .onLoad() -# calls rJava::.jpackage(), which runs .jinit(). Run in a fresh process: -# Rscript --vanilla tests-integration/xlsx.R -# RJE_RJAVA_LIB names the library with the deferred-loading rJava build. -home_a <- Sys.getenv("RJE_JAVA_HOME_A") -home_b <- Sys.getenv("RJE_JAVA_HOME_B") -stopifnot(dir.exists(home_a), dir.exists(home_b)) -# A binary rJava installed as a dependency would make this test meaningless. -rjava_lib <- normalizePath(Sys.getenv("RJE_RJAVA_LIB"), winslash = "/") -copies <- .libPaths()[file.exists(file.path(.libPaths(), "rJava", "DESCRIPTION"))] -cat("rJava installed in:", copies, sep = "\n ") -if (!length(copies) || !identical(normalizePath(copies[1], winslash = "/"), rjava_lib)) - stop("the first rJava on the library path is not the one in ", rjava_lib) - -windows <- .Platform$OS.type == "windows" -Sys.setenv(PATH = if (windows) paste(R.home("bin"), file.path(Sys.getenv("SystemRoot"), "System32"), sep = ";") else "/usr/bin:/bin") -ns <- loadNamespace("rJava") -state <- function() get(".jvmState", ns)() -stopifnot(identical(state()$state, "not-loaded"), exists("RgetMappedJVM", ns)) -# JDK B, so that the build JDK (A in the current set) cannot be picked up by accident. -Sys.setenv(JAVA_HOME = home_b) -invisible(loadNamespace("xlsx")) -stopifnot(state()$initialized) -actual <- normalizePath(rJava::.jcall("java/lang/System", "S", "getProperty", "java.home"), winslash = "/") -expected <- normalizePath(home_b, winslash = "/") -if (!identical(actual, expected) && !identical(dirname(actual), expected)) - stop("java.home is ", actual, ", expected ", expected) -cat("xlsx started", rJava::.jcall("java/lang/System", "S", "getProperty", "java.version"), "from", actual, "\n") - -# Java was selected when xlsx loaded: another runtime cannot be selected later. -other <- get(".find.jvm.library", ns)(home_a)[1] -result <- try(rJava::.jloadJVM(other), silent = TRUE) -stopifnot(inherits(result, "try-error")) -cat("rejected:", substr(gsub("\\s+", " ", result), 1L, 240L), "\n") - -file <- tempfile(fileext = ".xlsx") -data <- data.frame(id = 1:150, value = iris$Sepal.Length, species = as.character(iris$Species)) -xlsx::write.xlsx(data, file, row.names = FALSE) -back <- xlsx::read.xlsx(file, 1, stringsAsFactors = FALSE) -stopifnot(identical(dim(back), dim(data)), all.equal(back$value, data$value), - identical(back$species, data$species)) -cat("xlsx round trip:", nrow(back), "rows\n") From c5834c8efa6c97239f9f922d5bdedd8e143cf30a Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Fri, 23 Oct 2026 17:02:43 +0200 Subject: [PATCH 61/63] fix: retain bundled Windows runtimes for later Java native loads --- NEWS | 8 +++++++ man/jinit.Rd | 8 +++++++ src/djni.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/djni.h | 4 +++- 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 1a4b992..93e78f8 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,14 @@ used, since namespace loading never requires a JVM. + o On Windows, retain the selected JDK's bundled MSVC runtime DLLs + for native libraries Java loads after startup or host attachment. + This also supports systems with missing or older system VC++ + runtimes, without changing PATH or retaining a search directory. + Already loaded runtimes keep precedence; System32 is used when + a bundled copy is absent. Windows JRI build policy is unchanged. + + 1.0-18 2026-04-08 o JRI: fix regression in 1.0-16 breaking older versions of R. diff --git a/man/jinit.Rd b/man/jinit.Rd index e0d5f83..daaca79 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -144,6 +144,14 @@ verified, explicit selection fails; attaching to an existing VM remains possible without a pathname comparison. + On Windows, rJava retains MSVC runtime DLLs used by the selected JDK so + later native Java loads can use its bundled copies even when system + copies are absent or older. An already loaded runtime keeps precedence; + otherwise the JDK's bundled copy is preferred, with System32 as fallback + when a bundled copy is absent. No persistent change to \code{PATH} or + DLL search directories is made. A JDK that omits a required runtime may + still need a suitable system redistributable. + NOTE: \code{.jinit()} can be called implicitly by other functions such as \code{\link{.jpackage}} or \code{\link{J}}. } diff --git a/src/djni.c b/src/djni.c index 8154f08..281f234 100644 --- a/src/djni.c +++ b/src/djni.c @@ -277,6 +277,68 @@ static int created_vms(HMODULE module) { created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"); return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } +/* Keep already loaded runtimes alive; otherwise prefer the selected JDK's + own set, as java.exe does. Only a missing bundled copy falls back to + System32. References last until process exit, without changing PATH or + retaining a DLL search directory. Optional load failures are diagnosed. */ +static void load_bundled_msvc(void) { + static const wchar_t *names[] = { + L"vcruntime140.dll", L"vcruntime140_1.dll", L"msvcp140.dll", + L"msvcp140_1.dll", L"msvcp140_2.dll", L"msvcp140_atomic_wait.dll" + }; + static HMODULE retained[sizeof(names) / sizeof(names[0])]; + wchar_t *path = module_path(jni_dl), *slash, *candidate; + size_t i, len; + if (!path) { + fprintf(stderr, "rJava: cannot determine the selected JVM's MSVC runtime directory (Windows error %lu).\n", (unsigned long) GetLastError()); + return; + } + slash = wcsrchr(path, L'\\'); + if (!slash) { free(path); return; } + *slash = 0; /* bin\server */ + slash = wcsrchr(path, L'\\'); + if (!slash) { free(path); return; } + *slash = 0; /* bin */ + len = wcslen(path); + candidate = malloc(sizeof(wchar_t) * (len + 64)); + if (candidate) { + for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + DWORD attrs, error; + char *name; + if (retained[i]) continue; + /* Unlike GetModuleHandleW, this owns a reference even if the + component that originally loaded the runtime is unloaded. */ + if (GetModuleHandleExW(0, names[i], &retained[i])) continue; + wcscpy(candidate, path); + candidate[len] = L'\\'; + wcscpy(candidate + len + 1, names[i]); + attrs = GetFileAttributesW(candidate); + error = attrs == INVALID_FILE_ATTRIBUTES ? GetLastError() : 0; + if (error && error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { + name = to_utf8(candidate); + fprintf(stderr, "rJava: cannot inspect bundled MSVC runtime %s (Windows error %lu).\n", + name ? name : "(unknown path)", (unsigned long) error); + free(name); + continue; + } + if (attrs != INVALID_FILE_ATTRIBUTES) + retained[i] = LoadLibraryExW(candidate, NULL, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + else + retained[i] = LoadLibraryExW(names[i], NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (retained[i]) continue; + error = GetLastError(); + /* A JDK need not use every optional runtime in the list. */ + if (attrs == INVALID_FILE_ATTRIBUTES && error == ERROR_MOD_NOT_FOUND) continue; + name = to_utf8(attrs != INVALID_FILE_ATTRIBUTES ? candidate : names[i]); + fprintf(stderr, "rJava: cannot retain MSVC runtime %s (Windows error %lu); later Java native loads may fail.\n", + name ? name : "(unknown path)", (unsigned long) error); + free(name); + } + free(candidate); + } else fprintf(stderr, "rJava: cannot prepare MSVC runtimes: out of memory.\n"); + free(path); +} static int load_library(const char *path) { wchar_t *wide = NULL, *full = NULL, *bin = NULL, *slash; struct jvm_image *jvms = NULL; @@ -951,6 +1013,9 @@ int djni_load(const char *path) { #ifdef __linux__ /* After load_syms(), so that a library that is no JVM is reported as such. */ if (!res && (res = check_jvm_name(path))) djni_unload(); +#endif +#ifdef _WIN32 + if (!res) load_bundled_msvc(); #endif return res; } @@ -962,6 +1027,9 @@ int djni_attach(void) { if (jni_dl) return -1; if ((res = attach_library())) return res; res = load_syms(); +#ifdef _WIN32 + if (!res) load_bundled_msvc(); +#endif return res; } int djni_unload(void) { diff --git a/src/djni.h b/src/djni.h index ed4e178..10d9776 100644 --- a/src/djni.h +++ b/src/djni.h @@ -16,7 +16,9 @@ the mapped file, whether or not it has created a VM. */ int djni_load(const char *path); /* Selects the only JVM library mapped into the process if it has created a - VM, by its mapped image: no path is interpreted and no JVM is loaded. */ + VM, by its mapped image: no path is interpreted and no JVM is loaded. + On Windows, selection and attachment retain the selected JDK's MSVC + runtime dependencies for subsequent native Java loads. */ int djni_attach(void); int djni_unload(void); const char* djni_last_error(void); From 76d01fbac2080171cd9f37e02c6d9e9dc3851044 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Fri, 23 Oct 2026 18:35:22 +0200 Subject: [PATCH 62/63] fix: preserve newer Windows runtimes and adopt preloaded JVMs --- NEWS | 27 ++++---- R/jinit.R | 19 ++++-- man/jinit.Rd | 31 ++++++---- src/Makevars.win | 2 +- src/Rglue.c | 6 +- src/djni.c | 158 ++++++++++++++++++++++++++++------------------- src/djni.h | 4 +- 7 files changed, 151 insertions(+), 96 deletions(-) diff --git a/NEWS b/NEWS index 93e78f8..6d6bc03 100644 --- a/NEWS +++ b/NEWS @@ -4,7 +4,7 @@ 1.0-19 (not yet released) o The JVM run-time is no longer linked into rJava or loaded when the namespace loads. The first .jinit() selects one: a JVM - already created in the process, then java.home (Windows) or + already mapped in the process, then java.home (Windows) or JAVA_HOME, the JavaSoft registry (Windows), /usr/libexec/java_home (macOS), R CMD config JAVA_HOME (Linux), and java on PATH. Linux thus retains the javareconf JDK as its @@ -16,14 +16,16 @@ o A selected JVM library stays selected for the life of the R process: .junloadJVM() fails once one is loaded, and another run-time requires a new R process. A JVM library already mapped - into the process is selected only by a path naming the same - file; a different or unverifiable file is refused before it is + into the process can be selected explicitly only by a path naming + the same file; a different or unverifiable file is refused before it is loaded. Device mismatches on btrfs and overlayfs are not evidence of a different file: matching inodes also require a loader reference to the same image, without loading another library. - o If native code in the process (e.g. a Java application that - embeds R through JRI) has created a VM, .jinit() attaches to it + o If native code in the process has already mapped one JVM library + (e.g. JavaGD), .jinit() adopts it even before a VM exists and warns + if Java discovery selects a different path. If a Java application + embedding R through JRI has created a VM, .jinit() attaches to it whatever JAVA_HOME names. o Changed behavior: the Java home is now chosen when Java is @@ -40,12 +42,15 @@ used, since namespace loading never requires a JVM. - o On Windows, retain the selected JDK's bundled MSVC runtime DLLs - for native libraries Java loads after startup or host attachment. - This also supports systems with missing or older system VC++ - runtimes, without changing PATH or retaining a search directory. - Already loaded runtimes keep precedence; System32 is used when - a bundled copy is absent. Windows JRI build policy is unchanged. + o On Windows, retain MSVC runtime DLLs for native libraries Java + loads after startup or host attachment. Already loaded runtimes + keep precedence. For each runtime the JDK bundles, select the + newer of System32 and the bundled copy before loading jvm.dll, + preferring System32 on ties or unknown versions. This preserves + newer system runtimes for other native packages while supporting + missing or incomplete system VC++ installations. Retention lasts + for the process lifetime without changing PATH or retaining a + search directory. Windows JRI build policy is unchanged. 1.0-18 2026-04-08 diff --git a/R/jinit.R b/R/jinit.R index 63c5ca7..aaddc6d 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -16,15 +16,24 @@ .Call(RunloadJVM) .jfindAndLoadJVM <- function() { - ## A Java application embedding R (e.g. JRI) has already created a JVM. It - ## is the only runtime this process can use, so attach to it whatever Java - ## home is configured, as rJava linked against the JVM library did. The - ## mapped image is selected itself, not through its name: .jloadJVM(path) - ## accepts only a path that names the mapped file. + ## A mapped JVM is the only runtime this process can use, even before + ## a VM exists (e.g. JavaGD links libjvm). Select the held image itself; + ## explicit .jloadJVM(path) still requires a path naming that same file. mapped <- .Call(RgetMappedJVM) if (!is.null(mapped) && isTRUE(mapped$vms > 0L)) return(.Call(RattachJVM)) home <- .find.java.runtime() + if (!is.null(mapped)) { + discovered <- if (nzchar(home)) .find.jvm.library(home) else character() + if (length(discovered) && + !identical(normalizePath(discovered[1], winslash="/"), + normalizePath(mapped$path, winslash="/", mustWork=FALSE))) + warning("Using the JVM library already mapped into this process: ", mapped$path, + "; Java discovery selected ", discovered[1], + ". To select that runtime, start a new R process before loading packages that use Java.", + call.=FALSE) + return(.Call(RattachJVM)) + } if (!nzchar(home)) stop("Cannot find Java. Set JAVA_HOME to an existing Java home before initializing the JVM.") libjvm <- .find.jvm.library(home) diff --git a/man/jinit.Rd b/man/jinit.Rd index daaca79..f0f899d 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -105,8 +105,9 @@ Loading the rJava namespace does not select or load a JVM run-time. The first call to \code{.jinit} selects one, in this order: - a JVM already created in the process (e.g. when R is embedded in Java - via JRI), to which rJava attaches regardless of other settings; + the only JVM library already mapped in the process (e.g. by JavaGD), + even if no VM exists yet; if a VM exists (e.g. when R is embedded in + Java via JRI), rJava attaches regardless of other settings; otherwise the \code{java.home} option (Windows only) or the \code{JAVA_HOME} environment variable; then, on Windows, the \code{CurrentVersion} of the JavaSoft registry keys; on macOS, @@ -127,8 +128,11 @@ mapped, so retrying with any run-time then requires a new R process. If a JVM library is already mapped into the process, a JVM library path, such as one given to \code{.jloadJVM}, selects it only if it - names that same file, also when a JVM was created in it; attaching to - a created JVM is done by \code{.jinit} without a path. + names that same file, also when a JVM was created in it. Automatic + selection by \code{.jinit} adopts the mapped image without opening its + recorded path and warns if Java discovery selects a different path. + To select another JVM, start a new R process before loading packages + that map a JVM library. \code{.jloadJVM} selects a library without starting a VM and returns \code{TRUE} on success, or a status code when \code{silent = TRUE}. @@ -144,13 +148,18 @@ verified, explicit selection fails; attaching to an existing VM remains possible without a pathname comparison. - On Windows, rJava retains MSVC runtime DLLs used by the selected JDK so - later native Java loads can use its bundled copies even when system - copies are absent or older. An already loaded runtime keeps precedence; - otherwise the JDK's bundled copy is preferred, with System32 as fallback - when a bundled copy is absent. No persistent change to \code{PATH} or - DLL search directories is made. A JDK that omits a required runtime may - still need a suitable system redistributable. + On Windows, rJava retains MSVC runtime DLLs for later native Java + loads. Already loaded runtimes keep precedence. Otherwise, for each + runtime the JDK bundles, rJava selects the newer of System32 and the + bundled copy before loading \code{jvm.dll}, preferring System32 on a + tie or unknown version. Unbundled, unloaded runtimes are left to the + Windows loader. Retained copies stay loaded for the process lifetime; + this also determines which copies later native packages use. No + persistent change to \code{PATH} or DLL search directories is made. + A JDK that omits a required runtime may still need a suitable system + redistributable. A runtime already loaded by a host is not replaced, + even if a newer copy exists. + NOTE: \code{.jinit()} can be called implicitly by other functions such as \code{\link{.jpackage}} or \code{\link{J}}. diff --git a/src/Makevars.win b/src/Makevars.win index 34b3aab..4c6cdb4 100644 --- a/src/Makevars.win +++ b/src/Makevars.win @@ -23,7 +23,7 @@ $(warning JAVA_HOME is $(JAVA_HOME)) # debugging flags like -DRJ_DEBUG or -DRJ_PROFILE to PKG_CFLAGS JAVA_INCLUDES=$(JAVA_HOME)/include $(JAVA_HOME)/include/win32 PKG_CPPFLAGS = -D_R_ -DWin32 -Ijvm-w32 -I$(RHOME)/src/include $(JAVA_INCLUDES:%=-I%) -PKG_LIBS = -Ljvm-w32 +PKG_LIBS = -Ljvm-w32 -lversion $(warning PKG_CPPFLAGS are $(PKG_CPPFLAGS)) diff --git a/src/Rglue.c b/src/Rglue.c index b826d1c..831f680 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1165,9 +1165,9 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { return Rf_ScalarLogical(1); } -/* Selects the JVM library that created the VM already running in the - process, e.g. in a Java application embedding R. Unlike RloadJVM, no path - names it: the file at its recorded name may have been replaced since. */ +/* Selects the only mapped JVM library, e.g. from JavaGD or a Java host. + Unlike RloadJVM, no path names it: the file at its recorded name may have + been replaced since. The VM may not have been started yet. */ REPC SEXP RattachJVM(void) { load_error(djni_attach()); return Rf_ScalarLogical(1); diff --git a/src/djni.c b/src/djni.c index 281f234..4806217 100644 --- a/src/djni.c +++ b/src/djni.c @@ -277,66 +277,89 @@ static int created_vms(HMODULE module) { created = (JNI_GetCreatedJavaVMs_fnptr) GetProcAddress(module, "JNI_GetCreatedJavaVMs"); return created && created(&vm, 1, &count) == JNI_OK ? (int) count : -1; } -/* Keep already loaded runtimes alive; otherwise prefer the selected JDK's - own set, as java.exe does. Only a missing bundled copy falls back to - System32. References last until process exit, without changing PATH or - retaining a DLL search directory. Optional load failures are diagnosed. */ -static void load_bundled_msvc(void) { +/* File versions let an up-to-date system runtime take precedence over an + older JDK copy. Unknown versions are not evidence that the JDK is newer. */ +static ULONGLONG dll_version(const wchar_t *path) { + DWORD ignored, size = GetFileVersionInfoSizeW(path, &ignored); + VS_FIXEDFILEINFO *info; + UINT len; + ULONGLONG version = 0; + void *data = size ? malloc(size) : NULL; + if (data && GetFileVersionInfoW(path, 0, size, data) && + VerQueryValueW(data, L"\\", (void **) &info, &len) && + len >= sizeof(*info) && info->dwSignature == 0xfeef04bd) + version = ((ULONGLONG) info->dwFileVersionMS << 32) | info->dwFileVersionLS; + free(data); + return version; +} +/* Retain loaded runtimes without replacing a host's choice. For each DLL + the JDK bundles, otherwise select the newer of it and System32, preferring + System32 on a tie or unknown version. Do this before jvm.dll binds imports. + Unbundled, unloaded runtimes remain the loader's responsibility. References + last until process exit, without changing PATH or DLL search directories. */ +static void retain_msvc(const wchar_t *bin) { static const wchar_t *names[] = { L"vcruntime140.dll", L"vcruntime140_1.dll", L"msvcp140.dll", L"msvcp140_1.dll", L"msvcp140_2.dll", L"msvcp140_atomic_wait.dll" }; static HMODULE retained[sizeof(names) / sizeof(names[0])]; - wchar_t *path = module_path(jni_dl), *slash, *candidate; - size_t i, len; - if (!path) { - fprintf(stderr, "rJava: cannot determine the selected JVM's MSVC runtime directory (Windows error %lu).\n", (unsigned long) GetLastError()); + wchar_t system[MAX_PATH + 64], *bundled; + UINT slen = GetSystemDirectoryW(system, MAX_PATH); + size_t i, blen = wcslen(bin); + if (!slen || slen >= MAX_PATH || !(bundled = malloc(sizeof(wchar_t) * (blen + 64)))) { + fprintf(stderr, "rJava: cannot prepare MSVC runtime paths.\n"); return; } - slash = wcsrchr(path, L'\\'); - if (!slash) { free(path); return; } - *slash = 0; /* bin\server */ - slash = wcsrchr(path, L'\\'); - if (!slash) { free(path); return; } - *slash = 0; /* bin */ - len = wcslen(path); - candidate = malloc(sizeof(wchar_t) * (len + 64)); - if (candidate) { - for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { - DWORD attrs, error; - char *name; - if (retained[i]) continue; - /* Unlike GetModuleHandleW, this owns a reference even if the - component that originally loaded the runtime is unloaded. */ - if (GetModuleHandleExW(0, names[i], &retained[i])) continue; - wcscpy(candidate, path); - candidate[len] = L'\\'; - wcscpy(candidate + len + 1, names[i]); - attrs = GetFileAttributesW(candidate); - error = attrs == INVALID_FILE_ATTRIBUTES ? GetLastError() : 0; - if (error && error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { - name = to_utf8(candidate); - fprintf(stderr, "rJava: cannot inspect bundled MSVC runtime %s (Windows error %lu).\n", - name ? name : "(unknown path)", (unsigned long) error); - free(name); - continue; - } - if (attrs != INVALID_FILE_ATTRIBUTES) - retained[i] = LoadLibraryExW(candidate, NULL, - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); - else - retained[i] = LoadLibraryExW(names[i], NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - if (retained[i]) continue; + wcscpy(bundled, bin); + bundled[blen] = L'\\'; + system[slen] = L'\\'; + for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + const wchar_t *choice; + DWORD attrs, error; + char *name; + if (retained[i]) continue; + /* Own a reference even if the original loader later releases its DLL. */ + if (GetModuleHandleExW(0, names[i], &retained[i])) continue; + wcscpy(bundled + blen + 1, names[i]); + wcscpy(system + slen + 1, names[i]); + attrs = GetFileAttributesW(bundled); + if (attrs == INVALID_FILE_ATTRIBUTES) { error = GetLastError(); - /* A JDK need not use every optional runtime in the list. */ - if (attrs == INVALID_FILE_ATTRIBUTES && error == ERROR_MOD_NOT_FOUND) continue; - name = to_utf8(attrs != INVALID_FILE_ATTRIBUTES ? candidate : names[i]); - fprintf(stderr, "rJava: cannot retain MSVC runtime %s (Windows error %lu); later Java native loads may fail.\n", + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) continue; + name = to_utf8(bundled); + fprintf(stderr, "rJava: cannot inspect bundled MSVC runtime %s (Windows error %lu).\n", name ? name : "(unknown path)", (unsigned long) error); free(name); + continue; } - free(candidate); - } else fprintf(stderr, "rJava: cannot prepare MSVC runtimes: out of memory.\n"); + choice = bundled; + if (GetFileAttributesW(system) != INVALID_FILE_ATTRIBUTES) { + ULONGLONG vb = dll_version(bundled), vs = dll_version(system); + if (!vb || !vs || vs >= vb) choice = system; + } + retained[i] = LoadLibraryExW(choice, NULL, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + if (retained[i]) continue; + error = GetLastError(); + name = to_utf8(choice); + fprintf(stderr, "rJava: cannot retain MSVC runtime %s (Windows error %lu); later Java native loads may fail.\n", + name ? name : "(unknown path)", (unsigned long) error); + free(name); + } + free(bundled); +} +static void load_bundled_msvc(void) { + wchar_t *path = module_path(jni_dl), *slash; + if (!path) { + fprintf(stderr, "rJava: cannot determine the selected JVM's MSVC runtime directory (Windows error %lu).\n", (unsigned long) GetLastError()); + return; + } + slash = wcsrchr(path, L'\\'); + if (slash) { + *slash = 0; /* bin\\server */ + slash = wcsrchr(path, L'\\'); + if (slash) { *slash = 0; retain_msvc(path); } + } free(path); } static int load_library(const char *path) { @@ -405,6 +428,7 @@ static int load_library(const char *path) { *slash = 0; if ((slash = wcsrchr(bin, L'\\'))) { *slash = 0; + retain_msvc(bin); cookie = AddDllDirectory(bin); } } @@ -418,13 +442,12 @@ static int load_library(const char *path) { free(wide); free(full); free(bin); return res; } -/* Takes a reference to the only mapped JVM module if it has created a VM. - The module's name is not checked: no VM is started, so the JDK's DLLs, - which import jvm.dll by name, are not loaded again. */ +/* Select the only mapped JVM module. An unstarted module must have its + usual name, since the JDK DLLs import jvm.dll by name when starting a VM. */ static int attach_library(void) { struct jvm_image *jvms; char *name; - int n = find_jvms(&jvms), res = -4; + int n = find_jvms(&jvms), res = -4, count = -1; if (n < 0) { save_win_error("Cannot list the modules of", "this process", GetLastError()); return -4; @@ -435,8 +458,11 @@ static int attach_library(void) { else if (n > 1) snprintf(last_error, sizeof(last_error), "%d JVM libraries are already mapped, including %s. Use a new R process.", n, name ? name : "unknown path"); - else if (created_vms(jvms[0].handle) <= 0) - snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", + else if ((count = created_vms(jvms[0].handle)) < 0) + snprintf(last_error, sizeof(last_error), "Cannot determine the VM count of the mapped JVM library: %s.", + name ? name : "unknown path"); + else if (!count && (!jvms[0].path || !is_jvm_name(jvms[0].path))) + snprintf(last_error, sizeof(last_error), "The JVM library is already mapped under another name: %s. Use a new R process.", name ? name : "unknown path"); else { jni_dl = jvms[0].handle; @@ -902,13 +928,14 @@ static int load_library(const char *path) { free_jvms(jvms, n); return res; } -/* Takes the reference held to the only mapped JVM image if it has created - a VM. Its recorded name is not interpreted: the file there may have been - deleted or replaced, and on overlayfs the mapped file may be unverifiable, - but the created VM is the image's own. */ +#ifdef __linux__ +static int check_jvm_name(const char *path); +#endif +/* Select the only mapped JVM image using a held reference, without opening + its recorded path: the file there may have been deleted or replaced. */ static int attach_library(void) { struct jvm_image *jvms; - int n = find_jvms(&jvms), res = -4; + int n = find_jvms(&jvms), res = -4, count = -1; if (n < 0) { use_new_process(); return -4; @@ -920,12 +947,17 @@ static int attach_library(void) { n, jvms[0].name); else if (!jvms[0].handle) snprintf(last_error, sizeof(last_error), "Cannot reference the JVM library mapped into this process: %s. Use a new R process.", jvms[0].name); - else if (created_vms(&jvms[0]) <= 0) - snprintf(last_error, sizeof(last_error), "The JVM library mapped into this process has created no VM: %s.", jvms[0].name); + else if ((count = created_vms(&jvms[0])) < 0) + snprintf(last_error, sizeof(last_error), "Cannot determine the VM count of the mapped JVM library: %s.", jvms[0].name); else { jni_dl = jvms[0].handle; jvms[0].handle = NULL; res = 0; +#ifdef __linux__ + /* Starting an unstarted JVM still requires its dependencies to bind + to this very image, including on musl and with renamed libraries. */ + if (!count && (res = check_jvm_name(jvms[0].name))) djni_unload(); +#endif } free_jvms(jvms, n); return res; @@ -1020,7 +1052,7 @@ int djni_load(const char *path) { return res; } /* returns: 0 = success, -1 = already loaded, -3 = symbols not found, -4 = no - single mapped JVM library with a created VM */ + single verifiable mapped JVM library */ int djni_attach(void) { int res; last_error[0] = 0; diff --git a/src/djni.h b/src/djni.h index 10d9776..e8795e9 100644 --- a/src/djni.h +++ b/src/djni.h @@ -15,8 +15,8 @@ /* Selects the JVM library at path. If one is already mapped, path must name the mapped file, whether or not it has created a VM. */ int djni_load(const char *path); -/* Selects the only JVM library mapped into the process if it has created a - VM, by its mapped image: no path is interpreted and no JVM is loaded. +/* Selects the only JVM library mapped into the process, whether or not it + has created a VM, by its held image: no path is opened and no JVM is loaded. On Windows, selection and attachment retain the selected JDK's MSVC runtime dependencies for subsequent native Java loads. */ int djni_attach(void); From dbe0c72d263eb07aeefd8d1a5b81839452ec5ba7 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Fri, 23 Oct 2026 21:54:24 +0200 Subject: [PATCH 63/63] fix: report JVM adoption only after successful selection --- R/jinit.R | 3 ++- man/jinit.Rd | 6 ++++-- src/Rglue.c | 6 +++--- src/djni.c | 5 +++-- src/djni.h | 7 ++++--- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/R/jinit.R b/R/jinit.R index aaddc6d..274c1b2 100644 --- a/R/jinit.R +++ b/R/jinit.R @@ -24,6 +24,7 @@ return(.Call(RattachJVM)) home <- .find.java.runtime() if (!is.null(mapped)) { + selected <- .Call(RattachJVM) discovered <- if (nzchar(home)) .find.jvm.library(home) else character() if (length(discovered) && !identical(normalizePath(discovered[1], winslash="/"), @@ -32,7 +33,7 @@ "; Java discovery selected ", discovered[1], ". To select that runtime, start a new R process before loading packages that use Java.", call.=FALSE) - return(.Call(RattachJVM)) + return(selected) } if (!nzchar(home)) stop("Cannot find Java. Set JAVA_HOME to an existing Java home before initializing the JVM.") diff --git a/man/jinit.Rd b/man/jinit.Rd index f0f899d..990927c 100644 --- a/man/jinit.Rd +++ b/man/jinit.Rd @@ -129,8 +129,10 @@ If a JVM library is already mapped into the process, a JVM library path, such as one given to \code{.jloadJVM}, selects it only if it names that same file, also when a JVM was created in it. Automatic - selection by \code{.jinit} adopts the mapped image without opening its - recorded path and warns if Java discovery selects a different path. + selection by \code{.jinit} adopts the mapped image without loading another + JVM and, after successful selection, warns if Java discovery selects a + different path. Loader lookups may inspect the recorded path when acquiring + a reference to the mapped image. To select another JVM, start a new R process before loading packages that map a JVM library. diff --git a/src/Rglue.c b/src/Rglue.c index 831f680..df73eb6 100644 --- a/src/Rglue.c +++ b/src/Rglue.c @@ -1165,9 +1165,9 @@ REPC SEXP RloadJVM(SEXP sPath, SEXP sSilent) { return Rf_ScalarLogical(1); } -/* Selects the only mapped JVM library, e.g. from JavaGD or a Java host. - Unlike RloadJVM, no path names it: the file at its recorded name may have - been replaced since. The VM may not have been started yet. */ +/* Selects the only mapped JVM library, e.g. from JavaGD or a Java host, + without loading another JVM. Loader lookups may inspect its recorded path + to acquire a reference. The VM may not have been started yet. */ REPC SEXP RattachJVM(void) { load_error(djni_attach()); return Rf_ScalarLogical(1); diff --git a/src/djni.c b/src/djni.c index 4806217..52ded3c 100644 --- a/src/djni.c +++ b/src/djni.c @@ -931,8 +931,9 @@ static int load_library(const char *path) { #ifdef __linux__ static int check_jvm_name(const char *path); #endif -/* Select the only mapped JVM image using a held reference, without opening - its recorded path: the file there may have been deleted or replaced. */ +/* Select the only mapped JVM image using a held reference, without loading + another JVM. Acquiring the reference may inspect the recorded path, whose + file may have been deleted or replaced. */ static int attach_library(void) { struct jvm_image *jvms; int n = find_jvms(&jvms), res = -4, count = -1; diff --git a/src/djni.h b/src/djni.h index e8795e9..cd29f84 100644 --- a/src/djni.h +++ b/src/djni.h @@ -16,9 +16,10 @@ the mapped file, whether or not it has created a VM. */ int djni_load(const char *path); /* Selects the only JVM library mapped into the process, whether or not it - has created a VM, by its held image: no path is opened and no JVM is loaded. - On Windows, selection and attachment retain the selected JDK's MSVC - runtime dependencies for subsequent native Java loads. */ + has created a VM, by its held image: no additional JVM is loaded. + A loader lookup may inspect its recorded path to acquire the reference. + On Windows, selection and attachment retain MSVC runtimes from the JDK + or System32, preserving already loaded copies, for later Java native loads. */ int djni_attach(void); int djni_unload(void); const char* djni_last_error(void);