diff --git a/CodenameOne/src/com/codename1/system/package-info.java b/CodenameOne/src/com/codename1/system/package-info.java index 2b5b12fdb7..66468b03b3 100644 --- a/CodenameOne/src/com/codename1/system/package-info.java +++ b/CodenameOne/src/com/codename1/system/package-info.java @@ -2,7 +2,7 @@ /// [support for making platform native API calls](https://www.codenameone.com/how-do-i---access-native-device-functionality-invoke-native-interfaces.html). Notice /// that when we say "native" we do not mean C/C++ always but rather the platforms "native" environment. So in the /// case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C -/// message would be sent and so forth. +/// or Swift message would be sent and so forth. /// /// Native interfaces are designed to only allow primitive types, Strings, arrays (single dimension only!) of primitives /// and PeerComponent values. Any other type of parameter/return type is prohibited. However, once in the native layer @@ -59,9 +59,9 @@ /// These sources should be placed under the appropriate folder in the native directory and are sent to the /// server for compilation. /// -/// For Objective-C, one would need to define a class matching the name of the package and the class name -/// combined where the "." elements are replaced by underscores. One would need to provide both a header and -/// an "m" file following this convention e.g.: +/// For iOS, one would need to define a class matching the name of the package and the class name +/// combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C +/// (by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.: /// /// ```java /// @interface com_my_code_MyNative : NSObject { diff --git a/CodenameOne/src/com/codename1/system/package.html b/CodenameOne/src/com/codename1/system/package.html index 590dd50809..400f9a20ef 100644 --- a/CodenameOne/src/com/codename1/system/package.html +++ b/CodenameOne/src/com/codename1/system/package.html @@ -8,8 +8,8 @@ support for making platform native API calls. Notice that when we say "native" we do not mean C/C++ always but rather the platforms "native" environment. So in the - case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C - message would be sent and so forth. + case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C + or Swift message would be sent and so forth.

Native interfaces are designed to only allow primitive types, Strings, arrays (single dimension only!) of primitives @@ -66,9 +66,9 @@ server for compilation.

