Skip to content

Commit b47e206

Browse files
authored
Add Swift and Kotlin native interface support (#4684)
This is part of the work on #3274 With this change native interfaces can be written in Swift for iOS and in Kotlin for Android. This is only on the local version and isn't yet supported in the build servers.
1 parent 64b9c78 commit b47e206

20 files changed

Lines changed: 794 additions & 26 deletions

File tree

CodenameOne/src/com/codename1/system/package-info.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/// [support for making platform native API calls](https://www.codenameone.com/how-do-i---access-native-device-functionality-invoke-native-interfaces.html). Notice
33
/// that when we say "native" we do not mean C/C++ always but rather the platforms "native" environment. So in the
44
/// case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C
5-
/// message would be sent and so forth.
5+
/// or Swift message would be sent and so forth.
66
///
77
/// Native interfaces are designed to only allow primitive types, Strings, arrays (single dimension only!) of primitives
88
/// and PeerComponent values. Any other type of parameter/return type is prohibited. However, once in the native layer
@@ -59,9 +59,9 @@
5959
/// These sources should be placed under the appropriate folder in the native directory and are sent to the
6060
/// server for compilation.
6161
///
62-
/// For Objective-C, one would need to define a class matching the name of the package and the class name
63-
/// combined where the "." elements are replaced by underscores. One would need to provide both a header and
64-
/// an "m" file following this convention e.g.:
62+
/// For iOS, one would need to define a class matching the name of the package and the class name
63+
/// combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C
64+
/// (by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.:
6565
///
6666
/// ```java
6767
/// @interface com_my_code_MyNative : NSObject {

CodenameOne/src/com/codename1/system/package.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<a href="https://www.codenameone.com/how-do-i---access-native-device-functionality-invoke-native-interfaces.html">
99
support for making platform native API calls</a>. Notice
1010
that when we say "native" we do not mean C/C++ always but rather the platforms "native" environment. So in the
11-
case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C
12-
message would be sent and so forth.
11+
case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C
12+
or Swift message would be sent and so forth.
1313
</p>
1414
<p>
1515
Native interfaces are designed to only allow primitive types, Strings, arrays (single dimension only!) of primitives
@@ -66,9 +66,9 @@
6666
server for compilation.
6767
</p>
6868
<p>
69-
For Objective-C, one would need to define a class matching the name of the package and the class name
70-
combined where the "." elements are replaced by underscores. One would need to provide both a header and
71-
an "m" file following this convention e.g.:
69+
For iOS, one would need to define a class matching the name of the package and the class name
70+
combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C
71+
(by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.:
7272
</p>
7373
<pre>
7474
@interface com_my_code_MyNative : NSObject {

docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,7 @@ Sometimes you may wish to use an API that is unsupported by Codename One or inte
684684

685685
==== Introduction
686686

687-
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.
687+
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.
688688

689689
TIP: You can still access C code under Android either by using JNI from the Android native code or by using a library
690690

@@ -747,7 +747,7 @@ include::../demos/common/src/main/java/com/mycompany/myapp/MyNativeImplStub.java
747747

748748
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.
749749

750-
We can implement the Android version using code similar to this:
750+
We can implement the Android version in Java or Kotlin. Here is the Java version:
751751

752752
[source,java]
753753
----
@@ -768,7 +768,27 @@ Codename One doesn't include the native platforms in its bundle e.g. the full An
768768

769769
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
770770

771-
The implementation of this interface is nearly identical for Android, J2ME & Java SE.
771+
The implementation of this interface is nearly identical for Android (Java/Kotlin), J2ME & Java SE.
772+
773+
===== Swift (iOS) and Kotlin (Android) options
774+
775+
For iOS native interfaces you can implement the generated `...Impl` class in Objective-C _or_ Swift. +
776+
For Android native interfaces you can implement the generated `...Impl` class in Java _or_ Kotlin.
777+
778+
[cols="1,3",options="header"]
779+
|===
780+
| Platform
781+
| Typical file locations for native interface implementations
782+
783+
| Android (Java/Kotlin)
784+
| `android/src/main/java/com/mycompany/myapp/MyNativeImpl.java` +
785+
`android/src/main/java/com/mycompany/myapp/MyNativeImpl.kt`
786+
787+
| iOS (Objective-C/Swift)
788+
| `ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.h` +
789+
`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m` +
790+
`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.swift`
791+
|===
772792

773793
===== Use the Android Main Thread (Native EDT)
774794

@@ -834,9 +854,10 @@ android.sdkVersion=25
834854
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.
835855

836856

837-
==== Objective-C (iOS)
857+
==== Objective-C and Swift (iOS)
838858

839-
When generating the Objective-C code the "Generate Native Sources" tool produces two files: `com_mycompany_myapp_MyNativeImpl.h` & `com_mycompany_myapp_MyNativeImpl.m`.
859+
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`.
860+
If you enable Swift stub generation in the Maven goal, it can also produce `com_mycompany_myapp_MyNativeImpl.swift`.
840861

841862
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:
842863

@@ -863,6 +884,8 @@ Here is a simple implementation similar to above:
863884
include::../demos/ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m[tag=myNativeImplExample,indent=0]
864885
----
865886

887+
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.
888+
866889
===== Using the iOS Main Thread (Native EDT)
867890

868891
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.

docs/developer-guide/appendix_goal_generate_native_interfaces.adoc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ After creating this (and possibly other) native interfaces in our project, run
1717
mvn cn1:generate-native-interfaces
1818
----
1919
20+
By default this generates Java/Objective-C stubs. You can optionally include Swift and Kotlin stubs (both off by default):
21+
22+
[source,bash]
23+
----
24+
mvn cn1:generate-native-interfaces \
25+
-Dcn1.generateNativeInterfaces.swift=true \
26+
-Dcn1.generateNativeInterfaces.kotlin=true
27+
----
28+
2029
This will generate the following files (if they don't exist yet).
2130
2231
javase::
@@ -26,9 +35,12 @@ ios::
2635
. `ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m`
2736
android::
2837
`android/src/main/java/com/mycompany/myapp/MyNativeImpl.java`
38+
android (optional Kotlin)::
39+
`android/src/main/java/com/mycompany/myapp/MyNativeImpl.kt`
2940
javascript::
3041
`javascript/src/main/javascript/com_mycompany_myapp_MyNativeImpl.js`
42+
ios (optional Swift)::
43+
`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.swift`
3144
3245
Open and edit these files to implement your native interface methods as desired.
3346
34-

maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
import java.awt.image.ImageProducer;
4242
import java.awt.image.RGBImageFilter;
4343
import java.io.*;
44+
import java.lang.reflect.Method;
4445

46+
import java.net.MalformedURLException;
4547
import java.net.URL;
4648
import java.net.URLClassLoader;
4749
import java.nio.channels.FileChannel;
@@ -3972,6 +3974,117 @@ static String xmlize(String s) {
39723974
}
39733975

39743976

3977+
@Override
3978+
protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentClassLoader, File stubDir, File... classesDirectory) throws MalformedURLException, IOException {
3979+
Class[] discoveredNativeInterfaces = findNativeInterfaces(parentClassLoader, classesDirectory);
3980+
String registerNativeFunctions = "";
3981+
if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
3982+
for (Class n : discoveredNativeInterfaces) {
3983+
registerNativeFunctions += " NativeLookup.register(" + n.getName() + ".class, "
3984+
+ n.getName() + "Stub.class" + ");\n";
3985+
}
3986+
}
3987+
3988+
if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
3989+
for (Class currentNative : discoveredNativeInterfaces) {
3990+
File folder = new File(stubDir, currentNative.getPackage().getName().replace('.', File.separatorChar));
3991+
folder.mkdirs();
3992+
File javaFile = new File(folder, currentNative.getSimpleName() + "Stub.java");
3993+
3994+
String javaImplSourceFile = "package " + currentNative.getPackage().getName() + ";\n\n"
3995+
+ "import com.codename1.ui.PeerComponent;\n\n"
3996+
+ "public class " + currentNative.getSimpleName() + "Stub implements " + currentNative.getSimpleName() + "{\n"
3997+
+ " private final Object impl = createImpl();\n\n"
3998+
+ " private static Object createImpl() {\n"
3999+
+ " try {\n"
4000+
+ " return Class.forName(\"" + currentNative.getName() + getImplSuffix() + "\").newInstance();\n"
4001+
+ " } catch (Throwable t) {\n"
4002+
+ " throw new RuntimeException(\"Failed to instantiate native implementation for " + currentNative.getName() + "\", t);\n"
4003+
+ " }\n"
4004+
+ " }\n\n"
4005+
+ " private Object __cn1Invoke(String methodName, Object[] args) {\n"
4006+
+ " try {\n"
4007+
+ " java.lang.reflect.Method[] methods = impl.getClass().getMethods();\n"
4008+
+ " for (java.lang.reflect.Method method : methods) {\n"
4009+
+ " if (method.getName().equals(methodName) && method.getParameterTypes().length == args.length) {\n"
4010+
+ " return method.invoke(impl, args);\n"
4011+
+ " }\n"
4012+
+ " }\n"
4013+
+ " throw new RuntimeException(methodName + \" with \" + args.length + \" args\");\n"
4014+
+ " } catch (Throwable t) {\n"
4015+
+ " throw new RuntimeException(\"Failed to invoke native method \" + methodName, t);\n"
4016+
+ " }\n"
4017+
+ " }\n\n";
4018+
4019+
for (Method m : currentNative.getMethods()) {
4020+
String name = m.getName();
4021+
if (name.equals("hashCode") || name.equals("equals") || name.equals("toString")) {
4022+
continue;
4023+
}
4024+
4025+
Class returnType = m.getReturnType();
4026+
4027+
javaImplSourceFile += " public " + returnType.getSimpleName() + " " + name + "(";
4028+
Class[] params = m.getParameterTypes();
4029+
String args = "";
4030+
if (params != null && params.length > 0) {
4031+
for (int iter = 0; iter < params.length; iter++) {
4032+
if (iter > 0) {
4033+
javaImplSourceFile += ", ";
4034+
args += ", ";
4035+
}
4036+
javaImplSourceFile += params[iter].getSimpleName() + " param" + iter;
4037+
if (params[iter].getName().equals("com.codename1.ui.PeerComponent")) {
4038+
args += convertPeerComponentToNative("param" + iter);
4039+
} else {
4040+
args += "param" + iter;
4041+
}
4042+
}
4043+
}
4044+
javaImplSourceFile += ") {\n";
4045+
String invocationExpression = "__cn1Invoke(\"" + name + "\", new Object[]{" + args + "})";
4046+
if (Void.class == returnType || Void.TYPE == returnType) {
4047+
javaImplSourceFile += " " + invocationExpression + ";\n }\n\n";
4048+
} else {
4049+
if (returnType.getName().equals("com.codename1.ui.PeerComponent")) {
4050+
javaImplSourceFile += " return " + generatePeerComponentCreationCode(invocationExpression) + ";\n }\n\n";
4051+
} else if (returnType.isPrimitive()) {
4052+
if (returnType == Boolean.TYPE) {
4053+
javaImplSourceFile += " return ((Boolean)" + invocationExpression + ").booleanValue();\n }\n\n";
4054+
} else if (returnType == Integer.TYPE) {
4055+
javaImplSourceFile += " return ((Integer)" + invocationExpression + ").intValue();\n }\n\n";
4056+
} else if (returnType == Long.TYPE) {
4057+
javaImplSourceFile += " return ((Long)" + invocationExpression + ").longValue();\n }\n\n";
4058+
} else if (returnType == Byte.TYPE) {
4059+
javaImplSourceFile += " return ((Byte)" + invocationExpression + ").byteValue();\n }\n\n";
4060+
} else if (returnType == Short.TYPE) {
4061+
javaImplSourceFile += " return ((Short)" + invocationExpression + ").shortValue();\n }\n\n";
4062+
} else if (returnType == Character.TYPE) {
4063+
javaImplSourceFile += " return ((Character)" + invocationExpression + ").charValue();\n }\n\n";
4064+
} else if (returnType == Float.TYPE) {
4065+
javaImplSourceFile += " return ((Float)" + invocationExpression + ").floatValue();\n }\n\n";
4066+
} else if (returnType == Double.TYPE) {
4067+
javaImplSourceFile += " return ((Double)" + invocationExpression + ").doubleValue();\n }\n\n";
4068+
} else {
4069+
javaImplSourceFile += " return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n }\n\n";
4070+
}
4071+
} else {
4072+
javaImplSourceFile += " return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n }\n\n";
4073+
}
4074+
}
4075+
}
4076+
4077+
javaImplSourceFile += "}\n";
4078+
4079+
try (FileOutputStream out = new FileOutputStream(javaFile)) {
4080+
out.write(javaImplSourceFile.getBytes(StandardCharsets.UTF_8));
4081+
}
4082+
}
4083+
}
4084+
4085+
return registerNativeFunctions;
4086+
}
4087+
39754088
@Override
39764089
protected String generatePeerComponentCreationCode(String methodCallString) {
39774090
return "PeerComponent.create(" + methodCallString + ")";

0 commit comments

Comments
 (0)