Skip to content
This repository has been archived by the owner on Jan 28, 2024. It is now read-only.

Separate getDartType and getUserType #623

Merged
merged 5 commits into from
Sep 24, 2023
Merged

Separate getDartType and getUserType #623

merged 5 commits into from
Sep 24, 2023

Conversation

liamappelbe
Copy link
Contributor

@liamappelbe liamappelbe commented Sep 21, 2023

A bunch of ObjC polish bugs are related to the fact that we don't draw a distinction between the 2 different uses of Type.getDartType:

  1. The Dart type used in FFI function signatures and structs. The type that Type.getCType maps to.
  2. The user facing type we present in the generated API.

For C bindings these 2 types are always the same. But for ObjC types they can be different. For example, an NSString* has a Dart type of Pointer<ObjCObject> and a user type of the Dart wrapper class NSString. I've been distinguishing them in objc_interface.dart with some hard coded special cases for class methods, but this misses top level functions, ObjC blocks, struct fields, and typedefs.

More info: dart-lang/native#235

So we need to separate these 2 uses by defining a new Type.getUserType method. Over time I'll migrate the rest of ffigen to make this distinction (ie find everywhere Type.getDartType is called and switch the ones that are user facing to Type.getUserType), and that will fix all these bugs. But it'll be complicated because we have to do conversions between the Dart type and the user type. So to keep things simple I've just migrated objc_interface.dart for now.

To facilitate this I also had to define new specializations of some Types:

  • ObjCObject* was an ordinary Pointer but is now ObjCObjectPointer
  • instancetype was a Typealias but is now ObjCInstanceType

Related bugs: dart-lang/native#308 dart-lang/native#235 dart-lang/native#473 dart-lang/native#496

@liamappelbe liamappelbe changed the title WIP: Separate getDartType and getUserType Separate getDartType and getUserType Sep 21, 2023
@liamappelbe liamappelbe marked this pull request as ready for review September 21, 2023 04:55
Copy link
Contributor

@dcharkes dcharkes left a comment

Choose a reason for hiding this comment

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

https://github.com/dart-lang/ffigen/issues/386#issuecomment-1728584216

Nice write up!

Reading your write-up, I think we should make a general solution for where the user-visible-dart-type is not equal to the internally-used-with-ffi-dart-type.

Should we have a separate pass parse -> transform -> generate where in the transform step we recognize unwrapped types and wrap them? The current PR special cases the Pointer Type constructor, but that seems not the right place for the decision point.

cc @mannprerak2 What do you think about this?

That migration should be incremental to keep the PR size reasonable.

I'd prefer not having the repo in a half-migrated state. I'm happy with incremental PRs, but then please commit to doing the follow up PRs soon. (I want to avoid having a half-migrated repo and then contributors (including ourselves) having to deal with that technical debt.) Having a full migration PR is preferable imo.

lib/src/code_generator/type.dart Outdated Show resolved Hide resolved

PointerType._(this.child);

factory PointerType(Type child) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we maybe have something more general to recognize "internal-dart-types" and return the "user-visible-dart-types"?

For example, if we have something more general we could also recognize Pointer<Char> and make it available as String in signatures?

It should probably be a separate pass. Rather than making a choice point in factory constructors of types.

  // Parse the bindings according to config object provided.
  final library = parse(config);

  final libraryTransformed = transform(library);

  // Generate file for the parsed bindings.
  final gen = File(config.output);
  libraryTransformed.generateFile(gen);

Or maybe because we already make choices in parse based on the config, it should a Type wrapDartType(Type type) function being invoked on any type. And for now it only replaces Pointer with ObjCObjectPointer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment we can handle all these cases, including your Pointer<Char> idea, using this factory pattern. The type I'm returning is still a Pointer, it's just a more specialized version of it. That's one of the main use cases of a factory constructor, so I think it's appropriate to make the decision here. I don't see the point of adding a entire transformer infrastructure if we don't actually need it. We can add something like that later if we find a case that isn't cleanly handled by this pattern.

lib/src/code_generator/typealias.dart Show resolved Hide resolved
if (_isObject(type)) return 'NSObject';
if (_isInstanceType(type)) return enclosingClass;
return type.getDartType(w);
if (type is ObjCInstanceType) return enclosingClass;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice cleanup!

This special case is the instancetype right? The one that returns the type of the object itself even in subclasses.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep

/// Objective C's instancetype.
///
/// This is an alias for an NSObject* that is special cased in code generation.
class ObjCInstanceType extends Typealias {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to populate the enclosing class here? So that the code generator doesn't make the decision but we do it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so. Not with the existing plumbing.

@liamappelbe
Copy link
Contributor Author

I'd prefer not having the repo in a half-migrated state.

Well, the migration isn't really incremental. This PR fully migrates all existing bits of the repo that made this distinction (only objc_interface.dart). The incremental part is applying that distinction to the rest of the code gen. Each time I change one of those other modules to distinguish getFfiDartType and getDartType, I'll be fixing one of these ObjC polish bugs. So I think it makes sense to do them as follow up PRs. But I plan to do them immediately after this one.

Copy link
Contributor

@dcharkes dcharkes left a comment

Choose a reason for hiding this comment

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

lgtm!

I think we should consider refactoring this so that it becomes more of a pipeline with individual steps being responsible for individual aspects. Now it starts to be a spaghetti with lots of responsibilities both in the parse and in the generate step. However, we should not gate this CL on that.

@dcharkes
Copy link
Contributor

Thanks @liamappelbe ! 🚀

@liamappelbe liamappelbe merged commit 6746b58 into main Sep 24, 2023
6 checks passed
@liamappelbe liamappelbe deleted the usertype branch September 24, 2023 23:33
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants