I have a macOS application built using Compose Multiplatform. In this application, I need to call several native system functions, such as accessing a folder for writing or retrieving the device hostname.
As I understand it, this can be done by creating a dylib
library using JNI and then calling the required methods from Kotlin. Here’s my current implementation:
MacOsUtils.swift:
@objc public class MacOsUtils: NSObject {
@objc public func getDeviceHostName() -> String {
if let hostName = Host.current().localizedName {
print("Device host name: \(hostName)")
return hostName
} else {
print("Unable to retrieve device host name")
return "Unknown Device"
}
}
}
MacOsUtilsWrapper.mm:
#include <jni.h>
#include <string>
#include "MacOsUtils-Swift.h"
#include <Foundation/Foundation.h>
JavaVM* jvm = nullptr;
extern "C" {
JNIEXPORT jstring JNICALL Java_com_my_app_common_core_persits_MacOsUtils_getDeviceHostName(JNIEnv* env, jobject obj) {
@autoreleasepool {
MacOsUtils* macOsUtils = [MacOsUtils createInstance];
if (!macOsUtils) {
NSLog(@"Failed to create MacOsUtils instance");
return nullptr;
}
NSString* deviceName = [macOsUtils getDeviceHostName];
if (deviceName != nil) {
const char* deviceNameCStr = [deviceName UTF8String];
return env->NewStringUTF(deviceNameCStr);
} else {
return nullptr;
}
}
}
}
I compile the library with the following commands:
swiftc -emit-library -emit-objc-header -emit-objc-header-path MacOsUtils-Swift.h -static -o libMacOsUtils.a MacOsUtils.swift
clang++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -I"." -shared -o libMacOsUtilsWrapper.dylib MacOsUtilsWrapper.mm -L. -lMacOsUtils -framework Foundation -Wl,-all_load
I sign the library with this command:
codesign --force --sign "3rd Party Mac Developer Application: My Name (TEAM ID)" --timestamp --options runtime libMacOsUtilsWrapper.dylib
The libMacOsUtilsWrapper.dylib
file is then placed in the common/src/desktopMain/resources
folder. In my Kotlin code, I copy the library to a temporary directory and attempt to load it as follows:
object MacOsUtils {
init {
loadLibrary()
}
private fun loadLibrary() {
System.setProperty("jna.nosys", "true")
val libName = "libMacOsUtilsWrapper.dylib"
checkNotNull(this::class.java.classLoader?.getResource(libName)) {
"Library $libName not found in resources"
}
val tmpDir = Files.createTempDirectory("MyApp").toFile()
Napier.d("Loading $libName to $tmpDir")
val tmpLib = File(tmpDir, libName)
if (!tmpLib.exists()) {
this::class.java.classLoader?.getResourceAsStream(libName)?.use { inputStream ->
FileOutputStream(tmpLib).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
@Suppress("UnsafeDynamicallyLoadedCode")
System.load(tmpLib.absolutePath)
}
external fun getDeviceHostName(): String?
}
Locally, when running the application from Android Studio, everything works correctly. However, it does not work after publishing to TestFlight. When launching the app, I encounter this error:
The file "libMacOsUtilsWrapper.dylib" couldn’t be opened because Apple cannot check it for malicious software.
I have three questions:
- Is this the correct way to call native functions in a Compose Multiplatform macOS application?
- How should the
dylib
file be signed correctly? Currently, I use the3rd Party Mac Developer Application
identity. Is this the right approach? - How should the
dylib
file be packaged with the application so that it works both when running the app from Android Studio and for end users after publishing to TestFlight/App Store?
Any advice or guidance would be greatly appreciated. Thank you!
I have a macOS application built using Compose Multiplatform. In this application, I need to call several native system functions, such as accessing a folder for writing or retrieving the device hostname.
As I understand it, this can be done by creating a dylib
library using JNI and then calling the required methods from Kotlin. Here’s my current implementation:
MacOsUtils.swift:
@objc public class MacOsUtils: NSObject {
@objc public func getDeviceHostName() -> String {
if let hostName = Host.current().localizedName {
print("Device host name: \(hostName)")
return hostName
} else {
print("Unable to retrieve device host name")
return "Unknown Device"
}
}
}
MacOsUtilsWrapper.mm:
#include <jni.h>
#include <string>
#include "MacOsUtils-Swift.h"
#include <Foundation/Foundation.h>
JavaVM* jvm = nullptr;
extern "C" {
JNIEXPORT jstring JNICALL Java_com_my_app_common_core_persits_MacOsUtils_getDeviceHostName(JNIEnv* env, jobject obj) {
@autoreleasepool {
MacOsUtils* macOsUtils = [MacOsUtils createInstance];
if (!macOsUtils) {
NSLog(@"Failed to create MacOsUtils instance");
return nullptr;
}
NSString* deviceName = [macOsUtils getDeviceHostName];
if (deviceName != nil) {
const char* deviceNameCStr = [deviceName UTF8String];
return env->NewStringUTF(deviceNameCStr);
} else {
return nullptr;
}
}
}
}
I compile the library with the following commands:
swiftc -emit-library -emit-objc-header -emit-objc-header-path MacOsUtils-Swift.h -static -o libMacOsUtils.a MacOsUtils.swift
clang++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -I"." -shared -o libMacOsUtilsWrapper.dylib MacOsUtilsWrapper.mm -L. -lMacOsUtils -framework Foundation -Wl,-all_load
I sign the library with this command:
codesign --force --sign "3rd Party Mac Developer Application: My Name (TEAM ID)" --timestamp --options runtime libMacOsUtilsWrapper.dylib
The libMacOsUtilsWrapper.dylib
file is then placed in the common/src/desktopMain/resources
folder. In my Kotlin code, I copy the library to a temporary directory and attempt to load it as follows:
object MacOsUtils {
init {
loadLibrary()
}
private fun loadLibrary() {
System.setProperty("jna.nosys", "true")
val libName = "libMacOsUtilsWrapper.dylib"
checkNotNull(this::class.java.classLoader?.getResource(libName)) {
"Library $libName not found in resources"
}
val tmpDir = Files.createTempDirectory("MyApp").toFile()
Napier.d("Loading $libName to $tmpDir")
val tmpLib = File(tmpDir, libName)
if (!tmpLib.exists()) {
this::class.java.classLoader?.getResourceAsStream(libName)?.use { inputStream ->
FileOutputStream(tmpLib).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
@Suppress("UnsafeDynamicallyLoadedCode")
System.load(tmpLib.absolutePath)
}
external fun getDeviceHostName(): String?
}
Locally, when running the application from Android Studio, everything works correctly. However, it does not work after publishing to TestFlight. When launching the app, I encounter this error:
The file "libMacOsUtilsWrapper.dylib" couldn’t be opened because Apple cannot check it for malicious software.
I have three questions:
- Is this the correct way to call native functions in a Compose Multiplatform macOS application?
- How should the
dylib
file be signed correctly? Currently, I use the3rd Party Mac Developer Application
identity. Is this the right approach? - How should the
dylib
file be packaged with the application so that it works both when running the app from Android Studio and for end users after publishing to TestFlight/App Store?
Any advice or guidance would be greatly appreciated. Thank you!
Share Improve this question edited Nov 19, 2024 at 15:28 BArtWell asked Nov 17, 2024 at 17:10 BArtWellBArtWell 4,06410 gold badges67 silver badges109 bronze badges1 Answer
Reset to default 1I managed to integrate a native .dylib
library into my Kotlin Multiplatform Desktop (Compose for Desktop) application successfully. Here’s the step-by-step guide, starting with the implementation, moving to compilation and placement, and finishing with how to use it in your project.
Solution Steps:
1. Write the Native Code
The integration consists of three parts: Swift, Objective-C (as a bridge), and Kotlin.
Swift Code:
import Foundation @objc public class MacOsUtils: NSObject { private func logMessage(_ message: String) { NSLog("[MacOsUtils] \(message)") } @objc public func getDeviceHostName() -> String { if let hostName = Host.current().localizedName { logMessage("Device host name retrieved: \(hostName)") return hostName } else { logMessage("Unable to retrieve device host name") return "Unknown Device" } } }
Objective-C Code:
#include <jni.h> #include <string> #include "MacOsUtils-Swift.h" #include <Foundation/Foundation.h> JavaVM* jvm = nullptr; extern "C" { JNIEXPORT jstring JNICALL Java_com_site_myapp_path_to_MacOsUtils_getDeviceHostName(JNIEnv* env, jobject obj) { MacOsUtils *macOsUtils = [[MacOsUtils alloc] init]; NSString *deviceName = [macOsUtils getDeviceHostName]; if (deviceName != nil) { const char *deviceNameCStr = [deviceName UTF8String]; return env->NewStringUTF(deviceNameCStr); } else { return nullptr; } } }
Kotlin Code:
object MacOsUtils { init { val libName = "libMacOsUtilsWrapper.dylib" val resourcesPath = File(System.getProperty("compose.application.resources.dir")) val libPath = resourcesPath.resolve(libName) @Suppress("UnsafeDynamicallyLoadedCode") System.load(libPath.absolutePath) } external fun getDeviceHostName(): String? }
2. Compile and Sign the .dylib
Library
Use the following commands to compile and sign the library:
# Build the static library and Objective-C header
swiftc -emit-library -emit-objc-header -emit-objc-header-path MacOsUtils-Swift.h -static -o libMacOsUtils.a MacOsUtils.swift
# Create the `.dylib` library
clang++ -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -I"." -shared -o libMacOsUtilsWrapper.dylib MacOsUtilsWrapper.mm -L. -lMacOsUtils -framework Foundation -Wl,-all_load
# Sign the `.dylib` for App Store/TestFlight
codesign --force --sign "3rd Party Mac Developer Application: Your Name (TEAMID)" --timestamp --options runtime libMacOsUtilsWrapper.dylib
3. Place the .dylib
File in the Project
After compiling and signing the library:
- Create the directory
desktop/resources/macos-arm64
. - Place the
libMacOsUtilsWrapper.dylib
file in this directory.
When the application is installed from App Store or TestFlight, the library will be placed in /Applications/MyApp.app/Contents/app/resources
.
4. Configure the Project
Update your desktop/build.gradle.kts
to include the following:
compose.desktop {
application {
nativeDistributions {
appResourcesRootDir.set(project.layout.projectDirectory.dir("resources"))
}
}
}
This ensures that the resources
directory is packaged with your application.
5. Use the Native Function in Kotlin
Now you can use the native function anywhere in your Kotlin application as follows:
val hostName = MacOsUtils.getDeviceHostName()
println("Device Host Name: $hostName")
The library will be loaded dynamically at runtime, and the native functionality will be accessible via the MacOsUtils
object.
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745630091a4637052.html
评论列表(0条)