diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 00000000..744cfe36 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,170 @@ +name: Build and test pullrequest + +on: + pull_request: + branches: [ master ] +permissions: + contents: read + +env: + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + +jobs: + test-macos: + runs-on: macos-14 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Build macOS natives + run: | + # See https://github.com/actions/virtual-environments/issues/2557 + sudo mv /Library/Developer/CommandLineTools/SDKs/* /tmp + sudo mv /Applications/Xcode.app /Applications/Xcode.app.bak + sudo mv /Applications/Xcode_15.4.0.app /Applications/Xcode.app + sudo xcode-select -switch /Applications/Xcode.app + /usr/bin/xcodebuild -version + ./gradlew build_macos + ./gradlew jniGen jnigenBuildAllMacOsX + + - name: Run test on macos + run: | + ./gradlew test + + test-linux: + runs-on: ubuntu-20.04 + container: + image: ubuntu:18.04 + steps: + - name: Install dependencies into minimal dockerfile + run: | + # ubuntu dockerfile is very minimal (only 122 packages are installed) + # need to install updated git (from official git ppa) + apt update + apt install -y software-properties-common + add-apt-repository ppa:git-core/ppa -y + # install dependencies expected by other steps + apt update + apt install -y git \ + curl \ + ca-certificates \ + wget \ + bzip2 \ + zip \ + unzip \ + xz-utils \ + maven \ + ant sudo locales make texinfo + + # set Locale to en_US.UTF-8 (avoids hang during compilation) + locale-gen en_US.UTF-8 + echo "LANG=en_US.UTF-8" >> $GITHUB_ENV + echo "LANGUAGE=en_US.UTF-8" >> $GITHUB_ENV + echo "LC_ALL=en_US.UTF-8" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Install cross-compilation toolchains + run: | + sudo apt update + sudo apt install -y --force-yes gcc g++ + sudo apt install -y --force-yes gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross + sudo apt install -y --force-yes gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-dev-armhf-cross + sudo apt install -y --force-yes gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libc6-dev-riscv64-cross + + - name: Build Linux natives + run: | + ./gradlew build_linux + ./gradlew jnigen jnigenBuildAllLinux + + - name: Run test on linux + run: | + ./gradlew test + + natives-windows: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Install cross-compilation toolchains + run: | + sudo apt update + sudo apt install -y --force-yes mingw-w64 lib32z1 + + - name: Build Windows natives + run: | + ./gradlew build_windows + ./gradlew jnigen jnigenBuildAllWindows + + - name: Pack artifacts + run: | + find . -name "*.a" -o -name "*.dll" -o -name "*.dylib" -o -name "*.so" | grep "libs" > native-files-list + zip natives-windows -@ < native-files-list + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: natives-windows.zip + path: natives-windows.zip + + test-windows: + runs-on: windows-latest + needs: [natives-windows] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Download natives-windows artifact + uses: actions/download-artifact@v3 + with: + name: natives-windows.zip + + - name: Run windows tests + run: | + ./gradlew test \ No newline at end of file diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index 5c8889d5..2d12c475 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -243,9 +243,36 @@ jobs: name: natives-android.zip path: natives-android.zip + test-windows: + runs-on: windows-latest + needs: [natives-windows] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Download natives-windows artifact + uses: actions/download-artifact@v3 + with: + name: natives-windows.zip + + - name: Run windows tests + run: | + ./gradlew test + publish: runs-on: ubuntu-20.04 - needs: [natives-macos, natives-linux, natives-windows, natives-ios, natives-android] + needs: [natives-macos, natives-linux, natives-windows, natives-ios, natives-android, test-windows] steps: - uses: actions/checkout@v2 with: diff --git a/gdx-jnigen-generator-test/build.gradle b/gdx-jnigen-generator-test/build.gradle index 59e3a72b..70035942 100644 --- a/gdx-jnigen-generator-test/build.gradle +++ b/gdx-jnigen-generator-test/build.gradle @@ -21,7 +21,7 @@ dependencies { testRuntimeOnly fileTree(dir: file("build/jnigen/libs"), include: '*natives-desktop.jar') if (HostDetection.os === Os.Windows) { testImplementation "com.badlogicgames.jnigen:jnigen-runtime:3.0.0-SNAPSHOT" - testImplementation "com.badlogicgames.jnigen:jnigen-runtime:3.0.0-SNAPSHOT:natives-desktop" + testImplementation "com.badlogicgames.jnigen:jnigen-runtime-platform:3.0.0-SNAPSHOT:natives-desktop" } else { testImplementation project(path: ":jnigen-runtime") testImplementation project(path: ":jnigen-runtime", configuration: "archives") diff --git a/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/Constants.java b/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/Constants.java index 4633857a..6f573bf3 100644 --- a/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/Constants.java +++ b/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/Constants.java @@ -18,7 +18,7 @@ public final class Constants { public static final byte UNSIGNED_INT = 10; - public static final byte boolean_r = 10; + public static final byte final_r = 10; public static final byte special1 = 10; diff --git a/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/TestData.java b/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/TestData.java index 6b021a82..9337d601 100644 --- a/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/TestData.java +++ b/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/generated/TestData.java @@ -885,6 +885,16 @@ public static boolean validateString(CSizedIntPointer str) { return 0; */ + public static void call_callback_in_thread(ClosureObject thread_callback) { + call_callback_in_thread_internal(thread_callback.getFnPtr()); + } + + static private native void call_callback_in_thread_internal(long thread_callback);/* + HANDLE_JAVA_EXCEPTION_START() + call_callback_in_thread((void *(*)(void *))thread_callback); + HANDLE_JAVA_EXCEPTION_END() + */ + public interface methodWithCallbackBooleanArg extends com.badlogic.gdx.jnigen.runtime.closure.Closure { com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] __ffi_cache = new com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] { FFITypes.getCTypeInfo(-2), FFITypes.getCTypeInfo(0) }; @@ -1354,6 +1364,21 @@ default void invoke(com.badlogic.gdx.jnigen.runtime.ffi.JavaTypeWrapper[] parame } } + public interface thread_callback extends com.badlogic.gdx.jnigen.runtime.closure.Closure { + + com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] __ffi_cache = new com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] { FFITypes.getCTypeInfo(-1), FFITypes.getCTypeInfo(-1) }; + + VoidPointer thread_callback_call(VoidPointer arg0); + + default com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] functionSignature() { + return __ffi_cache; + } + + default void invoke(com.badlogic.gdx.jnigen.runtime.ffi.JavaTypeWrapper[] parameters, com.badlogic.gdx.jnigen.runtime.ffi.JavaTypeWrapper returnType) { + returnType.setValue(thread_callback_call(new VoidPointer(parameters[0].asLong(), false))); + } + } + public interface methodWithCallbackTestStructReturn extends com.badlogic.gdx.jnigen.runtime.closure.Closure { com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] __ffi_cache = new com.badlogic.gdx.jnigen.runtime.c.CTypeInfo[] { FFITypes.getCTypeInfo(19) }; diff --git a/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/tests/ClosureTest.java b/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/tests/ClosureTest.java index 426b8961..a9d4c996 100644 --- a/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/tests/ClosureTest.java +++ b/gdx-jnigen-generator-test/src/test/java/com/badlogic/jnigen/tests/ClosureTest.java @@ -229,4 +229,17 @@ public void testCallbackDoubleReturn() { assertEquals(55.55, ret); closureObject.free(); } + + @Test + public void closureFromThread() { + AtomicReference spawnedThreadName = new AtomicReference<>(); + ClosureObject closureObject = ClosureObject.fromClosure((arg) -> { + spawnedThreadName.set(Thread.currentThread().getName()); + return arg; + }); + call_callback_in_thread(closureObject); + assertNotEquals(spawnedThreadName.get(), Thread.currentThread().getName()); + assertTrue(spawnedThreadName.get().startsWith("Thread-")); + closureObject.free(); + } } diff --git a/gdx-jnigen-generator-test/src/test/resources/test_data.cpp b/gdx-jnigen-generator-test/src/test/resources/test_data.cpp index 3908443b..8b406ef5 100644 --- a/gdx-jnigen-generator-test/src/test/resources/test_data.cpp +++ b/gdx-jnigen-generator-test/src/test/resources/test_data.cpp @@ -414,4 +414,27 @@ void ensureParsed(AnonymousStructNoField, AnonymousStructField, AnonymousStructF void weirdPointer(FILE *_file) {} void constArrayParameter(const TestStruct structs[]) {} +#ifdef _WIN32 +#include + +DWORD WINAPI thread_wrapper(LPVOID param) { + auto callback = reinterpret_cast(param); + callback(nullptr); + return 0; +} + +void call_callback_in_thread(void* (*thread_callback)(void*)) { + HANDLE thread = CreateThread(NULL, 0, thread_wrapper, reinterpret_cast(thread_callback), 0, NULL); + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); +} +#else +#include + +void call_callback_in_thread(void* (*thread_callback)(void*)) { + pthread_t thread; + pthread_create(&thread, NULL, thread_callback, NULL); + pthread_join(thread, NULL); +} +#endif diff --git a/gdx-jnigen-generator-test/src/test/resources/test_data.h b/gdx-jnigen-generator-test/src/test/resources/test_data.h index 076b16fe..e17c3e1b 100644 --- a/gdx-jnigen-generator-test/src/test/resources/test_data.h +++ b/gdx-jnigen-generator-test/src/test/resources/test_data.h @@ -16,7 +16,7 @@ extern "C" { #define SIGNED_DOUBLE -10.5 #define UNSIGNED_HEX_INT 0x5B #define SIGNED_HEX_INT -0x5B -#define boolean 10 +#define final 10 #define special1 10L #define special2 10LLU #define special3 10.5 @@ -268,6 +268,7 @@ const char* returnThrownCauseMessage(methodWithThrowingCallback fnPtr); char* returnString(void); bool validateString(char* str); +void call_callback_in_thread(void* (*thread_callback)(void*)); #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/gdx-jnigen-runtime/build.gradle b/gdx-jnigen-runtime/build.gradle index e2e2f1fc..305f003f 100644 --- a/gdx-jnigen-runtime/build.gradle +++ b/gdx-jnigen-runtime/build.gradle @@ -184,6 +184,7 @@ jnigen { headerDirs += ["build/libffi-build/${combined}/include/", "src/main/native"] cppIncludes += ["src/main/native/*.cpp"] + cIncludes += ["src/main/native/*.c"] cFlags += " -std=c11 -fexceptions " cppFlags += " -std=c++11 -fexceptions " libraries += file("build/libffi-build/${combined}/lib/libffi.a").absolutePath diff --git a/gdx-jnigen-runtime/src/main/native/CHandler.cpp b/gdx-jnigen-runtime/src/main/native/CHandler.cpp index 701cc93a..a65d565e 100644 --- a/gdx-jnigen-runtime/src/main/native/CHandler.cpp +++ b/gdx-jnigen-runtime/src/main/native/CHandler.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef __linux__ #include @@ -90,7 +91,8 @@ static inline void calculateAlignmentAndOffset(ffi_type* type, bool isStruct) { } void callbackHandler(ffi_cif* cif, void* result, void** args, void* user) { - ATTACH_ENV() + JNIEnv* env; + tls_attach_jni_env(gJVM, &env); closure_info* info = (closure_info*) user; char backingBuffer[info->argumentSize]; jobject jBuffer = NULL; @@ -130,7 +132,6 @@ void callbackHandler(ffi_cif* cif, void* result, void** args, void* user) { } else { ENDIAN_INTCPY(result, rtype->size, &ret, sizeof(jlong)); } - DETACH_ENV() } JNIEXPORT jint JNICALL Java_com_badlogic_gdx_jnigen_runtime_CHandler_getPointerSize(JNIEnv* env, jclass clazz) { @@ -372,3 +373,11 @@ JNIEXPORT jlong JNICALL Java_com_badlogic_gdx_jnigen_runtime_CHandler_clone(JNIE return reinterpret_cast(dst); } +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + init_tls(); + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { + cleanup_tls(); +} \ No newline at end of file diff --git a/gdx-jnigen-runtime/src/main/native/CHandler.h b/gdx-jnigen-runtime/src/main/native/CHandler.h index b515f2b1..63f39b00 100644 --- a/gdx-jnigen-runtime/src/main/native/CHandler.h +++ b/gdx-jnigen-runtime/src/main/native/CHandler.h @@ -207,6 +207,9 @@ JNIEXPORT jlong JNICALL Java_com_badlogic_gdx_jnigen_runtime_CHandler_clone JNIEXPORT jlong JNICALL Java_com_badlogic_gdx_jnigen_runtime_CHandler_convertNativeTypeToFFIType (JNIEnv *, jclass, jlong); +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved); +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved); + #ifdef __cplusplus } #endif diff --git a/gdx-jnigen-runtime/src/main/native/jni_env_tls.c b/gdx-jnigen-runtime/src/main/native/jni_env_tls.c new file mode 100644 index 00000000..1b2aebf1 --- /dev/null +++ b/gdx-jnigen-runtime/src/main/native/jni_env_tls.c @@ -0,0 +1,110 @@ +#include "jni_env_tls.h" +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +#ifdef __ANDROID__ +#define THREAD_ATTACH_MACRO (*vm)->AttachCurrentThreadAsDaemon(vm, env, NULL); +#else +#define THREAD_ATTACH_MACRO (*vm)->AttachCurrentThreadAsDaemon(vm, (void**)env, NULL); +#endif + +#define HANDLE_RESULT(res, msg) \ + if (res != JNI_OK) { \ + fprintf(stderr, msg ": %d", res); \ + fflush(stderr); \ + exit(1); \ + } + +#ifdef _WIN32 +DWORD envTls = TLS_OUT_OF_INDEXES; + +BOOL WINAPI DllMain(HINSTANCE hDLL, DWORD fdwReason, LPVOID lpvReserved) { + // Docs say no cleanup when lpvReserved != NULL + if (fdwReason == DLL_THREAD_DETACH && lpvReserved == NULL) { + ThreadData* t_data = (ThreadData*)TlsGetValue(envTls); + detach_jni_env(t_data); + } + + return TRUE; +} + +void init_tls() { + if (envTls != TLS_OUT_OF_INDEXES) { + HANDLE_RESULT(-1, "TLS got double initialized") + } + envTls = TlsAlloc(); + if (envTls == TLS_OUT_OF_INDEXES) { + HANDLE_RESULT(-1, "Failed to allocate TLS") + } +} + +void cleanup_tls() { + if (envTls != TLS_OUT_OF_INDEXES) { + BOOL res = TlsFree(envTls); + HANDLE_RESULT(res, "Failed to deallocate TLS") + } +} + +void set_tls(ThreadData* t_data) { + if (envTls == TLS_OUT_OF_INDEXES) + init_tls(); + BOOL res = TlsSetValue(envTls, (LPVOID)t_data);; + HANDLE_RESULT(res, "Failed to set thread data") +} + +#else +pthread_key_t envTlsKey = 0; + +void init_tls() { + HANDLE_RESULT(envTlsKey, "TLS got double initialized") + + int res = pthread_key_create(&envTlsKey, detach_jni_env); + HANDLE_RESULT(res, "Failed to create pthread key") +} + +void cleanup_tls() { + if (envTlsKey != 0) { + int res = pthread_key_delete(envTlsKey); + HANDLE_RESULT(res, "Failed to delete pthread key") + envTlsKey = 0; + } +} + +void set_tls(ThreadData* t_data) { + if (envTlsKey == 0) + init_tls(); + int p_res = pthread_setspecific(envTlsKey, t_data); + HANDLE_RESULT(p_res, "Failed to set thread data") +} +#endif + + +void detach_jni_env(void* data) { + ThreadData* t_data = data; + JavaVM* vm = t_data->vm; + + jint res = (*vm)->DetachCurrentThread(vm); + HANDLE_RESULT(res, "Failed to detach thread") + + free(t_data); +} + +void tls_attach_jni_env(JavaVM* vm, JNIEnv** env) { + jint attach_res = (*vm)->GetEnv(vm, (void**)env, JNI_VERSION_1_6); + if (attach_res == JNI_EDETACHED) { + jint res = THREAD_ATTACH_MACRO + HANDLE_RESULT(res, "Failed to attach thread") + ThreadData* t_data = malloc(sizeof(ThreadData)); + t_data->env = *env; + t_data->vm = vm; + + set_tls(t_data); + } else { + HANDLE_RESULT(attach_res, "Failed to get thread") + } +} \ No newline at end of file diff --git a/gdx-jnigen-runtime/src/main/native/jni_env_tls.h b/gdx-jnigen-runtime/src/main/native/jni_env_tls.h new file mode 100644 index 00000000..5bbca682 --- /dev/null +++ b/gdx-jnigen-runtime/src/main/native/jni_env_tls.h @@ -0,0 +1,23 @@ +#ifndef JNI_ENV_TLS_H_ +#define JNI_ENV_TLS_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ThreadData { + JavaVM* vm; + JNIEnv* env; +} ThreadData; + +void init_tls(); +void cleanup_tls(); +void tls_attach_jni_env(JavaVM* vm, JNIEnv** env); +void detach_jni_env(void* data); + +#ifdef __cplusplus +} +#endif +#endif // JNI_ENV_TLS_H_