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:
dylib
file be signed correctly? Currently, I use the 3rd Party Mac Developer Application
identity. Is this the right approach?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 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.
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?
}
.dylib
LibraryUse 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
.dylib
File in the ProjectAfter compiling and signing the library:
desktop/resources/macos-arm64
.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
.
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.
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.