Skip to content

[ffigen] Add protocol methods to interfaces #1567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 16, 2024
Merged

[ffigen] Add protocol methods to interfaces #1567

merged 2 commits into from
Sep 16, 2024

Conversation

liamappelbe
Copy link
Contributor

If an interface implements a protocol, but doesn't declare or implement the protocol's methods in the same file, then they won't appear as methods directly on the interface in the AST. This is actually the typical case in most APIs, where the implementation is in a separate file to the interface. This means we don't codegen those methods, so they aren't invocable from Dart, even though they do have implementations and are invocable from ObjC.

The fix is just to parse all the protocols that the interface implements, and copy their methods to the interface.

The only wrinkle is that protocols can have optional methods, which the interface might not implement. If we generate the same Dart bindings for optional methods as we do for required methods, then invoking these methods may cause a crash at runtime with no useful stack trace. There's a special message we can send to the object to check if it has an implementation for a method: respondsToSelector:. So we just have to check if the object responds to the selector, and throw an exception if it doesn't.

Fixes #1487

Copy link

PR Health

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

Coverage ⚠️
File Coverage
pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart 💔 Not covered
pkgs/ffigen/lib/src/code_generator/objc_interface.dart 💔 Not covered
pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart 💔 Not covered
pkgs/objective_c/lib/src/internal.dart 💔 Not covered
pkgs/objective_c/lib/src/objective_c_bindings_generated.dart 💔 Not covered

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

License Headers ✔️
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/ffigen/example/libclang-example/generated_bindings.dart
pkgs/ffigen/example/shared_bindings/generate.dart
pkgs/ffigen/example/shared_bindings/lib/generated/a_gen.dart
pkgs/ffigen/example/shared_bindings/lib/generated/a_shared_b_gen.dart
pkgs/ffigen/example/shared_bindings/lib/generated/base_gen.dart
pkgs/ffigen/example/simple/generated_bindings.dart
pkgs/ffigen/lib/src/header_parser/clang_bindings/clang_bindings.dart
pkgs/ffigen/test/collision_tests/expected_bindings/_expected_decl_decl_collision_bindings.dart
pkgs/ffigen/test/collision_tests/expected_bindings/_expected_decl_symbol_address_collision_bindings.dart
pkgs/ffigen/test/collision_tests/expected_bindings/_expected_decl_type_name_collision_bindings.dart
pkgs/ffigen/test/collision_tests/expected_bindings/_expected_reserved_keyword_collision_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_comment_markup_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_dart_handle_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_enum_int_mimic_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_forward_decl_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_functions_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_imported_types_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_native_func_typedef_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_opaque_dependencies_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_packed_structs_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_regress_384_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_struct_fptr_fields_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_typedef_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_unions_bindings.dart
pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_varargs_bindings.dart
pkgs/ffigen/test/large_integration_tests/_expected_cjson_bindings.dart
pkgs/ffigen/test/large_integration_tests/_expected_libclang_bindings.dart
pkgs/ffigen/test/large_integration_tests/_expected_sqlite_bindings.dart
pkgs/ffigen/test/native_test/_expected_native_test_bindings.dart
pkgs/jni/lib/src/lang/jcharacter.dart
pkgs/jni/lib/src/third_party/generated_bindings.dart
pkgs/jni/lib/src/third_party/global_env_extensions.dart
pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
pkgs/jnigen/android_test_runner/lib/main.dart
pkgs/jnigen/example/in_app_java/lib/android_utils.dart
pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart
pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart
pkgs/jnigen/example/kotlin_plugin/lib/kotlin_plugin.dart
pkgs/jnigen/example/pdfbox_plugin/lib/pdfbox_plugin.dart
pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart
pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart
pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/_package.dart
pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart
pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/_package.dart
pkgs/jnigen/lib/src/bindings/descriptor.dart
pkgs/jnigen/lib/src/elements/elements.g.dart
pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/_package.dart
pkgs/jnigen/tool/command_runner.dart
pkgs/native_assets_builder/test_data/native_dynamic_linking/bin/native_dynamic_linking.dart
pkgs/swift2objc/lib/src/config.dart
pkgs/swift2objc/lib/src/generate_wrapper.dart
pkgs/swift2objc/lib/src/generator/_core/utils.dart
pkgs/swift2objc/lib/src/generator/generator.dart
pkgs/swift2objc/lib/src/generator/generators/class_generator.dart
pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_initializer_declaration.dart
pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_property_declaration.dart
pkgs/swift2objc/lib/src/transformer/_core/unique_namer.dart
pkgs/swift2objc/lib/src/transformer/_core/utils.dart
pkgs/swift2objc/lib/src/transformer/transformers/transform_property.dart
Package publish validation ✔️
Package Version Status
package:ffi 2.1.3 already published at pub.dev
package:ffigen 15.0.0-wip WIP (no publish necessary)
package:jni 0.11.0 already published at pub.dev
package:jnigen 0.12.0-wip WIP (no publish necessary)
package:native_assets_cli 0.8.1-wip WIP (no publish necessary)
package:objective_c 2.1.0-wip WIP (no publish necessary)
package:swift2objc 0.0.1-wip WIP (no publish necessary)
package:swiftgen 0.0.1-wip WIP (no publish necessary)

Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.

