Skip to content

Conversation

@tarrinneal
Copy link
Contributor

No description provided.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant new feature to Pigeon: support for generating native interop code using JNI on Android and FFI on iOS/macOS. This is a large and complex change that touches many parts of the code generation pipeline, from the AST to the individual language generators. The changes include new options to enable this functionality, new helper classes for type mapping and conversion, and updates to the generated code to use native interop where available, falling back to method channels. New test files and configurations have also been added to support this feature.

My review has found a few critical issues that should be addressed. The JNI implementation appears to be incomplete, with several key parts commented out. There are also some copy-paste errors from the JNI implementation into the FFI code that will cause it to fail. Additionally, the FFI configuration generator produces files with hardcoded absolute paths, which will break builds for other developers. I've also noted some areas for improvement in error handling and code correctness. Overall, this is a promising direction for Pigeon, but the implementation needs some fixes and completion before it's ready.

Comment on lines +1058 to +1072
void _writeToJni(Indent indent, Class classDefinition) {
// indent.writeScoped('jni_bridge.${classDefinition.name} toJni() {', '}', () {
// indent.writeScoped('return jni_bridge.${classDefinition.name} (', ');',
// () {
// for (final NamedType field in getFieldsInSerializationOrder(
// classDefinition,
// )) {
// final _JniType jniType = _JniType.fromTypeDeclaration(field.type);
// indent.writeln(
// '${field.name}: ${jniType.getToJniCall(field.type, field.name, jniType, forceNonNull: true)},',
// );
// }
// });
// });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The JNI implementation for custom classes appears to be a work-in-progress. This method (_writeToJni) and _writeFromJni are commented out, as are the class-handling sections within _PigeonJniCodec and the JNI path in _writeNativeInteropHostApi. If useJni is intended to be a user-facing feature in this PR, this functionality should be completed. If it's still a work-in-progress, it might be better to gate the feature more explicitly or add documentation about its current limitations.

Comment on lines +1477 to +1485
indent.addScoped('{', '}', () {
indent.format('''
dartApi = api;
final ffi_bridge.${api.name} impl =
ffi_bridge.${api.name}.implement(this);
ffi_bridge.${api.name}Registrar()
.registerInstance(impl, JString.fromString(name));
return api;
''');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This block in _writeFfiFlutterApi seems to be an incorrect copy from the JNI implementation. It uses jni_bridge and JString which are for JNI, not FFI. The FFI implementation should be using FFI-specific constructs, likely involving NSString and ffi_bridge types for registration.

Comment on lines +97 to +106
inputs: <SwiftGenInput>[ObjCCompatibleSwiftFileInput(files: <Uri>[
Uri.file('${generatorOptions.swiftOptions.swiftOut}')
])
],
include: (Declaration d) =>
classes.contains(d.name) || enums.contains(d.name),
output: Output(
module: '${generatorOptions.swiftOptions.ffiModuleName ?? ''}',
dartFile: Uri.file('${path.posix.join(generatorOptions.basePath ?? '', path.withoutExtension(generatorOptions.dartOut ?? ''))}.ffi.dart'),
objectiveCFile: Uri.file('${path.posix.join(generatorOptions.basePath ?? '', path.withoutExtension(generatorOptions.dartOut ?? ''))}.m'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The generated ffigen_config.dart file will contain absolute paths because Uri.file() is used with paths that are likely relative to the project root, which resolves them to absolute paths. This will break the build for other users or in different environments. The paths should be generated as relative paths, likely relative to the location of the generated ffigen_config.dart file itself. The JnigenYamlGenerator uses path.relative, which could be a good model to follow here.

Comment on lines +1696 to +1714
static $dartApiName? getInstance({String channelName = defaultInstanceName}) {
late $dartApiName res;
if (Platform.isAndroid) {
final $jniApiRegistrarName? link =
$jniApiRegistrarName().getInstance(JString.fromString(channelName));
if (link == null) {
_throwNoInstanceError(channelName);
}
res = $dartApiName._withRegistrar(jniApi: link);
} else if (Platform.isIOS || Platform.isMacOS) {
final $ffiApiRegistrarName? link =
$ffiApiRegistrarName.getInstanceWithName(NSString(channelName));
if (link == null) {
_throwNoInstanceError(channelName);
}
res = $dartApiName._withRegistrar(ffiApi: link);
}
return res;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The res variable in the generated getInstance method is declared as late but is only initialized for Android, iOS, and macOS. If this code is run on an unsupported platform, return res; at the end of the function will throw a LateInitializationError. It would be safer to throw a PlatformException for unsupported platforms to provide a clearer error, or ensure all paths initialize res or return.

Suggested change
static $dartApiName? getInstance({String channelName = defaultInstanceName}) {
late $dartApiName res;
if (Platform.isAndroid) {
final $jniApiRegistrarName? link =
$jniApiRegistrarName().getInstance(JString.fromString(channelName));
if (link == null) {
_throwNoInstanceError(channelName);
}
res = $dartApiName._withRegistrar(jniApi: link);
} else if (Platform.isIOS || Platform.isMacOS) {
final $ffiApiRegistrarName? link =
$ffiApiRegistrarName.getInstanceWithName(NSString(channelName));
if (link == null) {
_throwNoInstanceError(channelName);
}
res = $dartApiName._withRegistrar(ffiApi: link);
}
return res;
}
static $dartApiName? getInstance({String channelName = defaultInstanceName}) {
if (Platform.isAndroid) {
final $jniApiRegistrarName? link =
$jniApiRegistrarName().getInstance(JString.fromString(channelName));
if (link == null) {
_throwNoInstanceError(channelName);
}
return $dartApiName._withRegistrar(jniApi: link);
} else if (Platform.isIOS || Platform.isMacOS) {
final $ffiApiRegistrarName? link =
$ffiApiRegistrarName.getInstanceWithName(NSString(channelName));
if (link == null) {
_throwNoInstanceError(channelName);
}
return $dartApiName._withRegistrar(ffiApi: link);
}
return null;
}

Comment on lines +572 to 592
final Set<String> classesToCheck = Set<String>.from(referencedTypeNames);
while (classesToCheck.isNotEmpty) {
final String next = classesToCheck.removeLast();
final String next = classesToCheck.last;
final Class aClass = classes.firstWhere(
(Class x) => x.name == next,
orElse: () => Class(name: '', fields: <NamedType>[]),
);
for (final NamedType field in aClass.fields) {
references.add(field.type, field.offset);
if (_isUnseenCustomType(field.type, referencedTypeNames)) {
references.add(field.type, field.offset);
classesToCheck.add(field.type.baseName);
}
for (final TypeDeclaration typeArg in field.type.typeArguments) {
references.add(typeArg, field.offset);
if (_isUnseenCustomType(typeArg, referencedTypeNames)) {
references.add(typeArg, field.offset);
classesToCheck.add(typeArg.baseName);
}
}
}
classesToCheck.remove(next);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using a Set for classesToCheck is a good improvement for efficiency over a List. However, the item is removed at the end of the loop. If a class refers to itself, this will result in an infinite loop. It would be safer to remove next from classesToCheck at the beginning of the loop.

  final Set<String> classesToCheck = Set<String>.from(referencedTypeNames);
  while (classesToCheck.isNotEmpty) {
    final String next = classesToCheck.first;
    classesToCheck.remove(next);
    final Class aClass = classes.firstWhere(
      (Class x) => x.name == next,
      orElse: () => Class(name: '', fields: <NamedType>[]),
    );
    for (final NamedType field in aClass.fields) {
      references.add(field.type, field.offset);
      if (_isUnseenCustomType(field.type, referencedTypeNames)) {
        classesToCheck.add(field.type.baseName);
      }
      for (final TypeDeclaration typeArg in field.type.typeArguments) {
        references.add(typeArg, field.offset);
        if (_isUnseenCustomType(typeArg, referencedTypeNames)) {
          classesToCheck.add(typeArg.baseName);
        }
      }
    }
  }

if (type.isEnum) {
return 'NSNumber';
}
return 'There is something wrong, a type is not classified';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning a string literal for an unhandled type, such as 'There is something wrong, a type is not classified', will lead to generated code that doesn't compile and provides a poor developer experience. It would be better to throw a PigeonError with more context (like the type name and location) to help the user identify the problem in their pigeon file. This applies to _FfiType.ffiName, _JniType.jniName, and other similar places.

}
if (nativeInteropApi == null) {
throw ArgumentError(
'No ${api.name} instance with \${nativeInteropApiInstanceName.isEmpty ? 'no ' : ''} instance name \${nativeInteropApiInstanceName.isNotEmpty ? '"\$nativeInteropApiInstanceName"' : ''} "\$nativeInteropApiInstanceName "}found.');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the generated error message: a stray double quote at the end of the interpolated string.

Suggested change
'No ${api.name} instance with \${nativeInteropApiInstanceName.isEmpty ? 'no ' : ''} instance name \${nativeInteropApiInstanceName.isNotEmpty ? '"\$nativeInteropApiInstanceName"' : ''} "\$nativeInteropApiInstanceName "}found.');
'No ${api.name} instance with ${nativeInteropApiInstanceName.isEmpty ? 'no ' : ''}instance name ${nativeInteropApiInstanceName.isNotEmpty ? '"$nativeInteropApiInstanceName"' : ''}found.');

@@ -0,0 +1,1520 @@
// Copyright 2013 The Flutter Authors. All rights reserved.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file appears to be a duplicate of ni_tests.dart. It should probably be removed to avoid confusion and reduce maintenance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant