Skip to content

Commit

Permalink
Generate null-safe Kotlin code (#1758)
Browse files Browse the repository at this point in the history
Now we parse the Kotlin metadata and add a `Nullable` or `NonNull` annotation to each type based on that. Not everything is covered because we simply don't support all Kotlin features.
  • Loading branch information
HosseinYousefi authored Nov 29, 2024
1 parent 058675e commit 25f36d6
Show file tree
Hide file tree
Showing 11 changed files with 2,194 additions and 225 deletions.
2 changes: 1 addition & 1 deletion pkgs/jnigen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## 0.13.0-wip

- **Breaking Change**([#1644](https://github.com/dart-lang/native/issues/1644)):
Generate null-safe Dart bindings.
Generate null-safe Dart bindings for Java and Kotlin.

## 0.12.2

Expand Down
4 changes: 2 additions & 2 deletions pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ class _MyHomePageState extends State<MyHomePage> {
ElevatedButton(
onPressed: () {
setState(() {
answer = example.thinkBeforeAnswering().then((value) =>
value?.toDartString(releaseOriginal: true) ?? 'null');
answer = example.thinkBeforeAnswering().then(
(value) => value.toDartString(releaseOriginal: true));
});
},
child: const Text('Think...'),
Expand Down
12 changes: 6 additions & 6 deletions pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,25 @@ class Example extends _$jni.JObject {

/// from: `public final java.lang.Object thinkBeforeAnswering(kotlin.coroutines.Continuation continuation)`
/// The returned object must be released after use, by calling the [release] method.
_$core.Future<_$jni.JString?> thinkBeforeAnswering() async {
_$core.Future<_$jni.JString> thinkBeforeAnswering() async {
final $p = _$jni.ReceivePort();
final _$continuation = _$jni.ProtectedJniExtensions.newPortContinuation($p);

_thinkBeforeAnswering(
reference.pointer,
_id_thinkBeforeAnswering as _$jni.JMethodIDPtr,
_$continuation.pointer)
.object<_$jni.JObject?>(const _$jni.JObjectNullableType());
.object<_$jni.JObject>(const _$jni.JObjectType());
_$continuation.release();
final $o =
_$jni.JGlobalReference(_$jni.JObjectPtr.fromAddress(await $p.first));
final $k = const _$jni.JStringNullableType().jClass.reference;
final $k = const _$jni.JStringType().jClass.reference;
if (!_$jni.Jni.env.IsInstanceOf($o.pointer, $k.pointer)) {
$k.release();
throw 'Failed';
}
$k.release();
return const _$jni.JStringNullableType().fromReference($o);
return const _$jni.JStringType().fromReference($o);
}
}

Expand All @@ -133,7 +133,7 @@ final class $Example$NullableType extends _$jni.JObjType<Example?> {
);
@_$jni.internal
@_$core.override
_$jni.JObjType get superType => const _$jni.JObjectNullableType();
_$jni.JObjType get superType => const _$jni.JObjectType();

@_$jni.internal
@_$core.override
Expand Down Expand Up @@ -168,7 +168,7 @@ final class $Example$Type extends _$jni.JObjType<Example> {
);
@_$jni.internal
@_$core.override
_$jni.JObjType get superType => const _$jni.JObjectNullableType();
_$jni.JObjType get superType => const _$jni.JObjectType();

@_$jni.internal
@_$core.override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

public class KotlinPackage {
public List<KotlinFunction> functions;
public List<KotlinProperty> properties;

public static KotlinPackage fromKmPackage(KmPackage p) {
var pkg = new KotlinPackage();
pkg.functions =
p.getFunctions().stream().map(KotlinFunction::fromKmFunction).collect(Collectors.toList());
pkg.properties =
p.getProperties().stream().map(KotlinProperty::fromKmProperty).collect(Collectors.toList());
return pkg;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.util.List;
import java.util.stream.Collectors;
import kotlinx.metadata.Flag;
import kotlinx.metadata.KmClassifier;
import kotlinx.metadata.KmType;

Expand All @@ -15,11 +16,14 @@ public class KotlinType {
public String name;
public int id;
public List<KotlinTypeProjection> arguments;
public boolean isNullable;

public static KotlinType fromKmType(KmType t) {
if (t == null) return null;
var type = new KotlinType();
type.flags = t.getFlags();
// Processing the information needed from the flags.
type.isNullable = Flag.Type.IS_NULLABLE.invoke(type.flags);
var classifier = t.getClassifier();
if (classifier instanceof KmClassifier.Class) {
type.kind = "class";
Expand Down
225 changes: 223 additions & 2 deletions pkgs/jnigen/lib/src/bindings/kotlin_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@
import '../elements/elements.dart';
import 'visitor.dart';

String _toJavaBinaryName(String kotlinBinaryName) {
final binaryName =
kotlinBinaryName.replaceAll('.', r'$').replaceAll('/', '.');
return const {
'kotlin.Any': 'java.lang.Object',
'kotlin.Byte': 'java.lang.Byte',
'kotlin.Short': 'java.lang.Short',
'kotlin.Int': 'java.lang.Integer',
'kotlin.Long': 'java.lang.Long',
'kotlin.Char': 'java.lang.Character',
'kotlin.Float': 'java.lang.Float',
'kotlin.Double': 'java.lang.Double',
'kotlin.Boolean': 'java.lang.Boolean',
'kotlin.Cloneable': 'java.lang.Cloneable',
'kotlin.Comparable': 'java.lang.Comparable',
'kotlin.Enum': 'java.lang.Enum',
'kotlin.Annotation': 'java.lang.annotation.Annotation',
'kotlin.CharSequence': 'java.lang.CharSequence',
'kotlin.String': 'java.lang.String',
'kotlin.Number': 'java.lang.Number',
'kotlin.Throwable': 'java.lang.Throwable',
}[binaryName] ??
binaryName;
}

/// A [Visitor] that adds the the information from Kotlin's metadata to the Java
/// classes and methods.
class KotlinProcessor extends Visitor<Classes, void> {
Expand All @@ -24,6 +49,46 @@ class _KotlinClassProcessor extends Visitor<ClassDecl, void> {
return;
}
// This [ClassDecl] is actually a Kotlin class.
if (node.kotlinClass != null) {
for (var i = 0; i < node.kotlinClass!.typeParameters.length; ++i) {
node.typeParams[i].accept(
_KotlinTypeParamProcessor(node.kotlinClass!.typeParameters[i]));
}
if (node.superclass case final superClass?) {
final kotlinSuperTypes = node.kotlinClass!.superTypes.where(
(superType) =>
_toJavaBinaryName(superType.name ?? '') == superClass.name,
);
if (kotlinSuperTypes.isNotEmpty) {
superClass.accept(_KotlinTypeProcessor(kotlinSuperTypes.single));
}
}
}

// Matching fields and properties from the metadata.
final properties = <String, KotlinProperty>{};
final getters = <String, KotlinProperty>{};
final setters = <String, KotlinProperty>{};
final kotlinProperties =
(node.kotlinClass?.properties ?? node.kotlinPackage?.properties)!;
for (final property in kotlinProperties) {
if (property.fieldName case final fieldName?) {
properties[fieldName] = property;
}
if (property.getterName case final getterName?) {
final getterSignature = getterName + property.getterDescriptor!;
getters[getterSignature] = property;
}
if (property.setterName case final setterName?) {
final setterSignature = setterName + property.setterDescriptor!;
setters[setterSignature] = property;
}
}
for (final field in node.fields) {
if (properties[field.name] case final property?) {
field.accept(_KotlinPropertyProcessor(property));
}
}
// Matching methods and functions from the metadata.
final functions = <String, KotlinFunction>{};
final kotlinFunctions =
Expand All @@ -32,22 +97,49 @@ class _KotlinClassProcessor extends Visitor<ClassDecl, void> {
final signature = function.name + function.descriptor;
functions[signature] = function;
}
final constructors = <String, KotlinConstructor>{};
final kotlinConstructors = node.kotlinClass?.constructors ?? [];
for (final constructor in kotlinConstructors) {
final signature = constructor.name + constructor.descriptor;
constructors[signature] = constructor;
}
for (final method in node.methods) {
final signature = method.name + method.descriptor!;
if (functions.containsKey(signature)) {
method.accept(_KotlinMethodProcessor(functions[signature]!));
if (functions[signature] case final function?) {
method.accept(_KotlinMethodProcessor(function));
} else if (constructors[signature] case final constructor?) {
method.accept(_KotlinConstructorProcessor(constructor));
} else if (getters[signature] case final getter?) {
method.accept(_KotlinGetterProcessor(getter));
} else if (setters[signature] case final setter?) {
method.accept(_KotlinSetterProcessor(setter));
}
}
}
}

void _processParams(
List<Param> params, List<KotlinValueParameter> kotlinParams) {
if (params.length != kotlinParams.length) {
return;
}
for (var i = 0; i < params.length; ++i) {
params[i].accept(_KotlinParamProcessor(kotlinParams[i]));
}
}

class _KotlinMethodProcessor extends Visitor<Method, void> {
final KotlinFunction function;

_KotlinMethodProcessor(this.function);

@override
void visit(Method node) {
_processParams(node.params, function.valueParameters);
for (var i = 0; i < node.typeParams.length; ++i) {
node.typeParams[i]
.accept(_KotlinTypeParamProcessor(function.typeParameters[i]));
}
if (function.isSuspend) {
const kotlinContinutationType = 'kotlin.coroutines.Continuation';
assert(node.params.isNotEmpty &&
Expand All @@ -63,6 +155,135 @@ class _KotlinMethodProcessor extends Visitor<Method, void> {
node.asyncReturnType = continuationType == null
? TypeUsage.object
: continuationType.clone();
node.asyncReturnType!.accept(_KotlinTypeProcessor(function.returnType));

// The continuation object is always non-null.
node.returnType.type.annotations ??= [];
node.returnType.type.annotations!.add(Annotation.nonNull);
} else {
node.returnType.accept(_KotlinTypeProcessor(function.returnType));
}
}
}

class _KotlinConstructorProcessor extends Visitor<Method, void> {
final KotlinConstructor constructor;

_KotlinConstructorProcessor(this.constructor);

@override
void visit(Method node) {
_processParams(node.params, constructor.valueParameters);
}
}

class _KotlinGetterProcessor extends Visitor<Method, void> {
final KotlinProperty getter;

_KotlinGetterProcessor(this.getter);

@override
void visit(Method node) {
node.returnType.accept(_KotlinTypeProcessor(getter.returnType));
}
}

class _KotlinSetterProcessor extends Visitor<Method, void> {
final KotlinProperty setter;

_KotlinSetterProcessor(this.setter);

@override
void visit(Method node) {
if (setter.setterParameter case final setterParam?) {
node.params.single.type.accept(_KotlinTypeProcessor(setterParam.type));
}
node.params.single.type.accept(_KotlinTypeProcessor(setter.returnType));
}
}

class _KotlinPropertyProcessor extends Visitor<Field, void> {
final KotlinProperty property;

_KotlinPropertyProcessor(this.property);

@override
void visit(Field node) {
node.type.accept(_KotlinTypeProcessor(property.returnType));
}
}

class _KotlinParamProcessor extends Visitor<Param, void> {
final KotlinValueParameter kotlinParam;

_KotlinParamProcessor(this.kotlinParam);

@override
void visit(Param node) {
node.type.accept(_KotlinTypeProcessor(kotlinParam.type));
}
}

class _KotlinTypeParamProcessor extends Visitor<TypeParam, void> {
final KotlinTypeParameter kotlinTypeParam;

_KotlinTypeParamProcessor(this.kotlinTypeParam);

@override
void visit(TypeParam node) {
final kotlinBounds = kotlinTypeParam.upperBounds;
final bounds = <String, KotlinType>{};
for (final bound in kotlinBounds) {
if (bound.name case final boundName?) {
bounds[_toJavaBinaryName(boundName)] = bound;
}
}
for (final bound in node.bounds) {
if (bounds[bound.name] case final kotlinBound?) {
bound.accept(_KotlinTypeProcessor(kotlinBound));
}
}
}
}

class _KotlinTypeProcessor extends TypeVisitor<void> {
final KotlinType kotlinType;

_KotlinTypeProcessor(this.kotlinType);

@override
void visitDeclaredType(DeclaredType node) {
for (var i = 0; i < node.params.length; ++i) {
node.params[i].accept(_KotlinTypeProcessor(kotlinType.arguments[i].type));
}
super.visitDeclaredType(node);
}

@override
void visitArrayType(ArrayType node) {
if (kotlinType.arguments.isNotEmpty) {
node.elementType
.accept(_KotlinTypeProcessor(kotlinType.arguments.first.type));
}
super.visitArrayType(node);
}

@override
void visitWildcard(Wildcard node) {
node.extendsBound?.accept(_KotlinTypeProcessor(kotlinType));
node.superBound?.accept(_KotlinTypeProcessor(kotlinType));
super.visitWildcard(node);
}

@override
void visitNonPrimitiveType(ReferredType node) {
node.annotations ??= [];
node.annotations!
.add(kotlinType.isNullable ? Annotation.nullable : Annotation.nonNull);
}

@override
void visitPrimitiveType(PrimitiveType node) {
// Do nothing.
}
}
Loading

0 comments on commit 25f36d6

Please sign in to comment.