@coveralls
Copy link

coveralls commented Sep 16, 2024

Coverage Status

coverage: 91.483% (+0.6%) from 90.901%
when pulling d5fd845 on protomethod
into c216889 on main.

Copy link
Member

@HosseinYousefi HosseinYousefi left a comment

Choose a reason for hiding this comment

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

LGTM!

@liamappelbe liamappelbe merged commit e45b10e into main Sep 16, 2024
20 checks passed
@liamappelbe liamappelbe deleted the protomethod branch September 16, 2024 23:47
@stuartmorgan-g
Copy link

If we generate the same Dart bindings for optional methods as we do for required methods, then invoking these methods may cause a crash at runtime with no useful stack trace. There's a special message we can send to the object to check if it has an implementation for a method: respondsToSelector:. So we just have to check if the object responds to the selector, and throw an exception if it doesn't.

Out of curiosity, why is this built into the generated code? An Obj-C developer has to do that check themselves, and if they don't and call it anyway it will crash. It seems odd to have a different pattern in Dart, especially since anyone familiar with Obj-C is likely going to assume they need to check, resulting in everything being checked twice (once by the developer, and once by the generated code).

@liamappelbe
Copy link
Contributor Author

Out of curiosity, why is this built into the generated code? An Obj-C developer has to do that check themselves, and if they don't and call it anyway it will crash. It seems odd to have a different pattern in Dart, especially since anyone familiar with Obj-C is likely going to assume they need to check, resulting in everything being checked twice (once by the developer, and once by the generated code).

It's not really a different pattern imo. In ObjC if you call an unimplemented method it does crash, but you at least get a stack trace:

** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ObjCProtocolImplMissingMethod optionalMethod:]: unrecognized selector sent to instance 0x600003b98440'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007ff812230dc6 __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007ff811d209dd objc_exception_throw + 48
	2   CoreFoundation                      0x00007ff8122d6434 -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007ff81219e129 ___forwarding___ + 745
	4   CoreFoundation                      0x00007ff81219ddb8 _CF_forwarding_prep_0 + 120
	5   protocol_test.dylib                 0x00000001129937bc -[ProtocolConsumer callOptionalMethod:] + 76
	6   ???                                 0x000000011290713b 0x0 + 4606423355    // ^- Useful
)

Without the check in the Dart code, you still get the crash, but the stack trace doesn't include the Dart frames, so it's essentially useless:

** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ObjCProtocolImplMissingMethod optionalMethod:]: unrecognized selector sent to instance 0x6000002b7830'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007ff812230dc6 __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007ff811d209dd objc_exception_throw + 48
	2   CoreFoundation                      0x00007ff8122d6434 -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007ff81219e129 ___forwarding___ + 745
	4   CoreFoundation                      0x00007ff81219ddb8 _CF_forwarding_prep_0 + 120
	5   ???                                 0x000000010b20713b 0x0 + 4481642811    // ^- Useless
)

To emulate this stack trace behavior in Dart, I have to check respondsToSelector, but that's an implementation detail. I could change it in future to catch the ObjC exception and rethrow it as a Dart exception. If devs want to use the same pattern as they do in ObjC, where they check respondsToSelector before making the call, that's fine (tho I probably need to expose SEL more in package:objective_c to facilitate this).

@stuartmorgan-g
Copy link

but the stack trace doesn't include the Dart frames

Ah, right. Thanks for the explanation!

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.

Protocol methods missing from classes
4 participants