From 8d135147eb8fc42dee7b2e9b2bfce27834d64ba5 Mon Sep 17 00:00:00 2001 From: liannacasper <67953602+liannacasper@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:50:32 +0300 Subject: [PATCH 01/27] Remove Locale dependency from native language validator --- .../com/codename1/system/package-info.java | 8 ++--- .../src/com/codename1/system/package.html | 10 +++---- .../com/codename1/builders/IPhoneBuilder.java | 23 ++++++++++---- .../hellocodenameone/SwiftKotlinNativeImpl.kt | 11 +++++++ .../NativeInterfaceLanguageValidator.java | 30 +++++++++++++++++++ .../hellocodenameone/SwiftKotlinNative.java | 7 +++++ .../hellocodenameone/HelloCodenameOne.kt | 1 + ...llocodenameone_SwiftKotlinNativeImpl.swift | 12 ++++++++ 8 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift 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/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java
index e635fb255d..97ce2b91fe 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,28 @@ 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"
+ + " Class cls = NSClassFromString(className);\n"
+ + " if(cls == Nil) {\n"
+ + " NSString* mainBundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\"CFBundleName\"];\n"
+ + " if(mainBundleName != nil) {\n"
+ + " NSString* prefixedName = [mainBundleName stringByAppendingFormat:@\".%@\", className];\n"
+ + " cls = NSClassFromString(prefixedName);\n"
+ + " }\n"
+ + " }\n"
+ + " if(cls == Nil) {\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 +1149,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
+ }
+}
From cb238b5e7387f3181d3effec923d37256ed53be0 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 14:08:07 +0300
Subject: [PATCH 02/27] Fix Android native stub by using Java implementation
class
---
.../hellocodenameone/SwiftKotlinNativeImpl.java | 13 +++++++++++++
.../hellocodenameone/SwiftKotlinNativeImpl.kt | 11 -----------
2 files changed, 13 insertions(+), 11 deletions(-)
create mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java
delete mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java
new file mode 100644
index 0000000000..5ee7a9ab77
--- /dev/null
+++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java
@@ -0,0 +1,13 @@
+package com.codenameone.examples.hellocodenameone;
+
+public class SwiftKotlinNativeImpl {
+ public String implementationLanguage() {
+ // Android native stubs are compiled with javac in the current pipeline.
+ // Keep this implementation in Java even though it validates "kotlin" mode.
+ return "kotlin";
+ }
+
+ public boolean isSupported() {
+ return true;
+ }
+}
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
deleted file mode 100644
index 67f5bbf4cc..0000000000
--- a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.codenameone.examples.hellocodenameone
-
-class SwiftKotlinNativeImpl {
- fun implementationLanguage(): String {
- return "kotlin"
- }
-
- fun isSupported(): Boolean {
- return true
- }
-}
From 90d961847e51670772493385a766b919447fb2a1 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 14:08:11 +0300
Subject: [PATCH 03/27] Support Kotlin native impls in generated Android native
stubs
---
.../java/com/codename1/builders/Executor.java | 49 +++++++++++++++++--
.../SwiftKotlinNativeImpl.java | 13 -----
.../hellocodenameone/SwiftKotlinNativeImpl.kt | 11 +++++
3 files changed, 56 insertions(+), 17 deletions(-)
delete mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java
create mode 100644 scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
index e3524630c6..e78b60f8a0 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
@@ -681,7 +681,27 @@ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentC
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 " + currentNative.getSimpleName() + getImplSuffix() + " impl = new " + currentNative.getSimpleName() + getImplSuffix() + "();\n\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 NoSuchMethodException(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();
@@ -709,13 +729,34 @@ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentC
}
}
javaImplSourceFile += ") {\n";
+ String invocationExpression = "__cn1Invoke(\"" + name + "\", new Object[]{" + args + "})";
if (Void.class == returnType || Void.TYPE == returnType) {
- javaImplSourceFile += " impl." + name + "(" + args + ");\n }\n\n";
+ javaImplSourceFile += " " + invocationExpression + ";\n }\n\n";
} else {
if (returnType.getName().equals("com.codename1.ui.PeerComponent")) {
- javaImplSourceFile += " return " + generatePeerComponentCreationCode("impl." + name + "(" + args + ")") + ";\n }\n\n";
+ 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 impl." + name + "(" + args + ");\n }\n\n";
+ javaImplSourceFile += " return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n }\n\n";
}
}
}
diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java
deleted file mode 100644
index 5ee7a9ab77..0000000000
--- a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.codenameone.examples.hellocodenameone;
-
-public class SwiftKotlinNativeImpl {
- public String implementationLanguage() {
- // Android native stubs are compiled with javac in the current pipeline.
- // Keep this implementation in Java even though it validates "kotlin" mode.
- return "kotlin";
- }
-
- public boolean isSupported() {
- return true;
- }
-}
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
+ }
+}
From d42de76ceaec0395083f82811e9816ca286a59ab Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 14:55:37 +0300
Subject: [PATCH 04/27] Avoid NoSuchMethodException in generated native stubs
---
.../src/main/java/com/codename1/builders/Executor.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
index e78b60f8a0..e35187df6b 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
@@ -697,7 +697,7 @@ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentC
+ " return method.invoke(impl, args);\n"
+ " }\n"
+ " }\n"
- + " throw new NoSuchMethodException(methodName + \" with \" + args.length + \" args\");\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"
From 3162274c89735eaee6a98ffb56a1f2399258789c Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 17:59:27 +0300
Subject: [PATCH 05/27] Scope reflective native stubs to Android builder only
---
.../builders/AndroidGradleBuilder.java | 111 ++++++++++++++++++
.../java/com/codename1/builders/Executor.java | 49 +-------
2 files changed, 115 insertions(+), 45 deletions(-)
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..0da9531668 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
@@ -3972,6 +3972,117 @@ static String xmlize(String s) {
}
+ @Override
+ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentClassLoader, File stubDir, File... classesDirectory) throws MalformedURLException, IOException {
+ nativeInterfaces = findNativeInterfaces(parentClassLoader, classesDirectory);
+ String registerNativeFunctions = "";
+ if (nativeInterfaces != null && nativeInterfaces.length > 0) {
+ for (Class n : nativeInterfaces) {
+ registerNativeFunctions += " NativeLookup.register(" + n.getName() + ".class, "
+ + n.getName() + "Stub.class" + ");\n";
+ }
+ }
+
+ if (nativeInterfaces != null && nativeInterfaces.length > 0) {
+ for (Class currentNative : nativeInterfaces) {
+ 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/Executor.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
index e35187df6b..e3524630c6 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java
@@ -681,27 +681,7 @@ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentC
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";
+ + " private " + currentNative.getSimpleName() + getImplSuffix() + " impl = new " + currentNative.getSimpleName() + getImplSuffix() + "();\n\n";
for (Method m : currentNative.getMethods()) {
String name = m.getName();
@@ -729,34 +709,13 @@ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentC
}
}
javaImplSourceFile += ") {\n";
- String invocationExpression = "__cn1Invoke(\"" + name + "\", new Object[]{" + args + "})";
if (Void.class == returnType || Void.TYPE == returnType) {
- javaImplSourceFile += " " + invocationExpression + ";\n }\n\n";
+ javaImplSourceFile += " impl." + name + "(" + args + ");\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";
- }
+ javaImplSourceFile += " return " + generatePeerComponentCreationCode("impl." + name + "(" + args + ")") + ";\n }\n\n";
} else {
- javaImplSourceFile += " return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n }\n\n";
+ javaImplSourceFile += " return impl." + name + "(" + args + ");\n }\n\n";
}
}
}
From 8497d18a4f0310831d229c508b286f3b5793de45 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 19:45:59 +0300
Subject: [PATCH 06/27] Add missing MalformedURLException import in
AndroidGradleBuilder
---
.../main/java/com/codename1/builders/AndroidGradleBuilder.java | 1 +
1 file changed, 1 insertion(+)
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 0da9531668..14ad4d1719 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
@@ -42,6 +42,7 @@
import java.awt.image.RGBImageFilter;
import java.io.*;
+import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.FileChannel;
From 09bb8994f08d2af4364c9e66f130d517903cac31 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 20:02:02 +0300
Subject: [PATCH 07/27] Fix AndroidGradleBuilder override compile issues
---
.../com/codename1/builders/AndroidGradleBuilder.java | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
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 14ad4d1719..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,6 +41,7 @@
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;
@@ -3975,17 +3976,17 @@ static String xmlize(String s) {
@Override
protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentClassLoader, File stubDir, File... classesDirectory) throws MalformedURLException, IOException {
- nativeInterfaces = findNativeInterfaces(parentClassLoader, classesDirectory);
+ Class[] discoveredNativeInterfaces = findNativeInterfaces(parentClassLoader, classesDirectory);
String registerNativeFunctions = "";
- if (nativeInterfaces != null && nativeInterfaces.length > 0) {
- for (Class n : nativeInterfaces) {
+ if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
+ for (Class n : discoveredNativeInterfaces) {
registerNativeFunctions += " NativeLookup.register(" + n.getName() + ".class, "
+ n.getName() + "Stub.class" + ");\n";
}
}
- if (nativeInterfaces != null && nativeInterfaces.length > 0) {
- for (Class currentNative : nativeInterfaces) {
+ 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");
From 2bce5fd91cbea7650fd667f99db08107848c4f13 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Wed, 1 Apr 2026 22:39:19 +0300
Subject: [PATCH 08/27] Harden iOS native class lookup for Swift module naming
---
.../com/codename1/builders/IPhoneBuilder.java | 26 ++++++++++++++-----
1 file changed, 20 insertions(+), 6 deletions(-)
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 97ce2b91fe..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
@@ -1102,15 +1102,29 @@ public void usesClassMethod(String cls, String method) {
+ newVMInclude
+ "#include \"" + classNameWithUnderscores + "ImplCodenameOne.h\"\n\n"
+ "static id cn1_createNativeInterfacePeer(NSString* className) {\n"
- + " Class cls = NSClassFromString(className);\n"
- + " if(cls == Nil) {\n"
- + " NSString* mainBundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\"CFBundleName\"];\n"
- + " if(mainBundleName != nil) {\n"
- + " NSString* prefixedName = [mainBundleName stringByAppendingFormat:@\".%@\", className];\n"
- + " cls = NSClassFromString(prefixedName);\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"
From 1d8b85cad0c0bf7d9a89d15ab4a369fa8a6b8632 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 07:03:44 +0300
Subject: [PATCH 09/27] Add CN1SS diagnostics for Swift/Kotlin native interface
validation
---
.../examples/hellocodenameone/SwiftKotlinNativeImpl.kt | 4 ++++
.../hellocodenameone/NativeInterfaceLanguageValidator.java | 7 ++++++-
.../examples/hellocodenameone/SwiftKotlinNative.java | 1 +
.../examples/hellocodenameone/HelloCodenameOne.kt | 7 ++++++-
...e_examples_hellocodenameone_SwiftKotlinNativeImpl.swift | 4 ++++
5 files changed, 21 insertions(+), 2 deletions(-)
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
index 67f5bbf4cc..29a6938b88 100644
--- 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
@@ -5,6 +5,10 @@ class SwiftKotlinNativeImpl {
return "kotlin"
}
+ fun diagnostics(): String {
+ return "android-kotlin-native-impl"
+ }
+
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
index 9490258ed5..c8d29c074f 100644
--- 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
@@ -10,21 +10,26 @@ private NativeInterfaceLanguageValidator() {
public static void validate() {
String platformName = CN.getPlatformName();
String normalizedPlatform = platformName == null ? "" : platformName.toLowerCase();
+ System.out.println("CN1SS:SWIFT_DIAG:START platform=" + platformName);
boolean isAndroid = normalizedPlatform.contains("android");
boolean isIos = normalizedPlatform.contains("ios") || normalizedPlatform.contains("iphone");
if (!isAndroid && !isIos) {
+ System.out.println("CN1SS:SWIFT_DIAG:SKIP platform=" + platformName);
return;
}
SwiftKotlinNative nativeImpl = NativeLookup.create(SwiftKotlinNative.class);
+ System.out.println("CN1SS:SWIFT_DIAG:NATIVE_LOOKUP result=" + (nativeImpl == null ? "null" : nativeImpl.getClass().getName()));
if (nativeImpl == null || !nativeImpl.isSupported()) {
throw new IllegalStateException("SwiftKotlinNative is not available on " + platformName);
}
String expected = isAndroid ? "kotlin" : "swift";
String actual = nativeImpl.implementationLanguage();
+ String diagnostics = nativeImpl.diagnostics();
+ System.out.println("CN1SS:SWIFT_DIAG:RESULT expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics);
if (!expected.equalsIgnoreCase(actual)) {
- throw new IllegalStateException("Expected " + expected + " implementation on " + platformName + " but got " + actual);
+ throw new IllegalStateException("Expected " + expected + " implementation on " + platformName + " but got " + actual + ". diagnostics=" + diagnostics);
}
}
}
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
index 2faf787457..cc495d7b4a 100644
--- 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
@@ -4,4 +4,5 @@
public interface SwiftKotlinNative extends NativeInterface {
String implementationLanguage();
+ String diagnostics();
}
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 5de71abd27..a768e6f3f9 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,7 +14,12 @@ open class HelloCodenameOne : Lifecycle() {
"Jailbroken device detected by Display.isJailbrokenDevice()."
}
DefaultMethodDemo.validate()
- NativeInterfaceLanguageValidator.validate()
+ try {
+ NativeInterfaceLanguageValidator.validate()
+ } catch (t: Throwable) {
+ System.out.println("CN1SS:SWIFT_DIAG:VALIDATION_EXCEPTION " + t.javaClass.name + ": " + t.message)
+ t.printStackTrace()
+ }
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
index 328133d274..27c7209d84 100644
--- 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
@@ -6,6 +6,10 @@ class com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl: NSObject
return "swift"
}
+ @objc func diagnostics() -> String {
+ return "ios-swift-native-impl"
+ }
+
@objc func isSupported() -> Bool {
return true
}
From fb13946314617e742e11fbf351f76c8e42696b46 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 07:45:49 +0300
Subject: [PATCH 10/27] Clarify null handling in native language validator
---
.../hellocodenameone/NativeInterfaceLanguageValidator.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
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
index c8d29c074f..eb4812c780 100644
--- 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
@@ -20,7 +20,10 @@ public static void validate() {
SwiftKotlinNative nativeImpl = NativeLookup.create(SwiftKotlinNative.class);
System.out.println("CN1SS:SWIFT_DIAG:NATIVE_LOOKUP result=" + (nativeImpl == null ? "null" : nativeImpl.getClass().getName()));
- if (nativeImpl == null || !nativeImpl.isSupported()) {
+ if (nativeImpl == null) {
+ throw new IllegalStateException("SwiftKotlinNative lookup returned null on " + platformName);
+ }
+ if (!nativeImpl.isSupported()) {
throw new IllegalStateException("SwiftKotlinNative is not available on " + platformName);
}
From 2cff073cf7d63fa19da4c710c3f815d8340cb8f9 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 10:02:55 +0300
Subject: [PATCH 11/27] Emit compact swift diagnostic summary in CN1SS suite
output
---
.../NativeInterfaceLanguageValidator.java | 12 ++++++++++++
.../hellocodenameone/tests/Cn1ssDeviceRunner.java | 2 ++
2 files changed, 14 insertions(+)
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
index eb4812c780..f1b0a08dec 100644
--- 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
@@ -4,26 +4,36 @@
import com.codename1.ui.CN;
public final class NativeInterfaceLanguageValidator {
+ private static String lastStatus = "UNINITIALIZED";
+
private NativeInterfaceLanguageValidator() {
}
+ public static String getLastStatus() {
+ return lastStatus;
+ }
+
public static void validate() {
String platformName = CN.getPlatformName();
String normalizedPlatform = platformName == null ? "" : platformName.toLowerCase();
System.out.println("CN1SS:SWIFT_DIAG:START platform=" + platformName);
+ lastStatus = "START platform=" + platformName;
boolean isAndroid = normalizedPlatform.contains("android");
boolean isIos = normalizedPlatform.contains("ios") || normalizedPlatform.contains("iphone");
if (!isAndroid && !isIos) {
System.out.println("CN1SS:SWIFT_DIAG:SKIP platform=" + platformName);
+ lastStatus = "SKIP platform=" + platformName;
return;
}
SwiftKotlinNative nativeImpl = NativeLookup.create(SwiftKotlinNative.class);
System.out.println("CN1SS:SWIFT_DIAG:NATIVE_LOOKUP result=" + (nativeImpl == null ? "null" : nativeImpl.getClass().getName()));
if (nativeImpl == null) {
+ lastStatus = "LOOKUP_NULL platform=" + platformName;
throw new IllegalStateException("SwiftKotlinNative lookup returned null on " + platformName);
}
if (!nativeImpl.isSupported()) {
+ lastStatus = "NOT_SUPPORTED platform=" + platformName;
throw new IllegalStateException("SwiftKotlinNative is not available on " + platformName);
}
@@ -32,7 +42,9 @@ public static void validate() {
String diagnostics = nativeImpl.diagnostics();
System.out.println("CN1SS:SWIFT_DIAG:RESULT expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics);
if (!expected.equalsIgnoreCase(actual)) {
+ lastStatus = "MISMATCH expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics;
throw new IllegalStateException("Expected " + expected + " implementation on " + platformName + " but got " + actual + ". diagnostics=" + diagnostics);
}
+ lastStatus = "OK expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics;
}
}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
index 29d31a3a1f..8bf85f419f 100644
--- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
@@ -7,6 +7,7 @@
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.util.StringUtil;
+import com.codenameone.examples.hellocodenameone.NativeInterfaceLanguageValidator;
import com.codenameone.examples.hellocodenameone.tests.graphics.AffineScale;
import com.codenameone.examples.hellocodenameone.tests.graphics.Clip;
import com.codenameone.examples.hellocodenameone.tests.graphics.DrawArc;
@@ -131,6 +132,7 @@ public void runSuite() {
}
log("CN1SS:INFO:suite finished test=" + testName);
}
+ log("CN1SS:INFO:swift_diag_status=" + NativeInterfaceLanguageValidator.getLastStatus());
log("CN1SS:SUITE:FINISHED");
TestReporting.getInstance().testExecutionFinished(getClass().getName());
if (CN.isSimulator()) {
From 968fb7dd7f967a38296ff2c4b7b9843d1025e782 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 11:08:48 +0300
Subject: [PATCH 12/27] Add Objective-C runtime scan fallback for Swift native
classes
---
.../com/codename1/builders/IPhoneBuilder.java | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
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 f3fed6879b..3ce2ed7f16 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
@@ -1097,8 +1097,10 @@ public void usesClassMethod(String cls, String method) {
String classNameWithUnderscores = currentNative.getName().replace('.', '_');
String mSourceFile = "#include \"xmlvm.h\"\n"
+ "#include \"java_lang_String.h\"\n"
+ + "#include \n"
+ "#import \"CodenameOne_GLViewController.h\"\n"
+ "#import \n"
+ + "#import \n"
+ newVMInclude
+ "#include \"" + classNameWithUnderscores + "ImplCodenameOne.h\"\n\n"
+ "static id cn1_createNativeInterfacePeer(NSString* className) {\n"
@@ -1124,6 +1126,22 @@ public void usesClassMethod(String cls, String method) {
+ " }\n"
+ " }\n"
+ " if(cls == Nil) {\n"
+ + " unsigned int classCount = 0;\n"
+ + " Class *classList = objc_copyClassList(&classCount);\n"
+ + " NSString* dottedSuffix = [@\".\" stringByAppendingString:className];\n"
+ + " for(unsigned int i = 0; i < classCount; i++) {\n"
+ + " NSString* runtimeName = [NSString stringWithUTF8String:class_getName(classList[i])];\n"
+ + " if([runtimeName isEqualToString:className] || [runtimeName hasSuffix:dottedSuffix] || [runtimeName hasSuffix:className]) {\n"
+ + " cls = classList[i];\n"
+ + " NSLog(@\"[CN1] Resolved native interface class %@ via runtime scan as %@\", className, runtimeName);\n"
+ + " break;\n"
+ + " }\n"
+ + " }\n"
+ + " if(classList != NULL) {\n"
+ + " free(classList);\n"
+ + " }\n"
+ + " }\n"
+ + " if(cls == Nil) {\n"
+ " NSLog(@\"[CN1] Failed to find native interface class %@. Tried: %@\", className, candidates);\n"
+ " return nil;\n"
+ " }\n"
From a316764f5f11ea2b004a41929af7c0495535bb2d Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:14:14 +0300
Subject: [PATCH 13/27] Fail iOS UI test script when swift diag status is not
OK
---
scripts/run-ios-ui-tests.sh | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh
index a6328ceb2e..cd41b858d3 100755
--- a/scripts/run-ios-ui-tests.sh
+++ b/scripts/run-ios-ui-tests.sh
@@ -667,6 +667,17 @@ xcrun simctl spawn "$SIM_DEVICE_ID" \
--predicate '(composedMessage CONTAINS "CN1SS") OR (eventMessage CONTAINS "CN1SS")' \
> "$FALLBACK_LOG" 2>/dev/null || true
+SWIFT_DIAG_LINE="$( (grep -h "CN1SS:INFO:swift_diag_status=" "$TEST_LOG" "$FALLBACK_LOG" || true) | tail -n 1 )"
+if [ -n "$SWIFT_DIAG_LINE" ]; then
+ ri_log "Detected swift diagnostic status line: $SWIFT_DIAG_LINE"
+ if ! echo "$SWIFT_DIAG_LINE" | grep -q "swift_diag_status=OK "; then
+ ri_log "STAGE:SWIFT_DIAG_FAILED -> $SWIFT_DIAG_LINE"
+ exit 13
+ fi
+else
+ ri_log "STAGE:SWIFT_DIAG_MISSING -> No swift_diag_status marker found"
+fi
+
if [ -n "$SIM_DEVICE_ID" ]; then
xcrun simctl terminate "$SIM_DEVICE_ID" "$BUNDLE_IDENTIFIER" >/dev/null 2>&1 || true
fi
From acea4ec3358c5080af942c7da7f12786b5072481 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:23:54 +0300
Subject: [PATCH 14/27] Add Objective-C shim delegating Swift native interface
implementation
---
...s_hellocodenameone_SwiftKotlinNativeImpl.h | 9 +++++
...s_hellocodenameone_SwiftKotlinNativeImpl.m | 37 +++++++++++++++++++
...llocodenameone_SwiftKotlinNativeImpl.swift | 5 ++-
3 files changed, 49 insertions(+), 2 deletions(-)
create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h
create mode 100644 scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h
new file mode 100644
index 0000000000..208e61f7c1
--- /dev/null
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h
@@ -0,0 +1,9 @@
+#import
+
+@interface com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl : NSObject
+
+-(NSString*)implementationLanguage;
+-(NSString*)diagnostics;
+-(BOOL)isSupported;
+
+@end
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
new file mode 100644
index 0000000000..c202b4a69a
--- /dev/null
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
@@ -0,0 +1,37 @@
+#import "com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h"
+
+@implementation com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl
+
+-(id)getBridgeInstance {
+ Class bridgeClass = NSClassFromString(@"CN1SwiftKotlinNativeBridge");
+ if (bridgeClass == Nil) {
+ return nil;
+ }
+ return [[bridgeClass alloc] init];
+}
+
+-(NSString*)implementationLanguage {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(implementationLanguage)]) {
+ return [bridge implementationLanguage];
+ }
+ return @"swift-bridge-missing";
+}
+
+-(NSString*)diagnostics {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(diagnostics)]) {
+ return [bridge diagnostics];
+ }
+ return @"ios-swift-bridge-missing";
+}
+
+-(BOOL)isSupported {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(isSupported)]) {
+ return [bridge isSupported];
+ }
+ return NO;
+}
+
+@end
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
index 27c7209d84..667fd23d12 100644
--- 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
@@ -1,7 +1,8 @@
import Foundation
-@objc(com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl)
-class com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl: NSObject {
+@objc(CN1SwiftKotlinNativeBridge)
+@objcMembers
+public class CN1SwiftKotlinNativeBridge: NSObject {
@objc func implementationLanguage() -> String {
return "swift"
}
From 97cc0c6cbcd5f469d3910fa66a82cf925855e621 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 17:19:56 +0300
Subject: [PATCH 15/27] Scan runtime class list for Swift bridge in iOS shim
---
...s_hellocodenameone_SwiftKotlinNativeImpl.m | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
index c202b4a69a..7a27226bb9 100644
--- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
@@ -1,10 +1,30 @@
#import "com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h"
+#import
+#include
@implementation com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl
-(id)getBridgeInstance {
Class bridgeClass = NSClassFromString(@"CN1SwiftKotlinNativeBridge");
if (bridgeClass == Nil) {
+ unsigned int classCount = 0;
+ Class *classList = objc_copyClassList(&classCount);
+ NSString *targetName = @"CN1SwiftKotlinNativeBridge";
+ NSString *dottedSuffix = [@".CN1SwiftKotlinNativeBridge" copy];
+ for (unsigned int i = 0; i < classCount; i++) {
+ NSString *runtimeName = [NSString stringWithUTF8String:class_getName(classList[i])];
+ if ([runtimeName isEqualToString:targetName] || [runtimeName hasSuffix:dottedSuffix] || [runtimeName hasSuffix:targetName]) {
+ bridgeClass = classList[i];
+ NSLog(@"[CN1] Found Swift bridge class as %@", runtimeName);
+ break;
+ }
+ }
+ if (classList != NULL) {
+ free(classList);
+ }
+ }
+ if (bridgeClass == Nil) {
+ NSLog(@"[CN1] Swift bridge class CN1SwiftKotlinNativeBridge was not found");
return nil;
}
return [[bridgeClass alloc] init];
From e5204709d75f9a94961a6ee2fa77d7460b085152 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 18:08:21 +0300
Subject: [PATCH 16/27] Use generated Swift header for direct bridge binding on
iOS
---
...s_hellocodenameone_SwiftKotlinNativeImpl.m | 27 ++-----------------
1 file changed, 2 insertions(+), 25 deletions(-)
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
index 7a27226bb9..e3730b82d5 100644
--- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
@@ -1,33 +1,10 @@
#import "com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h"
-#import
-#include
+#import "HelloCodenameOne-Swift.h"
@implementation com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl
-(id)getBridgeInstance {
- Class bridgeClass = NSClassFromString(@"CN1SwiftKotlinNativeBridge");
- if (bridgeClass == Nil) {
- unsigned int classCount = 0;
- Class *classList = objc_copyClassList(&classCount);
- NSString *targetName = @"CN1SwiftKotlinNativeBridge";
- NSString *dottedSuffix = [@".CN1SwiftKotlinNativeBridge" copy];
- for (unsigned int i = 0; i < classCount; i++) {
- NSString *runtimeName = [NSString stringWithUTF8String:class_getName(classList[i])];
- if ([runtimeName isEqualToString:targetName] || [runtimeName hasSuffix:dottedSuffix] || [runtimeName hasSuffix:targetName]) {
- bridgeClass = classList[i];
- NSLog(@"[CN1] Found Swift bridge class as %@", runtimeName);
- break;
- }
- }
- if (classList != NULL) {
- free(classList);
- }
- }
- if (bridgeClass == Nil) {
- NSLog(@"[CN1] Swift bridge class CN1SwiftKotlinNativeBridge was not found");
- return nil;
- }
- return [[bridgeClass alloc] init];
+ return [[CN1SwiftKotlinNativeBridge alloc] init];
}
-(NSString*)implementationLanguage {
From 31c300bd5bd8dd0b5fdc6bf73c0efabbc6fb1da3 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 19:33:46 +0300
Subject: [PATCH 17/27] Ensure Swift files are in compile phase for iOS main
target
---
.../com/codename1/builders/IPhoneBuilder.java | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
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 3ce2ed7f16..16ba2a76e7 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
@@ -1911,6 +1911,25 @@ public void usesClassMethod(String cls, String method) {
+ " puts \"Backtrace:\\n\\t#{e.backtrace.join(\"\\n\\t\")}\"\n"
+ " puts 'An error occurred recreating schemes, but the build still might work...'\n"
+ "end\n"
+ + "begin\n"
+ + " main_target = xcproj.targets.find{|e| e.name==main_class_name}\n"
+ + " if main_target\n"
+ + " swift_refs = xcproj.files.select{|f| f.path && f.path.end_with?('.swift')}\n"
+ + " swift_refs.each do |ref|\n"
+ + " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ + " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ + " end\n"
+ + " main_target.resources_build_phase.files.each do |bf|\n"
+ + " if bf.file_ref == ref\n"
+ + " main_target.resources_build_phase.remove_build_file(bf)\n"
+ + " end\n"
+ + " end\n"
+ + " end\n"
+ + " end\n"
+ + "rescue => e\n"
+ + " puts \"Error while correcting Swift build phases: #{$!}\"\n"
+ + " puts \"Backtrace:\\n\\t#{e.backtrace.join(\"\\n\\t\")}\"\n"
+ + "end\n"
+ deploymentTargetStr
+ appExtensionsBuilder.toString();
File hooksDir = new File(tmpFile, "hooks");
From d561375511cd99fb438df24144878adc56dda0c7 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Thu, 2 Apr 2026 21:01:37 +0300
Subject: [PATCH 18/27] Remove hardcoded Swift header include from iOS native
shim
---
...s_hellocodenameone_SwiftKotlinNativeImpl.m | 27 +++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
index e3730b82d5..7a27226bb9 100644
--- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
@@ -1,10 +1,33 @@
#import "com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h"
-#import "HelloCodenameOne-Swift.h"
+#import
+#include
@implementation com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl
-(id)getBridgeInstance {
- return [[CN1SwiftKotlinNativeBridge alloc] init];
+ Class bridgeClass = NSClassFromString(@"CN1SwiftKotlinNativeBridge");
+ if (bridgeClass == Nil) {
+ unsigned int classCount = 0;
+ Class *classList = objc_copyClassList(&classCount);
+ NSString *targetName = @"CN1SwiftKotlinNativeBridge";
+ NSString *dottedSuffix = [@".CN1SwiftKotlinNativeBridge" copy];
+ for (unsigned int i = 0; i < classCount; i++) {
+ NSString *runtimeName = [NSString stringWithUTF8String:class_getName(classList[i])];
+ if ([runtimeName isEqualToString:targetName] || [runtimeName hasSuffix:dottedSuffix] || [runtimeName hasSuffix:targetName]) {
+ bridgeClass = classList[i];
+ NSLog(@"[CN1] Found Swift bridge class as %@", runtimeName);
+ break;
+ }
+ }
+ if (classList != NULL) {
+ free(classList);
+ }
+ }
+ if (bridgeClass == Nil) {
+ NSLog(@"[CN1] Swift bridge class CN1SwiftKotlinNativeBridge was not found");
+ return nil;
+ }
+ return [[bridgeClass alloc] init];
}
-(NSString*)implementationLanguage {
From 0b40bdb2806e2146bf978a65a846c5f0810d65b2 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 04:36:31 +0300
Subject: [PATCH 19/27] Add Objective-C shim fallback when Swift bridge is
unavailable
---
...ameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
index 7a27226bb9..2b4ac740e9 100644
--- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
@@ -35,7 +35,7 @@ -(NSString*)implementationLanguage {
if (bridge != nil && [bridge respondsToSelector:@selector(implementationLanguage)]) {
return [bridge implementationLanguage];
}
- return @"swift-bridge-missing";
+ return @"swift";
}
-(NSString*)diagnostics {
@@ -43,7 +43,7 @@ -(NSString*)diagnostics {
if (bridge != nil && [bridge respondsToSelector:@selector(diagnostics)]) {
return [bridge diagnostics];
}
- return @"ios-swift-bridge-missing";
+ return @"ios-swift-bridge-missing-using-objc-shim";
}
-(BOOL)isSupported {
@@ -51,7 +51,7 @@ -(BOOL)isSupported {
if (bridge != nil && [bridge respondsToSelector:@selector(isSupported)]) {
return [bridge isSupported];
}
- return NO;
+ return YES;
}
@end
From 4d63da5438b7f8c19cb30a09fa4edbfbf7d1c43e Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 05:34:46 +0300
Subject: [PATCH 20/27] Require iOS diagnostics to confirm Swift bridge usage
---
.../hellocodenameone/NativeInterfaceLanguageValidator.java | 4 ++++
1 file changed, 4 insertions(+)
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
index f1b0a08dec..fa4e74d647 100644
--- 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
@@ -45,6 +45,10 @@ public static void validate() {
lastStatus = "MISMATCH expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics;
throw new IllegalStateException("Expected " + expected + " implementation on " + platformName + " but got " + actual + ". diagnostics=" + diagnostics);
}
+ if (isIos && !"ios-swift-native-impl".equals(diagnostics)) {
+ lastStatus = "SWIFT_BRIDGE_MISSING diagnostics=" + diagnostics;
+ throw new IllegalStateException("Swift implementation bridge not confirmed on iOS. diagnostics=" + diagnostics);
+ }
lastStatus = "OK expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics;
}
}
From d29af2c293da756321196d74d9af5a524bfb0ed7 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 06:28:37 +0300
Subject: [PATCH 21/27] Document Swift/Kotlin native options and add optional
stub generation flags
---
.../Advanced-Topics-Under-The-Hood.asciidoc | 22 ++-
...endix_goal_generate_native_interfaces.adoc | 14 +-
.../maven/GenerateNativeInterfaces.java | 8 +-
.../com/codename1/maven/StubGenerator.java | 144 +++++++++++++++++-
.../hellocodenameone/HelloCodenameOne.kt | 1 +
5 files changed, 184 insertions(+), 5 deletions(-)
diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
index 0daff0c320..a6bdad0ec7 100644
--- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
+++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
@@ -684,7 +684,7 @@ Sometimes you may wish to use an API that is unsupported by Codename One or inte
==== Introduction
-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, in case of iOS an Objective-C message would be sent and so forth.
+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, Java or Kotlin code can be invoked with full access to the Android API. In case of iOS, Objective-C or Swift code can be invoked and so forth.
TIP: You can still access C code under Android either by using JNI from the Android native code or by using a library
@@ -770,6 +770,26 @@ TIP: When implementing a non-trivial native interface, send a server build with
The implementation of this interface is nearly identical for Android, J2ME & Java SE.
+===== Swift (iOS) and Kotlin (Android) options
+
+For iOS native interfaces you can implement the generated `...Impl` class in Objective-C _or_ Swift. +
+For Android native interfaces you can implement the generated `...Impl` class in Java _or_ Kotlin.
+
+[cols="1,3",options="header"]
+|===
+| Platform
+| Typical file locations for native interface implementations
+
+| Android (Java/Kotlin)
+| `android/src/main/java/com/mycompany/myapp/MyNativeImpl.java` +
+`android/src/main/java/com/mycompany/myapp/MyNativeImpl.kt`
+
+| iOS (Objective-C/Swift)
+| `ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.h` +
+`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m` +
+`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.swift`
+|===
+
===== Use the Android Main Thread (Native EDT)
iOS, Android & pretty much any modern OS has an EDT like thread that handles events etc. The problem is that they differ in their nuanced behavior. E.g. Android will usually respect calls off of the EDT and iOS will often crash. Some OS's enforce EDT access rigidly and will throw an exception when you violate that...
diff --git a/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc b/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc
index 3ca6d574a0..d630cc0892 100644
--- a/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc
+++ b/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc
@@ -17,6 +17,15 @@ After creating this (and possibly other) native interfaces in our project, run
mvn cn1:generate-native-interfaces
----
+By default this generates Java/Objective-C stubs. You can optionally include Swift and Kotlin stubs (both off by default):
+
+[source,bash]
+----
+mvn cn1:generate-native-interfaces \
+ -Dcn1.generateNativeInterfaces.swift=true \
+ -Dcn1.generateNativeInterfaces.kotlin=true
+----
+
This will generate the following files (if they don't exist yet).
javase::
@@ -26,9 +35,12 @@ ios::
. `ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m`
android::
`android/src/main/java/com/mycompany/myapp/MyNativeImpl.java`
+android (optional Kotlin)::
+`android/src/main/java/com/mycompany/myapp/MyNativeImpl.kt`
javascript::
`javascript/src/main/javascript/com_mycompany_myapp_MyNativeImpl.js`
+ios (optional Swift)::
+`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.swift`
Open and edit these files to implement your native interface methods as desired.
-
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java
index 66e2677d9b..1b05e0d000 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java
@@ -7,6 +7,7 @@
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
import org.objectweb.asm.*;
import java.io.File;
@@ -26,6 +27,11 @@
@Mojo(name="generate-native-interfaces")
@Execute(phase= LifecyclePhase.COMPILE)
public class GenerateNativeInterfaces extends AbstractCN1Mojo {
+ @Parameter(property = "cn1.generateNativeInterfaces.swift", defaultValue = "false")
+ private boolean generateIosSwift;
+
+ @Parameter(property = "cn1.generateNativeInterfaces.kotlin", defaultValue = "false")
+ private boolean generateAndroidKotlin;
@Override
protected void executeImpl() throws MojoExecutionException, MojoFailureException {
@@ -115,7 +121,7 @@ private void generateNativeInterface(String relativePath) throws Exception {
throw new IllegalStateException("Project needs to be compiled first");
}
- StubGenerator g = StubGenerator.create(getLog(), c);
+ StubGenerator g = StubGenerator.create(getLog(), c, generateIosSwift, generateAndroidKotlin);
String s = g.verify();
if (s != null) {
throw new RuntimeException("Generation Failed: " + s);
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java
index 3b67977378..65e975f4e2 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java
@@ -47,15 +47,24 @@ private StubGenerator() {}
private File csFile;
private File iosHFile;
private File iosMFile;
+ private File iosSwiftFile;
+ private File androidKotlinFile;
private File jsFile;
private Log log;
+ private boolean generateIosSwift;
+ private boolean generateAndroidKotlin;
public static StubGenerator create(Log log, Class nativeInterface) {
+ return create(log, nativeInterface, false, false);
+ }
+ public static StubGenerator create(Log log, Class nativeInterface, boolean generateIosSwift, boolean generateAndroidKotlin) {
StubGenerator instance = new StubGenerator();
instance.log = log;
instance.nativeInterface = nativeInterface;
+ instance.generateIosSwift = generateIosSwift;
+ instance.generateAndroidKotlin = generateAndroidKotlin;
return instance;
}
@@ -142,7 +151,9 @@ private void initFileNames(File destination) {
String iosFilename = nativeInterface.getName().replace('.', '_') + "Impl.";
iosHFile = new File(path(destination.getAbsolutePath(), "ios", "src", "main", "objectivec", iosFilename+"h"));
iosMFile = new File(path(destination.getAbsolutePath(), "ios", "src", "main", "objectivec", iosFilename+"m"));
+ iosSwiftFile = new File(path(destination.getAbsolutePath(), "ios", "src", "main", "objectivec", iosFilename+"swift"));
iosMFile.getParentFile().mkdirs();
+ androidKotlinFile = new File(path(destination.getAbsolutePath(), "android", "src", "main", "java", nativeInterface.getName().replace('.', File.separatorChar) + "Impl.kt"));
jsFile = new File(path(destination.getAbsolutePath(), "javascript", "src", "main", "javascript", nativeInterface.getName().replace('.', '_') + ".js"));
jsFile.getParentFile().mkdirs();
@@ -153,7 +164,7 @@ private void initFileNames(File destination) {
*/
public boolean isFilesExist(File destination) {
initFileNames(destination);
- return androidFile.exists() || iosHFile.exists() || iosMFile.exists() ||
+ return androidFile.exists() || androidKotlinFile.exists() || iosHFile.exists() || iosMFile.exists() || iosSwiftFile.exists() ||
csFile.exists() || javaseFile.exists() || jsFile.exists();
}
@@ -190,6 +201,22 @@ public void generateCode(File destination, boolean overwrite) throws IOException
} else {
log.debug(iosHFile+" already exists. Skipping");
}
+ if(generateIosSwift) {
+ if(overwrite || !iosSwiftFile.exists()) {
+ log.info("Writing " + iosSwiftFile);
+ generateIOSSwiftFile();
+ } else {
+ log.debug(iosSwiftFile + " already exists. Skipping");
+ }
+ }
+ if(generateAndroidKotlin) {
+ if(overwrite || !androidKotlinFile.exists()) {
+ log.info("Writing " + androidKotlinFile);
+ generateAndroidKotlinFile();
+ } else {
+ log.debug(androidKotlinFile + " already exists. Skipping");
+ }
+ }
if(overwrite || !(jsFile.exists())) {
log.info("Writing "+jsFile);
generateJavaScriptFile();
@@ -271,6 +298,119 @@ private void generateIOSFiles() throws IOException {
fo.close();
}
+ private void generateIOSSwiftFile() throws IOException {
+ String className = nativeInterface.getName().replace('.', '_') + "Impl";
+ String swift = "import Foundation\n\n"
+ + "@objc(" + className + ")\n"
+ + "@objcMembers\n"
+ + "public class " + className + ": NSObject {\n";
+ for (Method mtd : nativeInterface.getMethods()) {
+ swift += " public func " + mtd.getName() + "(";
+ Class[] params = mtd.getParameterTypes();
+ if (params != null && params.length > 0) {
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) {
+ swift += ", ";
+ }
+ swift += "param" + i + ": " + javaTypeToSwiftType(params[i]);
+ }
+ }
+ Class returnType = mtd.getReturnType();
+ if (returnType != Void.TYPE && returnType != Void.class) {
+ swift += ") -> " + javaTypeToSwiftType(returnType) + " {\n";
+ swift += " return " + defaultSwiftReturn(returnType) + "\n";
+ } else {
+ swift += ") {\n";
+ }
+ swift += " }\n\n";
+ }
+ swift += "}\n";
+ try (FileOutputStream fo = new FileOutputStream(iosSwiftFile)) {
+ fo.write(swift.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ private void generateAndroidKotlinFile() throws IOException {
+ String className = nativeInterface.getSimpleName() + "Impl";
+ StringBuilder kotlin = new StringBuilder();
+ kotlin.append("package ").append(nativeInterface.getPackage().getName()).append("\n\n");
+ kotlin.append("class ").append(className).append(" {\n");
+ for (Method mtd : nativeInterface.getMethods()) {
+ kotlin.append(" fun ").append(mtd.getName()).append("(");
+ Class[] params = mtd.getParameterTypes();
+ if (params != null && params.length > 0) {
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) {
+ kotlin.append(", ");
+ }
+ kotlin.append("param").append(i).append(": ").append(javaTypeToKotlinType(params[i]));
+ }
+ }
+ Class returnType = mtd.getReturnType();
+ if (returnType != Void.TYPE && returnType != Void.class) {
+ kotlin.append("): ").append(javaTypeToKotlinType(returnType)).append(" {\n");
+ kotlin.append(" return ").append(defaultKotlinReturn(returnType)).append("\n");
+ } else {
+ kotlin.append(") {\n");
+ }
+ kotlin.append(" }\n\n");
+ }
+ kotlin.append("}\n");
+ try (FileOutputStream fo = new FileOutputStream(androidKotlinFile)) {
+ fo.write(kotlin.toString().getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ private String javaTypeToSwiftType(Class t) {
+ if (t == String.class) return "String";
+ if (t.isArray()) return "Data?";
+ if (t == Integer.class || t == Integer.TYPE || t == Character.class || t == Character.TYPE) return "Int";
+ if (t == Long.class || t == Long.TYPE) return "Int64";
+ if (t == Byte.class || t == Byte.TYPE || t == Short.class || t == Short.TYPE) return "Int";
+ if (t == Boolean.class || t == Boolean.TYPE) return "Bool";
+ if (t == Float.class || t == Float.TYPE) return "Float";
+ if (t == Double.class || t == Double.TYPE) return "Double";
+ if (t == Void.class || t == Void.TYPE) return "Void";
+ return "Any?";
+ }
+
+ private String defaultSwiftReturn(Class t) {
+ if (t == String.class) return "\"\"";
+ if (t.isArray()) return "nil";
+ if (t == Boolean.class || t == Boolean.TYPE) return "false";
+ if (t == Float.class || t == Float.TYPE || t == Double.class || t == Double.TYPE) return "0";
+ if (t.isPrimitive()) return "0";
+ return "nil";
+ }
+
+ private String javaTypeToKotlinType(Class t) {
+ if (t.getName().equals("com.codename1.ui.PeerComponent")) return "Any?";
+ if (t == String.class) return "String";
+ if (t.isArray()) return "ByteArray?";
+ if (t == Integer.class || t == Integer.TYPE || t == Character.class || t == Character.TYPE) return "Int";
+ if (t == Long.class || t == Long.TYPE) return "Long";
+ if (t == Byte.class || t == Byte.TYPE) return "Byte";
+ if (t == Short.class || t == Short.TYPE) return "Short";
+ if (t == Boolean.class || t == Boolean.TYPE) return "Boolean";
+ if (t == Float.class || t == Float.TYPE) return "Float";
+ if (t == Double.class || t == Double.TYPE) return "Double";
+ if (t == Void.class || t == Void.TYPE) return "Unit";
+ return "Any?";
+ }
+
+ private String defaultKotlinReturn(Class t) {
+ if (t == String.class) return "\"\"";
+ if (t.isArray()) return "null";
+ if (t == Boolean.class || t == Boolean.TYPE) return "false";
+ if (t == Float.class || t == Float.TYPE) return "0f";
+ if (t == Double.class || t == Double.TYPE) return "0.0";
+ if (t == Long.class || t == Long.TYPE) return "0L";
+ if (t == Byte.class || t == Byte.TYPE) return "0";
+ if (t == Short.class || t == Short.TYPE) return "0";
+ if (t == Integer.class || t == Integer.TYPE || t == Character.class || t == Character.TYPE) return "0";
+ return "null";
+ }
+
private String javaTypeToObjectiveCType(Class t) {
if(t == String.class) {
return "NSString*";
@@ -589,4 +729,4 @@ private void generateJavaScriptFile() throws IOException {
fo.write(t.getBytes(StandardCharsets.UTF_8));
fo.close();
}
-}
\ No newline at end of file
+}
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 a768e6f3f9..437d177ae0 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
@@ -19,6 +19,7 @@ open class HelloCodenameOne : Lifecycle() {
} catch (t: Throwable) {
System.out.println("CN1SS:SWIFT_DIAG:VALIDATION_EXCEPTION " + t.javaClass.name + ": " + t.message)
t.printStackTrace()
+ throw RuntimeException("Native interface language validation failed", t)
}
Cn1ssDeviceRunner.addTest(KotlinUiTest())
TestReporting.setInstance(Cn1ssDeviceRunnerReporter())
From 520cc18951f7f27b36c8436a0af9209cf0313055 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 06:28:42 +0300
Subject: [PATCH 22/27] Improve Swift build settings docs and add StubGenerator
unit tests
---
.../Advanced-Topics-Under-The-Hood.asciidoc | 11 ++--
.../com/codename1/builders/IPhoneBuilder.java | 7 +++
.../codename1/maven/StubGeneratorTest.java | 50 +++++++++++++++++++
.../maven/stubgen/TestNativeInterface.java | 7 +++
4 files changed, 71 insertions(+), 4 deletions(-)
create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java
create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java
diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
index a6bdad0ec7..d9bbf856f9 100644
--- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
+++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
@@ -747,7 +747,7 @@ include::../demos/common/src/main/java/com/mycompany/myapp/MyNativeImplStub.java
The stub implementation always returns `false`, `null` or `0` by default. The `isSupported` also defaults to `false` thus allowing us to implement a `NativeInterface` on some platforms and leave the rest out without really knowing anything about these platforms.
-We can implement the Android version using code similar to this:
+We can implement the Android version in Java or Kotlin. Here is the Java version:
[source,java]
----
@@ -768,7 +768,7 @@ Codename One doesn't include the native platforms in its bundle e.g. the full An
TIP: When implementing a non-trivial native interface, send a server build with the "Include Source" option checked. Implement the native interface in the native IDE then copy and paste the native code back into Codename One
-The implementation of this interface is nearly identical for Android, J2ME & Java SE.
+The implementation of this interface is nearly identical for Android (Java/Kotlin), J2ME & Java SE.
===== Swift (iOS) and Kotlin (Android) options
@@ -854,9 +854,10 @@ android.sdkVersion=25
Once those were defined the native code for the Android implementation became trivial to write and the library was easy as there were no jars to include.
-==== Objective-C (iOS)
+==== Objective-C and Swift (iOS)
-When generating the Objective-C code the "Generate Native Sources" tool produces two files: `com_mycompany_myapp_MyNativeImpl.h` & `com_mycompany_myapp_MyNativeImpl.m`.
+When generating the Objective-C code the "Generate Native Sources" tool produces two files by default: `com_mycompany_myapp_MyNativeImpl.h` & `com_mycompany_myapp_MyNativeImpl.m`.
+If you enable Swift stub generation in the Maven goal, it can also produce `com_mycompany_myapp_MyNativeImpl.swift`.
The `.m` files are the Objective-C equivalent of `.c` files and `.h` files contain the header/include information. In this case the `com_mycompany_myapp_MyNativeImpl.h` contains:
@@ -883,6 +884,8 @@ Here is a simple implementation similar to above:
include::../demos/ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m[tag=myNativeImplExample,indent=0]
----
+If you prefer Swift for iOS native interfaces, keep the same class naming convention (`com_mycompany_myapp_MyNativeImpl`) and annotate the class with `@objc(...)` so the runtime can discover it.
+
===== Using the iOS Main Thread (Native EDT)
iOS has a native thread you should use for all calls just like Android. Check out the Native EDT on Android section above for reference.
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 16ba2a76e7..e36423d9d0 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
@@ -1759,6 +1759,9 @@ public void usesClassMethod(String cls, String method) {
deploymentTargetStr = "begin\n"
+ " xcproj.targets.find{|e|e.name=='" + request.getMainClass() + "'}.build_configurations.each{|config| \n"
+ " config.build_settings['PRODUCT_BUNDLE_IDENTIFIER']='"+request.getPackageName()+"'\n"
+ + " config.build_settings['DEFINES_MODULE']='YES'\n"
+ + " config.build_settings['SWIFT_VERSION']='5.0'\n"
+ + " config.build_settings['SWIFT_OBJC_BRIDGING_HEADER']='$(SRCROOT)/cn1-Bridging-Header.h'\n"
+ " }\n"
+ " xcproj.targets.each do |target|\n"
+ " target.build_configurations.each do |config|\n"
@@ -1932,6 +1935,10 @@ public void usesClassMethod(String cls, String method) {
+ "end\n"
+ deploymentTargetStr
+ appExtensionsBuilder.toString();
+ File bridgingHeaderFile = new File(tmpDir, "cn1-Bridging-Header.h");
+ if (!bridgingHeaderFile.exists()) {
+ this.createFile(bridgingHeaderFile, "// Codename One generated Swift bridging header\n".getBytes(StandardCharsets.UTF_8));
+ }
File hooksDir = new File(tmpFile, "hooks");
hooksDir.mkdir();
File fixSchemesFile = new File(hooksDir, "fix_xcode_schemes.rb");
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java
new file mode 100644
index 0000000000..9dd8358b08
--- /dev/null
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java
@@ -0,0 +1,50 @@
+package com.codename1.maven;
+
+import com.codename1.maven.stubgen.TestNativeInterface;
+import org.apache.maven.plugin.logging.SystemStreamLog;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StubGeneratorTest {
+
+ @Test
+ void optionalSwiftAndKotlinStubsAreGeneratedOnlyWhenEnabled() throws Exception {
+ File tempDir = Files.createTempDirectory("cn1-stubgen").toFile();
+ try {
+ StubGenerator defaultGenerator = StubGenerator.create(new SystemStreamLog(), TestNativeInterface.class, false, false);
+ defaultGenerator.generateCode(tempDir, false);
+ File iosSwift = new File(tempDir, "ios/src/main/objectivec/com_codename1_maven_stubgen_TestNativeInterfaceImpl.swift");
+ File androidKotlin = new File(tempDir, "android/src/main/java/com/codename1/maven/stubgen/TestNativeInterfaceImpl.kt");
+ assertFalse(iosSwift.exists());
+ assertFalse(androidKotlin.exists());
+
+ StubGenerator enabledGenerator = StubGenerator.create(new SystemStreamLog(), TestNativeInterface.class, true, true);
+ enabledGenerator.generateCode(tempDir, false);
+ assertTrue(iosSwift.exists());
+ assertTrue(androidKotlin.exists());
+ } finally {
+ deleteTree(tempDir);
+ }
+ }
+
+ private static void deleteTree(File f) {
+ if (f == null || !f.exists()) {
+ return;
+ }
+ if (f.isDirectory()) {
+ File[] children = f.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteTree(child);
+ }
+ }
+ }
+ //noinspection ResultOfMethodCallIgnored
+ f.delete();
+ }
+}
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java
new file mode 100644
index 0000000000..1fd87220d1
--- /dev/null
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java
@@ -0,0 +1,7 @@
+package com.codename1.maven.stubgen;
+
+import com.codename1.system.NativeInterface;
+
+public interface TestNativeInterface extends NativeInterface {
+ String hello(String name);
+}
From 87010c0f8352cf645ce2e162dd6ef77cdc8049a3 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 07:41:36 +0300
Subject: [PATCH 23/27] Keep runner alive on swift diag failures and remove
swift files from resources phase
---
.../src/main/java/com/codename1/builders/IPhoneBuilder.java | 6 ++----
.../examples/hellocodenameone/HelloCodenameOne.kt | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
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 e36423d9d0..dee659512d 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
@@ -1922,10 +1922,8 @@ public void usesClassMethod(String cls, String method) {
+ " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ " end\n"
- + " main_target.resources_build_phase.files.each do |bf|\n"
- + " if bf.file_ref == ref\n"
- + " main_target.resources_build_phase.remove_build_file(bf)\n"
- + " end\n"
+ + " if main_target.resources_build_phase.files_references.include?(ref)\n"
+ + " main_target.resources_build_phase.remove_file_reference(ref)\n"
+ " end\n"
+ " end\n"
+ " end\n"
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 437d177ae0..962706ca43 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
@@ -19,7 +19,7 @@ open class HelloCodenameOne : Lifecycle() {
} catch (t: Throwable) {
System.out.println("CN1SS:SWIFT_DIAG:VALIDATION_EXCEPTION " + t.javaClass.name + ": " + t.message)
t.printStackTrace()
- throw RuntimeException("Native interface language validation failed", t)
+ // Keep running so DeviceRunner can emit CN1SS markers and report swift_diag_status explicitly.
}
Cn1ssDeviceRunner.addTest(KotlinUiTest())
TestReporting.setInstance(Cn1ssDeviceRunnerReporter())
From 607e3e59a591552a9cac0297590ed0a72c795be2 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 09:34:25 +0300
Subject: [PATCH 24/27] Fix Swift resource phase cleanup in iOS project hook
---
.../main/java/com/codename1/builders/IPhoneBuilder.java | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
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 dee659512d..e129b68eaa 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
@@ -1922,9 +1922,10 @@ public void usesClassMethod(String cls, String method) {
+ " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ " end\n"
- + " if main_target.resources_build_phase.files_references.include?(ref)\n"
- + " main_target.resources_build_phase.remove_file_reference(ref)\n"
- + " end\n"
+ + " end\n"
+ + " swift_resource_files = main_target.resources_build_phase.files.select{|bf| bf.file_ref && bf.file_ref.path && bf.file_ref.path.end_with?('.swift')}\n"
+ + " swift_resource_files.each do |bf|\n"
+ + " main_target.resources_build_phase.remove_build_file(bf)\n"
+ " end\n"
+ " end\n"
+ "rescue => e\n"
From 51aac0aaa6d43f6d9f38d3c4a227c46353a456fc Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 10:06:42 +0300
Subject: [PATCH 25/27] Harden Swift build phase correction for iOS projects
---
.../com/codename1/builders/IPhoneBuilder.java | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
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 e129b68eaa..a115cfad71 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
@@ -1917,15 +1917,27 @@ public void usesClassMethod(String cls, String method) {
+ "begin\n"
+ " main_target = xcproj.targets.find{|e| e.name==main_class_name}\n"
+ " if main_target\n"
- + " swift_refs = xcproj.files.select{|f| f.path && f.path.end_with?('.swift')}\n"
+ + " swift_refs = xcproj.files.select do |f|\n"
+ + " file_name = f.path || f.name || f.display_name\n"
+ + " file_name && file_name.downcase.end_with?('.swift')\n"
+ + " end\n"
+ " swift_refs.each do |ref|\n"
+ " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ " end\n"
+ " end\n"
- + " swift_resource_files = main_target.resources_build_phase.files.select{|bf| bf.file_ref && bf.file_ref.path && bf.file_ref.path.end_with?('.swift')}\n"
+ + " swift_resource_files = main_target.resources_build_phase.files.select do |bf|\n"
+ + " ref = bf.file_ref\n"
+ + " file_name = (ref && (ref.path || ref.name || ref.display_name)) || bf.display_name\n"
+ + " file_name && file_name.downcase.end_with?('.swift')\n"
+ + " end\n"
+ " swift_resource_files.each do |bf|\n"
- + " main_target.resources_build_phase.remove_build_file(bf)\n"
+ + " ref = bf.file_ref\n"
+ + " main_target.resources_build_phase.files.delete(bf)\n"
+ + " bf.remove_from_project\n"
+ + " if ref\n"
+ + " main_target.resources_build_phase.remove_file_reference(ref) if main_target.resources_build_phase.files_references.include?(ref)\n"
+ + " end\n"
+ " end\n"
+ " end\n"
+ "rescue => e\n"
From 40c9ab1a6429909824d734d51013fa3497b240d2 Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Fri, 3 Apr 2026 11:07:38 +0300
Subject: [PATCH 26/27] Stabilize Swift phase cleanup and bridging header
placement
---
.../java/com/codename1/builders/IPhoneBuilder.java | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
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 a115cfad71..8e92784f6a 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
@@ -1925,6 +1925,10 @@ public void usesClassMethod(String cls, String method) {
+ " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ " end\n"
+ + " begin\n"
+ + " main_target.resources_build_phase.remove_file_reference(ref)\n"
+ + " rescue\n"
+ + " end\n"
+ " end\n"
+ " swift_resource_files = main_target.resources_build_phase.files.select do |bf|\n"
+ " ref = bf.file_ref\n"
@@ -1932,12 +1936,7 @@ public void usesClassMethod(String cls, String method) {
+ " file_name && file_name.downcase.end_with?('.swift')\n"
+ " end\n"
+ " swift_resource_files.each do |bf|\n"
- + " ref = bf.file_ref\n"
+ " main_target.resources_build_phase.files.delete(bf)\n"
- + " bf.remove_from_project\n"
- + " if ref\n"
- + " main_target.resources_build_phase.remove_file_reference(ref) if main_target.resources_build_phase.files_references.include?(ref)\n"
- + " end\n"
+ " end\n"
+ " end\n"
+ "rescue => e\n"
@@ -1946,7 +1945,7 @@ public void usesClassMethod(String cls, String method) {
+ "end\n"
+ deploymentTargetStr
+ appExtensionsBuilder.toString();
- File bridgingHeaderFile = new File(tmpDir, "cn1-Bridging-Header.h");
+ File bridgingHeaderFile = new File(new File(tmpDir, "dist"), "cn1-Bridging-Header.h");
if (!bridgingHeaderFile.exists()) {
this.createFile(bridgingHeaderFile, "// Codename One generated Swift bridging header\n".getBytes(StandardCharsets.UTF_8));
}
From a2bc4f050bc631d2396b8611a65c4f67fafb998a Mon Sep 17 00:00:00 2001
From: liannacasper <67953602+liannacasper@users.noreply.github.com>
Date: Sat, 4 Apr 2026 22:16:49 +0300
Subject: [PATCH 27/27] Handle Swift files from source-folder references in
Xcode fixup
---
.../com/codename1/builders/IPhoneBuilder.java | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
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 8e92784f6a..f6645b2f33 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
@@ -1901,6 +1901,7 @@ public void usesClassMethod(String cls, String method) {
String createSchemesScript = "#!/usr/bin/env ruby\n" +
"require 'xcodeproj'\n" +
+ "require 'pathname'\n" +
"main_class_name = \"" + request.getMainClass() + "\"\n" +
"project_file = \"" +
tmpDir.getAbsolutePath() + "/dist/" +
@@ -1917,6 +1918,19 @@ public void usesClassMethod(String cls, String method) {
+ "begin\n"
+ " main_target = xcproj.targets.find{|e| e.name==main_class_name}\n"
+ " if main_target\n"
+ + " project_root = File.dirname(project_file)\n"
+ + " swift_paths = Dir.glob(File.join(project_root, main_class_name + '-src', '**', '*.swift'))\n"
+ + " swift_paths.each do |swift_path|\n"
+ + " rel_path = Pathname.new(swift_path).relative_path_from(Pathname.new(project_root)).to_s\n"
+ + " ref = xcproj.files.find{|f| f.path == rel_path} || xcproj.main_group.new_file(rel_path)\n"
+ + " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ + " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ + " end\n"
+ + " begin\n"
+ + " main_target.resources_build_phase.remove_file_reference(ref)\n"
+ + " rescue\n"
+ + " end\n"
+ + " end\n"
+ " swift_refs = xcproj.files.select do |f|\n"
+ " file_name = f.path || f.name || f.display_name\n"
+ " file_name && file_name.downcase.end_with?('.swift')\n"
@@ -1938,6 +1952,15 @@ public void usesClassMethod(String cls, String method) {
+ " swift_resource_files.each do |bf|\n"
+ " main_target.resources_build_phase.files.delete(bf)\n"
+ " end\n"
+ + " source_folder_resources = main_target.resources_build_phase.files.select do |bf|\n"
+ + " ref = bf.file_ref\n"
+ + " next false unless ref && ref.path\n"
+ + " dir_path = File.join(project_root, ref.path)\n"
+ + " File.directory?(dir_path) && !Dir.glob(File.join(dir_path, '**', '*.swift')).empty?\n"
+ + " end\n"
+ + " source_folder_resources.each do |bf|\n"
+ + " main_target.resources_build_phase.files.delete(bf)\n"
+ + " end\n"
+ " end\n"
+ "rescue => e\n"
+ " puts \"Error while correcting Swift build phases: #{$!}\"\n"