diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d8f9516 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*.swift] +indent_style = space +indent_size = 2 +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01dc9f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +.swift-version +.sdkmanrc + +.DS_Store +.metals +.build +.idea +.vscode +Packages +xcuserdata/ +DerivedData/ +.swiftpm +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +*.class +bin/ +BuildLogic/out/ +.index-build +.build-vscode +**/.vscode + +# Ignore gradle build artifacts +.gradle +**/build/ +lib/ + +# Ignore JVM crash logs +**/*.log + +# Ignore package resolved +Package.resolved + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Ignore files generated by jextract, we always can re-generate them +*/**/src/generated/java/**/* + +*/**/*.d +*/**/*.o +*/**/*.swiftdeps +*/**/*.swiftdeps~ +*/**/.docc-build/ + + +BuildLogic/.kotlin/ diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 0000000..a49ab62 --- /dev/null +++ b/.licenseignore @@ -0,0 +1,54 @@ +.editorconfig +.gitignore +.licenseignore +.swiftformatignore +.spi.yml +.swift-format +.github/* +*.md +**/*.config +CONTRIBUTORS.txt +LICENSE.txt +NOTICE.txt +Package.swift +Package.resolved +README.md +SECURITY.md +scripts/unacceptable-language.txt +.unacceptablelanguageignore +docker/* +**/*.docc/* +**/.gitignore +**/Package.swift +**/Package.resolved +**/*.md +**/openapi.yml +**/petstore.yaml +**/openapi-generator-config.yaml +**/openapi-generator-config.yml +**/docker-compose.yaml +**/docker/* +**/.dockerignore +Makefile +**/Makefile +**/*.html +**/CMakeLists.txt +**/*.jar +**/generated/*.java +gradle.properties +**/gradle.properties +**/generated/*.swift +gradle/wrapper/gradle-wrapper.properties +gradlew +gradlew.bat +**/gradlew +**/gradlew.bat +**/ci-validate.sh +**/DO_NOT_EDIT.txt +Plugins/**/_PluginsShared +Plugins/**/0_PLEASE_SYMLINK* +Plugins/PluginsShared/JavaKitConfigurationShared +Samples/JavaDependencySampleApp/gradle +Sources/_Subprocess/** +Sources/_SubprocessCShims/** +Samples/gradle diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..18811c1 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,6 @@ +version: 1 +builder: + configs: + - documentation_targets: [ + SwiftJavaDocumentation + ] diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..c2a8a31 --- /dev/null +++ b/.swift-format @@ -0,0 +1,63 @@ +{ + "version" : 1, + "indentation" : { + "spaces" : 2 + }, + "tabWidth" : 2, + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "spacesAroundRangeFormationOperators" : false, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : true, + "lineLength" : 180, + "maximumBlankLines" : 2, + "spacesBeforeEndOfLineComments" : 1, + "respectsExistingLineBreaks" : true, + "prioritizeKeepingFunctionOutputTogether" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : false, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : false, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + } +} diff --git a/.unacceptablelanguageignore b/.unacceptablelanguageignore new file mode 100644 index 0000000..c3f97d3 --- /dev/null +++ b/.unacceptablelanguageignore @@ -0,0 +1,7 @@ +Sources/SwiftJavaBootstrapJavaTool/SwiftJavaBootstrapJavaTool.swift +Sources/_Subprocess/Platforms/Subprocess+Darwin.swift +Sources/_Subprocess/Platforms/Subprocess+Linux.swift +Sources/_Subprocess/Platforms/Subprocess+Unix.swift +Sources/_Subprocess/Teardown.swift +Sources/_Subprocess/Subprocess.swift +NOTICE.txt \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..05b02de --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,109 @@ +## Legal + +By submitting a pull request, you represent that you have the right to license +your contribution to Apple and the community, and agree by submitting the patch +that your contributions are licensed under the Apache 2.0 license (see +`LICENSE.txt`). + +## How to submit a bug report + +Please ensure to specify the following: + +* Commit hash +* Contextual information (e.g. what you were trying to achieve with swift-java) +* Simplest possible steps to reproduce + * More complex the steps are, lower the priority will be. + * A pull request with failing test case is preferred, but it's just fine to paste the test case into the issue description. +* Anything that might be relevant in your opinion, such as: + * Swift version or the output of `swift --version` + * OS version and the output of `uname -a` + +### Example + +``` +Commit hash: b17a8a9f0f814c01a56977680cb68d8a779c951f + +Context: +While testing my application that uses with swift-openapi-generator, I noticed that ... + +Steps to reproduce: +1. ... +2. ... +3. ... +4. ... + +$ swift --version +Swift version 4.0.2 (swift-4.0.2-RELEASE) +Target: x86_64-unknown-linux-gnu + +Operating system: Ubuntu Linux 16.04 64-bit + +$ uname -a +Linux beefy.machine 4.4.0-101-generic #124-Ubuntu SMP Fri Nov 10 18:29:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux + +My system has IPv6 disabled. +``` + +## Writing a Patch + +A good patch is: + +1. Concise, and contains as few changes as needed to achieve the end result. +2. Tested, ensuring that any tests provided failed before the patch and pass after it. +3. Documented, adding API documentation as needed to cover new functions and properties. +4. Accompanied by a great commit message, using our commit message template. + +### Run CI checks locally + +You can run the Github Actions workflows locally using +[act](https://github.com/nektos/act). To run all the jobs that run on a pull +request, use the following command: + +``` +% act pull_request +``` + +To run just a single job, use `workflow_call -j `, and specify the inputs +the job expects. For example, to run just shellcheck: + +``` +% act workflow_call -j soundness --input shell_check_enabled=true +``` + +To bind-mount the working directory to the container, rather than a copy, use +`--bind`. For example, to run just the formatting, and have the results +reflected in your working directory: + +``` +% act --bind workflow_call -j soundness --input format_check_enabled=true +``` + +If you'd like `act` to always run with certain flags, these can be be placed in +an `.actrc` file either in the current working directory or your home +directory, for example: + +``` +--container-architecture=linux/amd64 +--remote-name upstream +--action-offline-mode +``` + +For frequent contributors, we recommend adding the script as a [git pre-push hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks), which you can do via executing the following command in the project root directory: + +```bash +cat << EOF > .git/hooks/pre-push + +if [[ -f "scripts/soundness.sh" ]]; then + scripts/soundness.sh +fi +EOF +``` + +Which makes the script execute, and only allow the `git push` to complete if the check has passed. + +In the case of formatting issues, you can then `git add` the formatting changes, and attempt the push again. + +## How to contribute your work + +Please open a pull request at https://github.com/swiftlang/swift-java. Make sure the CI passes, and then wait for code review. + diff --git a/Package.swift b/Package.swift index 62b9d36..c57c7cc 100644 --- a/Package.swift +++ b/Package.swift @@ -119,17 +119,32 @@ func getJavaHomeFromPath() -> String? { let javaHome = findJavaHome() +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) +let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) +let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#elseif os(Windows) +let javaPlatformIncludePath = "\(javaIncludePath)/win32" +#endif + let package = Package( name: "swift-java-jni-core", products: [ .library( name: "SwiftJavaJNICore", targets: ["SwiftJavaJNICore"] - ), + ) ], targets: [ .target( name: "CSwiftJavaJNI", + cSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])) + ], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])) + ], linkerSettings: [ .linkedLibrary("log", .when(platforms: [.android])) ] @@ -138,10 +153,11 @@ let package = Package( .target( name: "SwiftJavaJNICore", dependencies: [ - "CSwiftJavaJNI", + "CSwiftJavaJNI" ], swiftSettings: [ .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])), ], linkerSettings: [ .unsafeFlags( @@ -169,6 +185,10 @@ let package = Package( name: "SwiftJavaJNICoreTests", dependencies: [ "SwiftJavaJNICore" + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])), ] ), ] diff --git a/README.md b/README.md index 9244727..de039e8 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ -# swift-java-jni-core \ No newline at end of file +# Swift Java JNI Core + +## Swift Java Interoperability + +Swift offers Java interoperability using the [SwiftJava](https://github.com/swiftlang/swift-java) package, package plugins, and also the `swift-java` command line tool for source generation (similar to OpenJDK jextract, if you are familiar with that concept). + +Most developers should prever the use of that package, as it features bullet proof ways to make your inter-op code safe and performant, by using either source generation (`swift-java`) when calling Swift from Java, or Swift macros (`@JavaMethod`, ...) when calling Java from Swift. Multiple safeguards and optimizations are built into the SwiftJava library and we highly recommend using it instead of this low level "raw JNI" package. + +## Swift Java JNI Core + +The swift-java-jni-core package presents a *low-level* Swift-friendly interface to the Java Native Interface (JNI) specification, which is the universal set of data types and functions for interacting with a Java Virtual Machine and compatible derivatives, such as the Android Runtime (ART). You can view it as a thin layer on top of the `jni.h` along with pre-packaged type conversions and ways to interact with the JVM from Swift. + +This package is designed to offer low-level zero-dependency support for higher-level modules, such as [SwiftJava](https://github.com/swiftlang/swift-java) and other projects. + +## Features + +### JavaValue + +A `JavaValue` describes a type that can be bridged with Java. `JavaValue` is the base protocol for bridging between Swift types and their Java counterparts via the Java Native Interface (JNI). It is suitable for describing both value types (such as `Int32` or `Bool`) and object types. + +### JavaVirtualMachine + +The `JavaVirtualMachine` provides access to a Java Virtual Machine (JVM), which can either be loaded from within a Swift process (via `JNI_CreateJavaVM`), or accessed from a pre-existing in-process handle (`JNI_GetCreatedJavaVMs`). The JavaVirtualMachine is the entry point to interfacing with the JVM, and handles finding and loading classes, looking up and invoking methods, and handling details like locking, threads, and references. + +### CSwiftJavaJNI + +This C module provides the standardized and implementation-agnostic headers for the Java Native Interface [specification](http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html). The shape of these structures and symbols are guaranteed to be ABI stable between any compatible Java implementation. + +## Contributing + +Contributions are more than welcome, and you can read more about the process in [CONTRIBUTING.md](CONTRIBUTING.md). + diff --git a/Sources/CSwiftJavaJNI/AndroidSupport.cpp b/Sources/CSwiftJavaJNI/AndroidSupport.cpp new file mode 100644 index 0000000..a223530 --- /dev/null +++ b/Sources/CSwiftJavaJNI/AndroidSupport.cpp @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#ifdef __ANDROID__ + +#include +#include +#include + +// these are not exported by the Android SDK + +extern "C" { + using JavaRuntime_GetDefaultJavaVMInitArgs_fn = jint (*)(void *vm_args); + using JavaRuntime_CreateJavaVM_fn = jint (*)(JavaVM **, JNIEnv **, void *); + using JavaRuntime_GetCreatedJavaVMs_fn = jint (*)(JavaVM **, jsize, jsize *); +} + +static JavaRuntime_GetDefaultJavaVMInitArgs_fn + JavaRuntime_GetDefaultJavaVMInitArgs; +static JavaRuntime_CreateJavaVM_fn JavaRuntime_CreateJavaVM; +static JavaRuntime_GetCreatedJavaVMs_fn JavaRuntime_GetCreatedJavaVMs; + +static void *JavaRuntime_dlhandle; + +__attribute__((constructor)) static void JavaRuntime_init(void) { + JavaRuntime_dlhandle = dlopen("libnativehelper.so", RTLD_NOW | RTLD_LOCAL); + if (JavaRuntime_dlhandle == nullptr) { + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "failed to open libnativehelper.so: %s", dlerror()); + return; + } + + JavaRuntime_GetDefaultJavaVMInitArgs = + reinterpret_cast( + dlsym(JavaRuntime_dlhandle, "JNI_GetDefaultJavaVMInitArgs")); + if (JavaRuntime_GetDefaultJavaVMInitArgs == nullptr) + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "JNI_GetDefaultJavaVMInitArgs not found: %s", + dlerror()); + + JavaRuntime_CreateJavaVM = reinterpret_cast( + dlsym(JavaRuntime_dlhandle, "JNI_CreateJavaVM")); + if (JavaRuntime_CreateJavaVM == nullptr) + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "JNI_CreateJavaVM not found: %s", dlerror()); + + JavaRuntime_GetCreatedJavaVMs = + reinterpret_cast( + dlsym(JavaRuntime_dlhandle, "JNI_GetCreatedJavaVMs")); + if (JavaRuntime_GetCreatedJavaVMs == nullptr) + __android_log_print(ANDROID_LOG_FATAL, "JavaRuntime", + "JNI_GetCreatedJavaVMs not found: %s", dlerror()); +} + +__attribute__((destructor)) static void JavaRuntime_deinit(void) { + if (JavaRuntime_dlhandle) { + dlclose(JavaRuntime_dlhandle); + JavaRuntime_dlhandle = nullptr; + } + + JavaRuntime_GetDefaultJavaVMInitArgs = nullptr; + JavaRuntime_CreateJavaVM = nullptr; + JavaRuntime_GetCreatedJavaVMs = nullptr; +} + +jint JNI_GetDefaultJavaVMInitArgs(void *vm_args) { + if (JavaRuntime_GetDefaultJavaVMInitArgs == nullptr) + return JNI_ERR; + + return (*JavaRuntime_GetDefaultJavaVMInitArgs)(vm_args); +} + +jint JNI_CreateJavaVM(JavaVM **vm, JNIEnv **env, void *vm_args) { + if (JavaRuntime_CreateJavaVM == nullptr) + return JNI_ERR; + + return (*JavaRuntime_CreateJavaVM)(vm, env, vm_args); +} + +jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) { + if (JavaRuntime_GetCreatedJavaVMs == nullptr) + return JNI_ERR; + + return (*JavaRuntime_GetCreatedJavaVMs)(vmBuf, bufLen, nVMs); +} + +#endif diff --git a/Sources/CSwiftJavaJNI/dummy.c b/Sources/CSwiftJavaJNI/dummy.c index 36db493..3360088 100644 --- a/Sources/CSwiftJavaJNI/dummy.c +++ b/Sources/CSwiftJavaJNI/dummy.c @@ -1 +1,15 @@ -// intentionally left empty +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Dummy file so that SwiftPM produces an object file diff --git a/Sources/CSwiftJavaJNI/include/CSwiftJavaJNI.h b/Sources/CSwiftJavaJNI/include/CSwiftJavaJNI.h new file mode 100644 index 0000000..fb5f71a --- /dev/null +++ b/Sources/CSwiftJavaJNI/include/CSwiftJavaJNI.h @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#ifndef CSwiftJavaJNI_h +#define CSwiftJavaJNI_h + +#include + +#endif /* CSwiftJavaJNI_h */ diff --git a/Sources/CSwiftJavaJNI/include/module.modulemap b/Sources/CSwiftJavaJNI/include/module.modulemap index a1acee9..2be3eda 100644 --- a/Sources/CSwiftJavaJNI/include/module.modulemap +++ b/Sources/CSwiftJavaJNI/include/module.modulemap @@ -1,2 +1,4 @@ module CSwiftJavaJNI { + umbrella header "CSwiftJavaJNI.h" + export * } diff --git a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift new file mode 100644 index 0000000..8a0cb31 --- /dev/null +++ b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift @@ -0,0 +1,161 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +extension Array: JavaValue where Element: JavaValue { + public typealias JNIType = jobject? + + public static var jvalueKeyPath: WritableKeyPath { \.l } + + public static var javaType: JavaType { .array(Element.javaType) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + let jniCount = environment.interface.GetArrayLength(environment, value) + let count = Int(jniCount) + + guard let value else { + self = [] + return + } + + // Fast path for byte types: Since the memory layout of `jbyte` (Int8) and UInt8/Int8 is identical, + // we can rebind the memory and fill it directly without creating an intermediate array. + // This mirrors the optimization in `getJNIValue` in the reverse direction. + if Element.self == UInt8.self { + let result = [UInt8](unsafeUninitializedCapacity: count) { buffer, initializedCount in + buffer.withMemoryRebound(to: jbyte.self) { jbyteBuffer in + UInt8.jniGetArrayRegion(in: environment)( + environment, + value, + 0, + jniCount, + jbyteBuffer.baseAddress + ) + } + initializedCount = count + } + self = result as! Self + } else if Element.self == Int8.self { + let result = [Int8](unsafeUninitializedCapacity: Int(jniCount)) { buffer, initializedCount in + Int8.jniGetArrayRegion(in: environment)( + environment, + value, + 0, + jniCount, + buffer.baseAddress + ) + initializedCount = count + } + self = result as! Self + } else { + // Slow path for other types: create intermediate array and map + let jniArray = [Element.JNIType](unsafeUninitializedCapacity: count) { buffer, initializedCount in + Element.jniGetArrayRegion(in: environment)( + environment, + value, + 0, + jniCount, + buffer.baseAddress + ) + initializedCount = Int(jniCount) + } + self = jniArray.map { Element(fromJNI: $0, in: environment) } + } + } + + @inlinable + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + let count = self.count + var jniArray = Element.jniNewArray(in: environment)(environment, Int32(count))! + + if Element.self == UInt8.self || Element.self == Int8.self { + // Fast path, Since the memory layout of `jbyte`` and those is the same, we rebind the memory + // rather than convert every element independently. This allows us to avoid another Swift array creation. + self.withUnsafeBytes { buffer in + buffer.getJNIValue(into: &jniArray, in: environment) + } + } else { + // Slow path, convert every element to the apropriate JNIType: + let jniElementBuffer: [Element.JNIType] = self.map { // meh, temporary array + $0.getJNIValue(in: environment) + } + Element.jniSetArrayRegion(in: environment)( + environment, + jniArray, + 0, + jsize(self.count), + jniElementBuffer + ) + } + + return jniArray + } + + public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { + environment.interface.CallObjectMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetObjectField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetObjectField + } + + public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall { + environment.interface.CallStaticObjectMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticObjectField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticObjectField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + { environment, size in + // FIXME: We should have a bridged JavaArray that we can use here. + let arrayClass = environment.interface.FindClass(environment, "java/lang/Array") + return environment.interface.NewObjectArray(environment, size, arrayClass, nil) + } + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in 0.. JNISetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start.. { \.z } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { self ? 1 : 0 } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = value != 0 + } + + public static var javaType: JavaType { .boolean } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallBooleanMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetBooleanField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetBooleanField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticBooleanMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticBooleanField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticBooleanField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewBooleanArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetBooleanArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetBooleanArrayRegion + } + + public static var jniPlaceholderValue: jboolean { + 0 + } +} diff --git a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+FloatingPoint.swift b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+FloatingPoint.swift new file mode 100644 index 0000000..4f7a584 --- /dev/null +++ b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+FloatingPoint.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +extension Float: JavaValue { + public typealias JNIType = jfloat + + public static var jvalueKeyPath: WritableKeyPath { \.f } + + public static var javaType: JavaType { .float } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallFloatMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetFloatField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetFloatField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticFloatMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticFloatField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticFloatField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewFloatArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetFloatArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetFloatArrayRegion + } + + public static var jniPlaceholderValue: jfloat { + 0 + } +} + +extension Double: JavaValue { + public typealias JNIType = jdouble + + public static var jvalueKeyPath: WritableKeyPath { \.d } + + public static var javaType: JavaType { .double } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallDoubleMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetDoubleField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetDoubleField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticDoubleMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticDoubleField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticDoubleField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewDoubleArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetDoubleArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetDoubleArrayRegion + } + + public static var jniPlaceholderValue: jdouble { + 0 + } +} diff --git a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Integers.swift b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Integers.swift new file mode 100644 index 0000000..c5b72e0 --- /dev/null +++ b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Integers.swift @@ -0,0 +1,724 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +extension UInt8: JavaValue { + public typealias JNIType = jbyte + + public static var jvalueKeyPath: WritableKeyPath { \.b } + + public static var javaType: JavaType { .byte } + + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(bitPattern: self) } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self(bitPattern: value) + } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallByteMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetByteField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetByteField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticByteMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticByteField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticByteField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewByteArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetByteArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetByteArrayRegion + } + + public static var jniPlaceholderValue: jbyte { + 0 + } +} + +extension Int8: JavaValue { + public typealias JNIType = jbyte + + public static var jvalueKeyPath: WritableKeyPath { \.b } + + public static var javaType: JavaType { .byte } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallByteMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetByteField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetByteField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticByteMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticByteField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticByteField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewByteArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetByteArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetByteArrayRegion + } + + public static var jniPlaceholderValue: jbyte { + 0 + } +} + +extension UInt16: JavaValue { + public typealias JNIType = jchar + + public static var jvalueKeyPath: WritableKeyPath { \.c } + + public static var javaType: JavaType { .char } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallCharMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetCharField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetCharField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticCharMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticCharField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticCharField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewCharArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetCharArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetCharArrayRegion + } + + public static var jniPlaceholderValue: jchar { + 0 + } +} + +extension Int16: JavaValue { + public typealias JNIType = jshort + + public static var jvalueKeyPath: WritableKeyPath { \.s } + + public static var javaType: JavaType { .short } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallShortMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetShortField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetShortField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticShortMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticShortField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticShortField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewShortArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetShortArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetShortArrayRegion + } + + public static var jniPlaceholderValue: jshort { + 0 + } +} + +extension UInt32: JavaValue { + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public static var javaType: JavaType { .int } + + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(bitPattern: self) } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self(bitPattern: value) + } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} + +extension Int32: JavaValue { + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int32(value) + } + + public static var javaType: JavaType { .int } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} + +extension UInt64: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + // `jlong` is always 64-bit, no matter the system pointer size. + // Due to differences in JNI headers between Android, JDK and how Swift sees these type imports + // we have to handle this a bit differently. + // + // On 32-bit, the standard JDK jlong is defined in C as `long long`, which yields Swift `Int64` + // On 64-bit, the standard JDK jlong is defined in C as `long`, which yields Swift `Int` + // On Android it's correctly marked as int64_t, always yielding `Int64` in Swift. + #if os(Android) || _pointerBitWidth(_32) + return Int64(bitPattern: self) + #else + return Int(bitPattern: UInt(self)) + #endif + } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + // `jlong` is always 64-bit, no matter the system pointer size. + // Due to differences in JNI headers between Android, JDK and how Swift sees these type imports + // we have to handle this a bit differently. + // + // On 32-bit, the standard JDK jlong is defined in C as `long long`, which yields Swift `Int64` + // On 64-bit, the standard JDK jlong is defined in C as `long`, which yields Swift `Int` + // On Android it's correctly marked as int64_t, always yielding `Int64` in Swift. + #if os(Android) || _pointerBitWidth(_32) + // In this case `jlong` is seen in Swift as `Int64`. + self = UInt64(bitPattern: value) + #else + // In this case `jlong` is since as just `Int`, which is always Int64 + self = UInt64(bitPattern: Int64(value)) + #endif + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} + +extension Int64: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int64(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} + +#if _pointerBitWidth(_32) +extension Int: JavaValue { + + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int(value) + } + + public static var javaType: JavaType { .int } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} +extension UInt: JavaValue { + + public typealias JNIType = jint + + public static var jvalueKeyPath: WritableKeyPath { \.i } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = UInt(value) + } + + public static var javaType: JavaType { .int } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallIntMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetIntField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetIntField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticIntMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticIntField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticIntField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewIntArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetIntArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetIntArrayRegion + } + + public static var jniPlaceholderValue: jint { + 0 + } +} +#elseif _pointerBitWidth(_64) +extension Int: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Int(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} +extension UInt: JavaValue { + public typealias JNIType = jlong + + public static var jvalueKeyPath: WritableKeyPath { \.j } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { JNIType(self) } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = UInt(value) + } + + public static var javaType: JavaType { .long } + + public static func jniMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallLongMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetLongField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetLongField + } + + public static func jniStaticMethodCall( + in environment: JNIEnvironment + ) -> ((JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> JNIType) { + environment.interface.CallStaticLongMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticLongField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticLongField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + environment.interface.NewLongArray + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + environment.interface.GetLongArrayRegion + } + + public static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion { + environment.interface.SetLongArrayRegion + } + + public static var jniPlaceholderValue: jlong { + 0 + } +} +#endif diff --git a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+String.swift b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+String.swift new file mode 100644 index 0000000..6ac766c --- /dev/null +++ b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+String.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +extension String: JavaValue { + public typealias JNIType = jstring? + + public static var jvalueKeyPath: WritableKeyPath { \.l } + + public static var javaType: JavaType { + .class(package: "java.lang", name: "String") + } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + guard let value else { + self.init() + return + } + let cString = environment.interface.GetStringUTFChars(environment, value, nil)! + defer { environment.interface.ReleaseStringUTFChars(environment, value, cString) } + self = String(cString: cString) // copy + } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + // FIXME: this works, but isn't great. Swift uses UTF8 and Java uses UTF8 with + // some encoding quirks for non-ascii. So round-tripping via UTF16 is unfortunate, + // but correct, so good enough for now. + var utfBuffer = Array(utf16) + return environment.interface.NewString(environment, &utfBuffer, Int32(utfBuffer.count)) + } + + public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { + environment.interface.CallObjectMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetObjectField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetObjectField + } + + public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall { + environment.interface.CallStaticObjectMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticObjectField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticObjectField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + { environment, size in + // FIXME: Introduce a JavaString class that we can use for this. + let stringClass = environment.interface.FindClass(environment, "java/lang/String") + return environment.interface.NewObjectArray(environment, size, stringClass, nil) + } + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in 0.. JNISetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start.. jbyteArray { + let count = self.count + var jniArray: jbyteArray = UInt8.jniNewArray(in: environment)(environment, Int32(count))! + getJNIValue(into: &jniArray, in: environment) + return jniArray + } + + public func getJNIValue(into jniArray: inout jbyteArray, in environment: JNIEnvironment) { + assert(Element.self == UInt8.self, "We're going to rebind memory with the assumption storage are bytes") + + // Fast path, Since the memory layout of `jbyte`` and those is the same, we rebind the memory + // rather than convert every element independently. This allows us to avoid another Swift array creation. + self.withUnsafeBytes { buffer in + guard let baseAddress = buffer.baseAddress else { + fatalError("Buffer had no base address?! \(self)") + } + + baseAddress.withMemoryRebound(to: jbyte.self, capacity: count) { ptr in + UInt8.jniSetArrayRegion(in: environment)( + environment, + jniArray, + 0, + jsize(count), + ptr + ) + } + } + } +} diff --git a/Sources/SwiftJavaJNICore/JavaAnnotation.swift b/Sources/SwiftJavaJNICore/JavaAnnotation.swift new file mode 100644 index 0000000..233af73 --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaAnnotation.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a Java annotation (e.g. `@Deprecated` or `@Unsigned`) +public struct JavaAnnotation: Equatable, Hashable { + public let type: JavaType + public let arguments: [String] + + public init(className name: some StringProtocol, arguments: [String] = []) { + type = JavaType(className: name) + self.arguments = arguments + } + + public func render() -> String { + guard let className = type.className else { + fatalError("Java annotation must have a className") + } + + var res = "@\(className)" + guard !arguments.isEmpty else { + return res + } + + res += "(" + res += arguments.joined(separator: ",") + res += ")" + return res + } + +} + +extension JavaAnnotation { + public static var unsigned: JavaAnnotation { + JavaAnnotation(className: "Unsigned") + } +} diff --git a/Sources/SwiftJavaJNICore/JavaDemanglingError.swift b/Sources/SwiftJavaJNICore/JavaDemanglingError.swift new file mode 100644 index 0000000..445957d --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaDemanglingError.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes an error that can occur when demangling a Java name. +enum JavaDemanglingError: Error { + /// This does not match the form of a Java mangled type name. + case invalidMangledName(String) + + /// Extra text after the mangled name. + case extraText(String) +} diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment.swift b/Sources/SwiftJavaJNICore/JavaEnvironment.swift new file mode 100644 index 0000000..1612799 --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaEnvironment.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + + +#if canImport(Android) +public typealias JNINativeInterface_ = JNINativeInterface +#endif + +extension UnsafeMutablePointer { + public var interface: JNINativeInterface_ { self.pointee!.pointee } +} diff --git a/Sources/SwiftJavaJNICore/JavaType+JNI.swift b/Sources/SwiftJavaJNICore/JavaType+JNI.swift new file mode 100644 index 0000000..d75245e --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaType+JNI.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension JavaType { + /// Map this Java type to the appropriate JNI type name. + public var jniTypeName: String { + switch self { + case .boolean: "jboolean" + case .float: "jfloat" + case .double: "jdouble" + case .byte: "jbyte" + case .char: "jchar" + case .short: "jshort" + case .int: "jint" + case .long: "jlong" + case .void: "void" + case .array(.boolean): "jbooleanArray?" + case .array(.byte): "jbyteArray?" + case .array(.char): "jcharArray?" + case .array(.short): "jshortArray?" + case .array(.int): "jintArray?" + case .array(.long): "jlongArray?" + case .array(.float): "jfloatArray?" + case .array(.double): "jdoubleArray?" + case .array: "jobjectArray?" + case .class(package: "java.lang", name: "String", _): "jstring?" + case .class(package: "java.lang", name: "Class", _): "jclass?" + case .class(package: "java.lang", name: "Throwable", _): "jthrowable?" + case .class: "jobject?" + } + } + + /// Map this Java type to the appropriate JNI field name within the 'jvalue' + /// union. + public var jniFieldName: String { + switch self { + case .boolean: "z" + case .byte: "b" + case .char: "c" + case .short: "s" + case .int: "i" + case .long: "j" + case .float: "f" + case .double: "d" + case .class, .array: "l" + case .void: fatalError("There is no field name for 'void'") + } + } +} diff --git a/Sources/SwiftJavaJNICore/JavaType+JavaSource.swift b/Sources/SwiftJavaJNICore/JavaType+JavaSource.swift new file mode 100644 index 0000000..96f72be --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaType+JavaSource.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension JavaType { + /// Form a Java type based on the name that is produced by + /// java.lang.Class.getName(). This can be primitive types like "int", + /// class types like "java.lang.String", or arrays thereof. + public init(javaTypeName: String) throws { + switch javaTypeName { + case "boolean": self = .boolean + case "byte": self = .byte + case "char": self = .char + case "short": self = .short + case "int": self = .int + case "long": self = .long + case "float": self = .float + case "double": self = .double + case "void": self = .void + + case let name where name.starts(with: "["): + self = try JavaType(mangledName: name) + + case let className: + self = JavaType(className: className) + } + } +} + +extension JavaType: CustomStringConvertible { + /// Description of the Java type as it would appear in Java source. + public var description: String { + switch self { + case .boolean: "boolean" + case .byte: "byte" + case .char: "char" + case .short: "short" + case .int: "int" + case .long: "long" + case .float: "float" + case .double: "double" + case .void: "void" + case .array(let elementType): "\(elementType.description)[]" + case .class(let package, let name, let typeParameters): + if let package { + if !typeParameters.isEmpty { + "\(package).\(name)<\(typeParameters.map(\.description).joined(separator: ", "))>" + } else { + "\(package).\(name)" + } + } else { + name + } + } + } + + /// Returns the class name if this java type was a class, + /// and nil otherwise. + public var className: String? { + switch self { + case .class(_, let name, _): + return name + default: + return nil + } + } + + /// Returns the fully qualified class name if this java type was a class, + /// and nil otherwise. + public var fullyQualifiedClassName: String? { + switch self { + case .class(.some(let package), let name, _): + return "\(package).\(name)" + case .class(nil, let name, _): + return name + default: + return nil + } + } +} diff --git a/Sources/SwiftJavaJNICore/JavaType+SwiftNames.swift b/Sources/SwiftJavaJNICore/JavaType+SwiftNames.swift new file mode 100644 index 0000000..04159bd --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaType+SwiftNames.swift @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// The type of a resolver function that turns a canonical Java class name into +/// the corresponding Swift type name. If there is no such Swift type, the +/// resolver can throw an error to indicate the problem. +public typealias JavaToSwiftClassNameResolver = (String) throws -> String + +extension JavaType { + /// Whether this Java type needs to be represented by a Swift optional. + public func isSwiftOptional(stringIsValueType: Bool) -> Bool { + switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, + .array: + return false + + case .class(package: "java.lang", name: "String", _): + return !stringIsValueType + + case .class: + return true + } + } + + public var isSwiftClosure: Bool { + switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, + .array: + return false + case .class(package: "java.lang", name: "Runnable", _): + return true + case .class: + return false + } + } + + public var isVoid: Bool { + if case .void = self { + return true + } + return false + } + + public var isString: Bool { + switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, + .array: + return false + case .class(package: "java.lang", name: "String", _): + return true + case .class: + return false + } + } + + /// Produce the Swift type name for this Java type. + public func swiftTypeName(resolver: JavaToSwiftClassNameResolver) rethrows -> String { + switch self { + case .boolean: return "Bool" + case .byte: return "Int8" + case .char: return "UInt16" + case .short: return "Int16" + case .int: return "Int32" + case .long: return "Int64" + case .float: return "Float" + case .double: return "Double" + case .void: return "Void" + case .array(let elementType): + let elementTypeName = try elementType.swiftTypeName(resolver: resolver) + let elementIsOptional = elementType.isSwiftOptional(stringIsValueType: true) + return "[\(elementTypeName)\(elementIsOptional ? "?" : "")]" + + case .class: return try resolver(description) + } + } + +} + +/// Determines how type conversion should deal with Swift's unsigned numeric types. +/// +/// When `ignoreSign` is used, unsigned Swift types are imported directly as their corresponding bit-width types, +/// which may yield surprising values when an unsigned Swift value is interpreted as a signed Java type: +/// - `UInt8` is imported as `byte` +/// - `UInt16` is imported as `char` (this is always correct, since `char` is unsigned in Java) +/// - `UInt32` is imported as `int` +/// - `UInt64` is imported as `long` +/// +/// When `wrapUnsignedGuava` is used, unsigned Swift types are imported as safe "wrapper" types from the popular Guava +/// library on the Java side. SwiftJava does not include these types, so you would have to make sure your project depends +/// on Guava for such generated code to be able to compile. +/// +/// These make the Unsigned nature of the types explicit in Java, however they come at a cost of allocating the wrapper +/// object, and indirection when accessing the underlying numeric value. These are often useful as a signal to watch out +/// when dealing with a specific API, however in high performance use-cases, one may want to choose using the primitive +/// values directly, and interact with them using {@code UnsignedIntegers} SwiftKit helper classes on the Java side. +/// +/// The type mappings in this mode are as follows: +/// - `UInt8` is imported as `com.google.common.primitives.UnsignedInteger` +/// - `UInt16` is imported as `char` (this is always correct, since `char` is unsigned in Java) +/// - `UInt32` is imported as `com.google.common.primitives.UnsignedInteger` +/// - `UInt64` is imported as `com.google.common.primitives.UnsignedLong` +public enum UnsignedNumericsMode { + case ignoreSign + case wrapUnsignedGuava +} diff --git a/Sources/SwiftJavaJNICore/JavaType.swift b/Sources/SwiftJavaJNICore/JavaType.swift new file mode 100644 index 0000000..0e80235 --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaType.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes the Java type system. +/// +/// Some types may need to be annotated when in parameter position, +public enum JavaType: Equatable, Hashable { + case boolean + case byte(parameterAnnotations: [JavaAnnotation]) + case char(parameterAnnotations: [JavaAnnotation]) + case short(parameterAnnotations: [JavaAnnotation]) + case int(parameterAnnotations: [JavaAnnotation]) + case long(parameterAnnotations: [JavaAnnotation]) + case float + case double + case void + + /// A Java class separated into its package (e.g., "java.lang") and class name + /// (e.g., "Object") + case `class`(package: String?, name: String, typeParameters: [JavaType] = []) + + /// A Java array. + indirect case array(JavaType) + + public static var byte: JavaType { .byte(parameterAnnotations: []) } + public static var char: JavaType { .char(parameterAnnotations: []) } + public static var short: JavaType { .short(parameterAnnotations: []) } + public static var int: JavaType { .int(parameterAnnotations: []) } + public static var long: JavaType { .long(parameterAnnotations: []) } + + /// Given a class name such as "java.lang.Object", split it into + /// its package and class name to form a class instance. + public init(className name: some StringProtocol) { + if let lastDot = name.lastIndex(of: ".") { + self = .class( + package: String(name[.. JNIType + + /// Initialize from a JNI value. + init(fromJNI value: JNIType, in environment: JNIEnvironment) + + /// The key path used to access the appropriate member of jvalue. + static var jvalueKeyPath: WritableKeyPath { get } + + /// The Java type this describes. + static var javaType: JavaType { get } + + /// The JNI function that performs a call to a method returning this type. + static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall + + /// The JNI function to get the value of a field of this type. + static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet + + /// The JNI function to set the value of a field of this type. + static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet + + /// The JNI function that performs a call to a static method returning this type. + static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall + + /// The JNI function to get the value of a static field of this type. + static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet + + /// The JNI function to set the value of a static field of this type. + static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet + + /// The JNI function to create a new array of this type. + static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray + + /// The JNI function to get a region of an array. + static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion + + /// The JNI function to set a region of an array. + static func jniSetArrayRegion(in environment: JNIEnvironment) -> JNISetArrayRegion + + /// The placeholder value used when we need a JNI value that will not + /// actually be used. For example, this is used for a return back to JNI + /// after we have thrown a Java exception. + static var jniPlaceholderValue: JNIType { get } +} + +/// The JNI environment. +public typealias JNIEnvironment = UnsafeMutablePointer + +/// Type of an operation that performs a JNI method call. +public typealias JNIMethodCall = (JNIEnvironment, jobject, jmethodID, UnsafePointer?) -> Result + +/// Type of an operation that gets a field's value via JNI. +public typealias JNIFieldGet = (JNIEnvironment, jobject, jfieldID) -> FieldType + +/// Type of an operation that sets a field's value via JNI. +public typealias JNIFieldSet = (JNIEnvironment, jobject, jfieldID, FieldType) -> Void + +/// Type of an operation that performs a JNI static method call. +public typealias JNIStaticMethodCall = (JNIEnvironment, jclass, jmethodID, UnsafePointer?) -> Result + +/// Type of an operation that gets a static field's value via JNI. +public typealias JNIStaticFieldGet = (JNIEnvironment, jclass, jfieldID) -> FieldType + +/// Type of an operation that sets a static field's value via JNI. +public typealias JNIStaticFieldSet = (JNIEnvironment, jclass, jfieldID, FieldType) -> Void + +/// The type of an operation that produces a new Java array of type ArrayType +/// via JNI. +public typealias JNINewArray = (JNIEnvironment, jsize) -> jobject? + +/// The type of an operation that fills in a buffer with elements in an +/// array via JNI. +public typealias JNIGetArrayRegion = ( + JNIEnvironment, jobject, jsize, jsize, UnsafeMutablePointer? +) + -> Void + +/// The type of an operation that fills in a Java array with elements from +/// a buffer. +/// array via JNI. +public typealias JNISetArrayRegion = (JNIEnvironment, jobject, jsize, jsize, UnsafePointer?) + -> Void + +extension JavaValue where Self: ~Copyable { + /// Helper function to let us set a JNI type via keypath subscripting. + private static func assignJNIType(_ cell: inout JNIType, to newValue: JNIType) { + cell = newValue + } + + /// Convert to a jvalue within the given JNI environment. + public consuming func getJValue(in environment: JNIEnvironment) -> jvalue { + var result = jvalue() + Self.assignJNIType(&result[keyPath: Self.jvalueKeyPath], to: getJNIValue(in: environment)) + return result + } + + /// Initialize from a jvalue + init(fromJava value: jvalue, in environment: JNIEnvironment) { + self.init(fromJNI: value[keyPath: Self.jvalueKeyPath], in: environment) + } +} + +// Default implementations when the JNI type and the Swift type line up. +extension JavaValue where Self.JNIType == Self { + /// Retrieve the JNI value. + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { self } + + /// Initialize from a JNI value. + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = value + } +} + +extension JavaValue where Self: ~Copyable { + /// The mangling string for this particular value. + public static var jniMangling: String { javaType.mangledName } +} diff --git a/Sources/SwiftJavaJNICore/Mangling.swift b/Sources/SwiftJavaJNICore/Mangling.swift new file mode 100644 index 0000000..0b92911 --- /dev/null +++ b/Sources/SwiftJavaJNICore/Mangling.swift @@ -0,0 +1,171 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension JavaType { + /// Demangle a Java type name into a representation of the type. + public init(mangledName: String) throws { + var mangledName = mangledName[...] + self = try JavaType.demangleNextType(from: &mangledName) + if !mangledName.isEmpty { + throw JavaDemanglingError.extraText(String(mangledName)) + } + } + + /// Produce a Java mangled type name for this type. + public var mangledName: String { + switch self { + case .boolean: "Z" + case .byte: "B" + case .char: "C" + case .double: "D" + case .float: "F" + case .int: "I" + case .long: "J" + case .short: "S" + case .void: "V" + case .array(let elementType): "[" + elementType.mangledName + case .class(let package, let name, _): + "L\(package!).\(name.replacingPeriodsWithDollars());".replacingPeriodsWithSlashes() + } + } +} + +extension MethodSignature { + /// Demangle the given method Java signature. + public init(mangledName: String) throws { + // Method signatures have the form "(parameter-types)result-type". + guard mangledName.starts(with: "(") else { + throw JavaDemanglingError.invalidMangledName(mangledName) + } + var remainingName = mangledName.dropFirst() + + // Demangle the parameter types. + var parameterTypes: [JavaType] = [] + while let firstChar = remainingName.first, firstChar != ")" { + let parameterType = try JavaType.demangleNextType(from: &remainingName) + parameterTypes.append(parameterType) + } + self.parameterTypes = parameterTypes + + guard remainingName.first == ")" else { + throw JavaDemanglingError.invalidMangledName(mangledName) + } + + // Demangle the result type. + remainingName = remainingName.dropFirst() + self.resultType = try JavaType(mangledName: String(remainingName)) + } + + /// Produce a mangled name for this method signature. + public var mangledName: String { + var result = "(" + for parameterType in parameterTypes { + result += parameterType.mangledName + } + result += ")" + result += resultType.mangledName + return result + } +} + +extension JavaType { + /// Demangle the next Java type from the given string, shrinking the input + /// string and producing demangled type. + static func demangleNextType(from string: inout Substring) throws -> JavaType { + guard let firstChar = string.first else { + throw JavaDemanglingError.invalidMangledName(String(string)) + } + + switch firstChar { + case "Z": + string = string.dropFirst() + return .boolean + case "B": + string = string.dropFirst() + return .byte + case "C": + string = string.dropFirst() + return .char + case "D": + string = string.dropFirst() + return .double + case "F": + string = string.dropFirst() + return .float + case "I": + string = string.dropFirst() + return .int + case "J": + string = string.dropFirst() + return .long + case "S": + string = string.dropFirst() + return .short + case "V": + string = string.dropFirst() + return .void + case "[": + // Count the brackets to determine array depth. + var arrayDepth = 1 + string = string.dropFirst() + while string.first == "[" { + arrayDepth += 1 + string = string.dropFirst() + } + + var resultType = try demangleNextType(from: &string) + while arrayDepth > 0 { + resultType = .array(resultType) + arrayDepth -= 1 + } + + return resultType + + case "L": + guard let semicolonIndex = string.firstIndex(of: ";") else { + throw JavaDemanglingError.invalidMangledName(String(string)) + } + + // Extract the canonical Java class name with the slashes in it. + let afterStart = string.index(after: string.startIndex) + let canonicalNameWithSlashes = string[afterStart.. String { + String(self.map { $0 == "." ? "/" as Character : $0 }) + } + + /// Return the string after replacing all of the forward slashes ("/") with + /// periods ("."). + fileprivate func replacingSlashesWithPeriods() -> String { + String(self.map { $0 == "/" ? "." as Character : $0 }) + } + + /// Return the string after replacing all of the periods (".") with slashes ("$"). + fileprivate func replacingPeriodsWithDollars() -> String { + String(self.map { $0 == "." ? "$" as Character : $0 }) + } +} diff --git a/Sources/SwiftJavaJNICore/MethodSignature.swift b/Sources/SwiftJavaJNICore/MethodSignature.swift new file mode 100644 index 0000000..3e2b661 --- /dev/null +++ b/Sources/SwiftJavaJNICore/MethodSignature.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a Java method signature. +public struct MethodSignature: Equatable, Hashable { + /// The result type of this method. + public let resultType: JavaType + + /// The parameter types of this method. + public let parameterTypes: [JavaType] + + public init(resultType: JavaType, parameterTypes: [JavaType]) { + self.resultType = resultType + self.parameterTypes = parameterTypes + } +} diff --git a/Sources/SwiftJavaJNICore/SwiftJavaJNICore.swift b/Sources/SwiftJavaJNICore/SwiftJavaJNICore.swift index 36db493..9b2c02a 100644 --- a/Sources/SwiftJavaJNICore/SwiftJavaJNICore.swift +++ b/Sources/SwiftJavaJNICore/SwiftJavaJNICore.swift @@ -1 +1,15 @@ -// intentionally left empty +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_exported import CSwiftJavaJNI diff --git a/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift b/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift new file mode 100644 index 0000000..96eba79 --- /dev/null +++ b/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift @@ -0,0 +1,374 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public typealias JavaVMPointer = UnsafeMutablePointer +#if canImport(Android) +typealias JNIEnvPointer = UnsafeMutablePointer +#else +typealias JNIEnvPointer = UnsafeMutableRawPointer +#endif + +extension FileManager { + #if os(Windows) + static let pathSeparator = ";" + #else + static let pathSeparator = ":" + #endif +} + +public final class JavaVirtualMachine: @unchecked Sendable { + /// The JNI version that we depend on. + static let jniVersion = JNI_VERSION_1_6 + + /// Thread-local storage to detach from thread on exit + private static let destroyTLS = ThreadLocalStorage { _ in + try? JavaVirtualMachine.shared().detachCurrentThread() + } + + /// The Java virtual machine instance. + private let jvm: JavaVMPointer + + let classpath: [String] + + /// Whether to destroy the JVM on deinit. + private let destroyOnDeinit: LockedState // FIXME: we should require macOS 15 and then use Synchronization + + /// Adopt an existing JVM pointer. + public init(adoptingJVM jvm: JavaVMPointer) { + self.jvm = jvm + self.classpath = [] // FIXME: bad... + self.destroyOnDeinit = .init(initialState: false) + } + + /// Initialize a new Java virtual machine instance. + /// + /// - Parameters: + /// - classpath: The directories, JAR files, and ZIP files in which the JVM + /// should look to find classes. This maps to the VM option + /// `-Djava.class.path=`. + /// - vmOptions: Options that should be passed along to the JVM, which will + /// be prefixed by the class-path argument described above. + /// - ignoreUnrecognized: Whether the JVM should ignore any VM options it + /// does not recognize. + private init( + classpath: [String] = [], + vmOptions: [String] = [], + ignoreUnrecognized: Bool = false + ) throws { + self.classpath = classpath + var jvm: JavaVMPointer? = nil + var environment: JNIEnvPointer? = nil + var vmArgs = JavaVMInitArgs() + vmArgs.version = JavaVirtualMachine.jniVersion + vmArgs.ignoreUnrecognized = jboolean(ignoreUnrecognized ? JNI_TRUE : JNI_FALSE) + + // Construct the complete list of VM options. + var allVMOptions: [String] = [] + if !classpath.isEmpty { + let fileManager = FileManager.default + for path in classpath { + if !fileManager.fileExists(atPath: path) { + // FIXME: this should be configurable, a classpath missing a directory isn't reason to blow up + print( + "[warning][swift-java][JavaVirtualMachine] Missing classpath element: \(URL(fileURLWithPath: path).absoluteString)" + ) // TODO: stderr + } + } + let pathSeparatedClassPath = classpath.joined(separator: FileManager.pathSeparator) + allVMOptions.append("-Djava.class.path=\(pathSeparatedClassPath)") + } + allVMOptions.append(contentsOf: vmOptions) + + // Convert the options + let optionsBuffer = UnsafeMutableBufferPointer.allocate(capacity: allVMOptions.count) + defer { + optionsBuffer.deallocate() + } + for (index, vmOption) in allVMOptions.enumerated() { + let optionString = vmOption.utf8CString.withUnsafeBufferPointer { buffer in + let cString = UnsafeMutableBufferPointer.allocate(capacity: buffer.count + 1) + _ = cString.initialize(from: buffer) + cString[buffer.count] = 0 + return cString + } + optionsBuffer[index] = JavaVMOption(optionString: optionString.baseAddress, extraInfo: nil) + } + defer { + for option in optionsBuffer { + option.optionString.deallocate() + } + } + vmArgs.options = optionsBuffer.baseAddress + vmArgs.nOptions = jint(optionsBuffer.count) + + // Create the JVM instance. + if let createError = VMError(fromJNIError: JNI_CreateJavaVM(&jvm, &environment, &vmArgs)) { + throw createError + } + + self.jvm = jvm! + self.destroyOnDeinit = .init(initialState: true) + } + + public func destroyJVM() throws { + try self.detachCurrentThread() + if let error = VMError(fromJNIError: jvm.pointee!.pointee.DestroyJavaVM(jvm)) { + throw error + } + + destroyOnDeinit.withLock { $0 = false } // we destroyed explicitly, disable destroy in deinit + } + + deinit { + if destroyOnDeinit.withLock({ $0 }) { + do { + try destroyJVM() + } catch { + fatalError("Failed to destroy the JVM: \(error)") + } + } + } +} + +extension JavaVirtualMachine: CustomStringConvertible { + public var description: String { + "\(Self.self)(\(jvm))" + } +} + +// ==== ------------------------------------------------------------------------ +// MARK: Java thread management. + +extension JavaVirtualMachine { + /// Produce the JNI environment for the active thread, attaching this + /// thread to the JVM if it isn't already. + /// + /// - Parameter + /// - asDaemon: Whether this thread should be treated as a daemon + /// thread in the Java Virtual Machine. + public func environment(asDaemon: Bool = false) throws -> JNIEnvironment { + // Check whether this thread is already attached. If so, return the + // corresponding environment. + var environment: UnsafeMutableRawPointer? = nil + let getEnvResult = jvm.pointee!.pointee.GetEnv( + jvm, + &environment, + JavaVirtualMachine.jniVersion + ) + if getEnvResult == JNI_OK, let environment { + return environment.assumingMemoryBound(to: JNIEnv?.self) + } + + #if canImport(Android) + var jniEnv = environment?.assumingMemoryBound(to: JNIEnv?.self) + #else + var jniEnv = environment + #endif + + // Attach the current thread to the JVM. + let attachResult: jint + if asDaemon { + attachResult = jvm.pointee!.pointee.AttachCurrentThreadAsDaemon(jvm, &jniEnv, nil) + } else { + attachResult = jvm.pointee!.pointee.AttachCurrentThread(jvm, &jniEnv, nil) + } + + // If we failed to attach, report that. + if let attachError = VMError(fromJNIError: attachResult) { + // throw attachError + fatalError("JVM Error: \(attachError)") + } + + JavaVirtualMachine.destroyTLS.set(jniEnv!) + + #if canImport(Android) + return jniEnv! + #else + return jniEnv!.assumingMemoryBound(to: JNIEnv?.self) + #endif + } + + /// Detach the current thread from the Java Virtual Machine. All Java + /// threads waiting for this thread to die are notified. + func detachCurrentThread() throws { + if let resultError = VMError(fromJNIError: jvm.pointee!.pointee.DetachCurrentThread(jvm)) { + throw resultError + } + } +} +// ==== ------------------------------------------------------------------------ +// MARK: Shared Java Virtual Machine management. + +extension JavaVirtualMachine { + /// The globally shared JavaVirtualMachine instance, behind a lock. + /// + /// TODO: If the use of the lock itself ends up being slow, we could + /// use an atomic here instead because our access pattern is fairly + /// simple. + static let sharedJVM: LockedState = .init(initialState: nil) + + /// Set the shared Java Virtual Machine instance. + /// + /// This is used by higher-level modules (e.g. SwiftJava) to register + /// a JVM that was created or adopted externally. + public static func setSharedJVM(_ jvm: JavaVirtualMachine) { + sharedJVM.withLock { $0 = jvm } + } + + /// Access the shared Java Virtual Machine instance. + /// + /// If there is no shared Java Virtual Machine, create one with the given + /// arguments. Note that this function makes no attempt to try to augment + /// an existing virtual machine instance with the options given, so it is + /// up to clients to ensure that consistent arguments are provided to all + /// calls. + /// + /// - Parameters: + /// - classpath: The directories, JAR files, and ZIP files in which the JVM + /// should look to find classes. This maps to the VM option + /// `-Djava.class.path=`. + /// - vmOptions: Options that should be passed along to the JVM, which will + /// be prefixed by the class-path argument described above. + /// - ignoreUnrecognized: Whether the JVM should ignore any VM options it + /// does not recognize. + /// - replace: replace the existing shared JVM instance + public static func shared( + classpath: [String] = [], + vmOptions: [String] = [], + ignoreUnrecognized: Bool = false, + replace: Bool = false + ) throws -> JavaVirtualMachine { + precondition( + !classpath.contains(where: { $0.contains(FileManager.pathSeparator) }), + "Classpath element must not contain `\(FileManager.pathSeparator)`! Split the path into elements! Was: \(classpath)" + ) + + return try sharedJVM.withLock { (sharedJVMPointer: inout JavaVirtualMachine?) in + // If we already have a JavaVirtualMachine instance, return it. + if replace { + print("[swift-java] Replace JVM instance!") + try sharedJVMPointer?.destroyJVM() + sharedJVMPointer = nil + } else { + if let existingInstance = sharedJVMPointer { + // FIXME: this isn't ideal; we silently ignored that we may have requested a different classpath or options + return existingInstance + } + } + + while true { + var wasExistingVM: Bool = false + while true { + // Query the JVM itself to determine whether there is a JVM + // instance that we don't yet know about. + var jvm: UnsafeMutablePointer? = nil + var numJVMs: jsize = 0 + if JNI_GetCreatedJavaVMs(&jvm, 1, &numJVMs) == JNI_OK, numJVMs >= 1 { + // Adopt this JVM into a new instance of the JavaVirtualMachine + // wrapper. + let javaVirtualMachine = JavaVirtualMachine(adoptingJVM: jvm!) + sharedJVMPointer = javaVirtualMachine + return javaVirtualMachine + } + + precondition( + !wasExistingVM, + "JVM reports that an instance of the JVM was already created, but we didn't see it." + ) + + // Create a new instance of the JVM. + let javaVirtualMachine: JavaVirtualMachine + do { + javaVirtualMachine = try JavaVirtualMachine( + classpath: classpath, + vmOptions: vmOptions, + ignoreUnrecognized: ignoreUnrecognized + ) + } catch VMError.existingVM { + // We raced with code outside of this JavaVirtualMachine instance + // that created a VM while we were trying to do the same. Go + // through the loop again to pick up the underlying JVM pointer. + wasExistingVM = true + continue + } + + sharedJVMPointer = javaVirtualMachine + return javaVirtualMachine + } + } + } + } + + /// "Forget" the shared JavaVirtualMachine instance. + /// + /// This will allow the shared JavaVirtualMachine instance to be deallocated. + public static func forgetShared() { + sharedJVM.withLock { sharedJVMPointer in + sharedJVMPointer = nil + } + } + + /// Set the shared JavaVirtualMachine instance. + /// + /// This is used by `JNI_OnLoad` to register the JVM instance + /// when loaded as a dynamic library by Java. + public static func setShared(_ javaVM: JavaVirtualMachine) { + sharedJVM.withLock { $0 = javaVM } + } +} + +extension JavaVirtualMachine { + /// Describes the kinds of errors that can occur when interacting with JNI. + enum VMError: Error { + /// There is already a Java Virtual Machine. + case existingVM + + /// JNI version mismatch error. + case jniVersion + + /// Thread is detached from the VM. + case threadDetached + + /// Out of memory. + case outOfMemory + + /// Invalid arguments. + case invalidArguments + + /// Unknown JNI error. + case unknown(jint, file: String, line: UInt) + + init?(fromJNIError error: jint, file: String = #fileID, line: UInt = #line) { + switch error { + case JNI_OK: return nil + case JNI_EDETACHED: self = .threadDetached + case JNI_EVERSION: self = .jniVersion + case JNI_ENOMEM: self = .outOfMemory + case JNI_EEXIST: self = .existingVM + case JNI_EINVAL: self = .invalidArguments + default: self = .unknown(error, file: file, line: line) + } + } + } + + enum JavaKitError: Error { + case classpathEntryNotFound(entry: String, classpath: [String]) + } +} diff --git a/Sources/SwiftJavaJNICore/VirtualMachine/LockedState.swift b/Sources/SwiftJavaJNICore/VirtualMachine/LockedState.swift new file mode 100644 index 0000000..745dfa5 --- /dev/null +++ b/Sources/SwiftJavaJNICore/VirtualMachine/LockedState.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(os) +import os +#if FOUNDATION_FRAMEWORK && canImport(C.os.lock) +import C.os.lock +#endif +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WinSDK) +import WinSDK +#endif + +package struct LockedState { + + // Internal implementation for a cheap lock to aid sharing code across platforms + private struct _Lock { + #if canImport(os) + typealias Primitive = os_unfair_lock + #elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl) + typealias Primitive = pthread_mutex_t + #elseif canImport(WinSDK) + typealias Primitive = SRWLOCK + #elseif os(WASI) + // WASI is single-threaded, so we don't need a lock. + typealias Primitive = Void + #endif + + typealias PlatformLock = UnsafeMutablePointer + var _platformLock: PlatformLock + + fileprivate static func initialize(_ platformLock: PlatformLock) { + #if canImport(os) + platformLock.initialize(to: os_unfair_lock()) + #elseif canImport(Bionic) || canImport(Glibc) + pthread_mutex_init(platformLock, nil) + #elseif canImport(WinSDK) + InitializeSRWLock(platformLock) + #elseif os(WASI) + // no-op + #endif + } + + fileprivate static func deinitialize(_ platformLock: PlatformLock) { + #if canImport(Bionic) || canImport(Glibc) + pthread_mutex_destroy(platformLock) + #endif + platformLock.deinitialize(count: 1) + } + + static fileprivate func lock(_ platformLock: PlatformLock) { + #if canImport(os) + os_unfair_lock_lock(platformLock) + #elseif canImport(Bionic) || canImport(Glibc) + pthread_mutex_lock(platformLock) + #elseif canImport(WinSDK) + AcquireSRWLockExclusive(platformLock) + #elseif os(WASI) + // no-op + #endif + } + + static fileprivate func unlock(_ platformLock: PlatformLock) { + #if canImport(os) + os_unfair_lock_unlock(platformLock) + #elseif canImport(Bionic) || canImport(Glibc) + pthread_mutex_unlock(platformLock) + #elseif canImport(WinSDK) + ReleaseSRWLockExclusive(platformLock) + #elseif os(WASI) + // no-op + #endif + } + } + + private class _Buffer: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { + _Lock.deinitialize($0) + } + } + } + + private let _buffer: ManagedBuffer + + package init(initialState: State) { + _buffer = _Buffer.create( + minimumCapacity: 1, + makingHeaderWith: { buf in + buf.withUnsafeMutablePointerToElements { + _Lock.initialize($0) + } + return initialState + } + ) + } + + package func withLock(_ body: @Sendable (inout State) throws -> T) rethrows -> T { + try withLockUnchecked(body) + } + + package func withLockUnchecked(_ body: (inout State) throws -> T) rethrows -> T { + try _buffer.withUnsafeMutablePointers { state, lock in + _Lock.lock(lock) + defer { _Lock.unlock(lock) } + return try body(&state.pointee) + } + } + + // Ensures the managed state outlives the locked scope. + package func withLockExtendingLifetimeOfState(_ body: @Sendable (inout State) throws -> T) rethrows -> T { + try _buffer.withUnsafeMutablePointers { state, lock in + _Lock.lock(lock) + return try withExtendedLifetime(state.pointee) { + defer { _Lock.unlock(lock) } + return try body(&state.pointee) + } + } + } +} + +extension LockedState where State == Void { + package init() { + self.init(initialState: ()) + } + + package func withLock(_ body: @Sendable () throws -> R) rethrows -> R { + try withLock { _ in + try body() + } + } + + package func lock() { + _buffer.withUnsafeMutablePointerToElements { lock in + _Lock.lock(lock) + } + } + + package func unlock() { + _buffer.withUnsafeMutablePointerToElements { lock in + _Lock.unlock(lock) + } + } +} + +extension LockedState: @unchecked Sendable where State: Sendable {} diff --git a/Sources/SwiftJavaJNICore/VirtualMachine/ThreadLocalStorage.swift b/Sources/SwiftJavaJNICore/VirtualMachine/ThreadLocalStorage.swift new file mode 100644 index 0000000..8a08d03 --- /dev/null +++ b/Sources/SwiftJavaJNICore/VirtualMachine/ThreadLocalStorage.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WinSDK) +import WinSDK +#endif + +#if !(canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) || canImport(WinSDK)) +private var _globalTlsValue: UnsafeMutableRawPointer? +#endif + +package struct ThreadLocalStorage: ~Copyable { + #if canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) + private typealias PlatformKey = pthread_key_t + #elseif canImport(WinSDK) + private typealias PlatformKey = DWORD + #else + private typealias PlatformKey = Void + #endif + + #if canImport(Darwin) + package typealias Value = UnsafeMutableRawPointer + #else + package typealias Value = UnsafeMutableRawPointer? + #endif + + package typealias OnThreadExit = @convention(c) (_: Value) -> Void + + #if canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) + private var _key: PlatformKey + #elseif canImport(WinSDK) + private let _key: PlatformKey + #endif + + package init(onThreadExit: OnThreadExit) { + #if canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) + _key = 0 + pthread_key_create(&_key, onThreadExit) + #elseif canImport(WinSDK) + _key = FlsAlloc(onThreadExit) + #endif + } + + package func get() -> UnsafeMutableRawPointer? { + #if canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) + pthread_getspecific(_key) + #elseif canImport(WinSDK) + FlsGetValue(_key) + #else + _globalTlsValue + #endif + } + + package func set(_ value: Value) { + #if canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) + pthread_setspecific(_key, value) + #elseif canImport(WinSDK) + FlsSetValue(_key, value) + #else + _globalTlsValue = value + #endif + } + + deinit { + #if canImport(Darwin) || canImport(Bionic) || canImport(Glibc) || canImport(Musl) + pthread_key_delete(_key) + #elseif canImport(WinSDK) + FlsFree(_key) + #endif + } +} diff --git a/Tests/SwiftJavaJNICoreTests/ManglingTests.swift b/Tests/SwiftJavaJNICoreTests/ManglingTests.swift new file mode 100644 index 0000000..672555c --- /dev/null +++ b/Tests/SwiftJavaJNICoreTests/ManglingTests.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaJNICore +import Testing + +@Suite +struct ManglingTests { + + @Test + func methodMangling() throws { + let demangledSignature = try MethodSignature( + mangledName: "(ILjava/lang/String;[I)J" + ) + let expectedSignature = MethodSignature( + resultType: .long, + parameterTypes: [ + .int, + .class(package: "java.lang", name: "String"), + .array(.int), + ] + ) + #expect(demangledSignature == expectedSignature) + #expect(expectedSignature.mangledName == "(ILjava/lang/String;[I)J") + } +} diff --git a/Tests/SwiftJavaJNICoreTests/NoopTest.swift b/Tests/SwiftJavaJNICoreTests/NoopTest.swift deleted file mode 100644 index 1dac5e2..0000000 --- a/Tests/SwiftJavaJNICoreTests/NoopTest.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Testing - -@Test func noop() { -}