- For Objective-C, one would need to define a class matching the name of the package and the class name - combined where the "." elements are replaced by underscores. One would need to provide both a header and - an "m" file following this convention e.g.: + For iOS, one would need to define a class matching the name of the package and the class name + combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C + (by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.:

 @interface com_my_code_MyNative : NSObject {
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
index 4377ebab93..3f83d699cf 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
@@ -41,7 +41,9 @@
 import java.awt.image.ImageProducer;
 import java.awt.image.RGBImageFilter;
 import java.io.*;
+import java.lang.reflect.Method;
 
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.nio.channels.FileChannel;
@@ -3972,6 +3974,117 @@ static String xmlize(String s) {
     }
 
 
+    @Override
+    protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentClassLoader, File stubDir, File... classesDirectory) throws MalformedURLException, IOException {
+        Class[] discoveredNativeInterfaces = findNativeInterfaces(parentClassLoader, classesDirectory);
+        String registerNativeFunctions = "";
+        if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
+            for (Class n : discoveredNativeInterfaces) {
+                registerNativeFunctions += "        NativeLookup.register(" + n.getName() + ".class, "
+                        + n.getName() + "Stub.class" + ");\n";
+            }
+        }
+
+        if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
+            for (Class currentNative : discoveredNativeInterfaces) {
+                File folder = new File(stubDir, currentNative.getPackage().getName().replace('.', File.separatorChar));
+                folder.mkdirs();
+                File javaFile = new File(folder, currentNative.getSimpleName() + "Stub.java");
+
+                String javaImplSourceFile = "package " + currentNative.getPackage().getName() + ";\n\n"
+                        + "import com.codename1.ui.PeerComponent;\n\n"
+                        + "public class " + currentNative.getSimpleName() + "Stub implements " + currentNative.getSimpleName() + "{\n"
+                        + "    private final Object impl = createImpl();\n\n"
+                        + "    private static Object createImpl() {\n"
+                        + "        try {\n"
+                        + "            return Class.forName(\"" + currentNative.getName() + getImplSuffix() + "\").newInstance();\n"
+                        + "        } catch (Throwable t) {\n"
+                        + "            throw new RuntimeException(\"Failed to instantiate native implementation for " + currentNative.getName() + "\", t);\n"
+                        + "        }\n"
+                        + "    }\n\n"
+                        + "    private Object __cn1Invoke(String methodName, Object[] args) {\n"
+                        + "        try {\n"
+                        + "            java.lang.reflect.Method[] methods = impl.getClass().getMethods();\n"
+                        + "            for (java.lang.reflect.Method method : methods) {\n"
+                        + "                if (method.getName().equals(methodName) && method.getParameterTypes().length == args.length) {\n"
+                        + "                    return method.invoke(impl, args);\n"
+                        + "                }\n"
+                        + "            }\n"
+                        + "            throw new RuntimeException(methodName + \" with \" + args.length + \" args\");\n"
+                        + "        } catch (Throwable t) {\n"
+                        + "            throw new RuntimeException(\"Failed to invoke native method \" + methodName, t);\n"
+                        + "        }\n"
+                        + "    }\n\n";
+
+                for (Method m : currentNative.getMethods()) {
+                    String name = m.getName();
+                    if (name.equals("hashCode") || name.equals("equals") || name.equals("toString")) {
+                        continue;
+                    }
+
+                    Class returnType = m.getReturnType();
+
+                    javaImplSourceFile += "    public " + returnType.getSimpleName() + " " + name + "(";
+                    Class[] params = m.getParameterTypes();
+                    String args = "";
+                    if (params != null && params.length > 0) {
+                        for (int iter = 0; iter < params.length; iter++) {
+                            if (iter > 0) {
+                                javaImplSourceFile += ", ";
+                                args += ", ";
+                            }
+                            javaImplSourceFile += params[iter].getSimpleName() + " param" + iter;
+                            if (params[iter].getName().equals("com.codename1.ui.PeerComponent")) {
+                                args += convertPeerComponentToNative("param" + iter);
+                            } else {
+                                args += "param" + iter;
+                            }
+                        }
+                    }
+                    javaImplSourceFile += ") {\n";
+                    String invocationExpression = "__cn1Invoke(\"" + name + "\", new Object[]{" + args + "})";
+                    if (Void.class == returnType || Void.TYPE == returnType) {
+                        javaImplSourceFile += "        " + invocationExpression + ";\n    }\n\n";
+                    } else {
+                        if (returnType.getName().equals("com.codename1.ui.PeerComponent")) {
+                            javaImplSourceFile += "        return " + generatePeerComponentCreationCode(invocationExpression) + ";\n    }\n\n";
+                        } else if (returnType.isPrimitive()) {
+                            if (returnType == Boolean.TYPE) {
+                                javaImplSourceFile += "        return ((Boolean)" + invocationExpression + ").booleanValue();\n    }\n\n";
+                            } else if (returnType == Integer.TYPE) {
+                                javaImplSourceFile += "        return ((Integer)" + invocationExpression + ").intValue();\n    }\n\n";
+                            } else if (returnType == Long.TYPE) {
+                                javaImplSourceFile += "        return ((Long)" + invocationExpression + ").longValue();\n    }\n\n";
+                            } else if (returnType == Byte.TYPE) {
+                                javaImplSourceFile += "        return ((Byte)" + invocationExpression + ").byteValue();\n    }\n\n";
+                            } else if (returnType == Short.TYPE) {
+                                javaImplSourceFile += "        return ((Short)" + invocationExpression + ").shortValue();\n    }\n\n";
+                            } else if (returnType == Character.TYPE) {
+                                javaImplSourceFile += "        return ((Character)" + invocationExpression + ").charValue();\n    }\n\n";
+                            } else if (returnType == Float.TYPE) {
+                                javaImplSourceFile += "        return ((Float)" + invocationExpression + ").floatValue();\n    }\n\n";
+                            } else if (returnType == Double.TYPE) {
+                                javaImplSourceFile += "        return ((Double)" + invocationExpression + ").doubleValue();\n    }\n\n";
+                            } else {
+                                javaImplSourceFile += "        return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n    }\n\n";
+                            }
+                        } else {
+                            javaImplSourceFile += "        return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n    }\n\n";
+                        }
+                    }
+                }
+
+                javaImplSourceFile += "}\n";
+
+                try (FileOutputStream out = new FileOutputStream(javaFile)) {
+                    out.write(javaImplSourceFile.getBytes(StandardCharsets.UTF_8));
+                }
+            }
+        }
+
+        return registerNativeFunctions;
+    }
+
     @Override
     protected String generatePeerComponentCreationCode(String methodCallString) {
         return "PeerComponent.create(" + methodCallString + ")";
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
index e635fb255d..f3fed6879b 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
@@ -1099,14 +1099,42 @@ public void usesClassMethod(String cls, String method) {
                         + "#include \"java_lang_String.h\"\n"
                         + "#import \"CodenameOne_GLViewController.h\"\n"
                         + "#import \n"
-                        + "#import \"" + classNameWithUnderscores + "Impl.h\"\n" + newVMInclude
+                        + newVMInclude
                         + "#include \"" + classNameWithUnderscores + "ImplCodenameOne.h\"\n\n"
+                        + "static id cn1_createNativeInterfacePeer(NSString* className) {\n"
+                        + "    NSMutableArray* candidates = [NSMutableArray arrayWithObject:className];\n"
+                        + "    NSString* executableName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\"CFBundleExecutable\"];\n"
+                        + "    NSString* bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\"CFBundleName\"];\n"
+                        + "    NSArray* moduleNames = @[executableName ?: @\"\", bundleName ?: @\"\"];\n"
+                        + "    for(NSString* moduleName in moduleNames) {\n"
+                        + "        if(moduleName.length == 0) {\n"
+                        + "            continue;\n"
+                        + "        }\n"
+                        + "        NSString* sanitized = [[moduleName stringByReplacingOccurrencesOfString:@\"-\" withString:@\"_\"] stringByReplacingOccurrencesOfString:@\" \" withString:@\"_\"];\n"
+                        + "        [candidates addObject:[sanitized stringByAppendingFormat:@\".%@\", className]];\n"
+                        + "        if(![sanitized isEqualToString:moduleName]) {\n"
+                        + "            [candidates addObject:[moduleName stringByAppendingFormat:@\".%@\", className]];\n"
+                        + "        }\n"
+                        + "    }\n"
+                        + "    Class cls = Nil;\n"
+                        + "    for(NSString* candidate in candidates) {\n"
+                        + "        cls = NSClassFromString(candidate);\n"
+                        + "        if(cls != Nil) {\n"
+                        + "            break;\n"
+                        + "        }\n"
+                        + "    }\n"
+                        + "    if(cls == Nil) {\n"
+                        + "        NSLog(@\"[CN1] Failed to find native interface class %@. Tried: %@\", className, candidates);\n"
+                        + "        return nil;\n"
+                        + "    }\n"
+                        + "    return [[cls alloc] init];\n"
+                        + "}\n\n"
                         + "JAVA_LONG " + classNameWithUnderscores + "ImplCodenameOne_initializeNativePeer__" + postfixForNewVM + "(" + prefixForNewVM + ") {\n"
-                        + "    " + classNameWithUnderscores + "Impl* i = [[" + classNameWithUnderscores + "Impl alloc] init];\n"
+                        + "    id i = cn1_createNativeInterfacePeer(@\"" + classNameWithUnderscores + "Impl\");\n"
                         + "    return i;\n"
                         + "}\n\n"
                         + "void " + classNameWithUnderscores + "ImplCodenameOne_releaseNativePeerInstance___long(" + prefix2ForNewVM + "JAVA_LONG l) {\n"
-                        + "    " + classNameWithUnderscores + "Impl* i = (" + classNameWithUnderscores + "Impl*)l;\n"
+                        + "    id i = (id)l;\n"
                         + "    [i release];\n"
                         + "}\n\n"
                         + "extern NSData* arrayToData(JAVA_OBJECT arr);\n"
@@ -1135,8 +1163,7 @@ public void usesClassMethod(String cls, String method) {
                     String mFileBody;
 
                     mFileArgs = "(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT me";
-                    mFileBody = "    " + classNameWithUnderscores + "Impl* ptr = (" + classNameWithUnderscores +
-                        "Impl*)get_field_" + classNameWithUnderscores + "ImplCodenameOne_nativePeer(me);\n";
+                    mFileBody = "    id ptr = (id)get_field_" + classNameWithUnderscores + "ImplCodenameOne_nativePeer(me);\n";
 
                     
                     if(!(returnType.equals(Void.class) || returnType.equals(Void.TYPE))) {
diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
new file mode 100644
index 0000000000..67f5bbf4cc
--- /dev/null
+++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
@@ -0,0 +1,11 @@
+package com.codenameone.examples.hellocodenameone
+
+class SwiftKotlinNativeImpl {
+    fun implementationLanguage(): String {
+        return "kotlin"
+    }
+
+    fun isSupported(): Boolean {
+        return true
+    }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java
new file mode 100644
index 0000000000..9490258ed5
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java
@@ -0,0 +1,30 @@
+package com.codenameone.examples.hellocodenameone;
+
+import com.codename1.system.NativeLookup;
+import com.codename1.ui.CN;
+
+public final class NativeInterfaceLanguageValidator {
+    private NativeInterfaceLanguageValidator() {
+    }
+
+    public static void validate() {
+        String platformName = CN.getPlatformName();
+        String normalizedPlatform = platformName == null ? "" : platformName.toLowerCase();
+        boolean isAndroid = normalizedPlatform.contains("android");
+        boolean isIos = normalizedPlatform.contains("ios") || normalizedPlatform.contains("iphone");
+        if (!isAndroid && !isIos) {
+            return;
+        }
+
+        SwiftKotlinNative nativeImpl = NativeLookup.create(SwiftKotlinNative.class);
+        if (nativeImpl == null || !nativeImpl.isSupported()) {
+            throw new IllegalStateException("SwiftKotlinNative is not available on " + platformName);
+        }
+
+        String expected = isAndroid ? "kotlin" : "swift";
+        String actual = nativeImpl.implementationLanguage();
+        if (!expected.equalsIgnoreCase(actual)) {
+            throw new IllegalStateException("Expected " + expected + " implementation on " + platformName + " but got " + actual);
+        }
+    }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java
new file mode 100644
index 0000000000..2faf787457
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java
@@ -0,0 +1,7 @@
+package com.codenameone.examples.hellocodenameone;
+
+import com.codename1.system.NativeInterface;
+
+public interface SwiftKotlinNative extends NativeInterface {
+    String implementationLanguage();
+}
diff --git a/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt b/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt
index 2214782035..5de71abd27 100644
--- a/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt
+++ b/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt
@@ -14,6 +14,7 @@ open class HelloCodenameOne : Lifecycle() {
             "Jailbroken device detected by Display.isJailbrokenDevice()."
         }
         DefaultMethodDemo.validate()
+        NativeInterfaceLanguageValidator.validate()
         Cn1ssDeviceRunner.addTest(KotlinUiTest())
         TestReporting.setInstance(Cn1ssDeviceRunnerReporter())
     }
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift
new file mode 100644
index 0000000000..328133d274
--- /dev/null
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift
@@ -0,0 +1,12 @@
+import Foundation
+
+@objc(com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl)
+class com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl: NSObject {
+    @objc func implementationLanguage() -> String {
+        return "swift"
+    }
+
+    @objc func isSupported() -> Bool {
+        return true
+    }
+}