Skip to content

Instantly share code, notes, and snippets.

@mraleph
Created May 21, 2024 12:51
Show Gist options
  • Save mraleph/5971f1904d474526066dc3c792c9436e to your computer and use it in GitHub Desktop.
Save mraleph/5971f1904d474526066dc3c792c9436e to your computer and use it in GitHub Desktop.

Dart and Objective-C interoperability

Imagine you have the following Objective-C code:

#import <Foundation/Foundation.h>

@interface Animal : NSObject {
    NSString* name;
}

- (void)makeSound;

- (Animal*)initWithName:(NSString*) name;

+ (void)petAnAnimal:(Animal*) animal;

@end

@implementation Animal

- (void)makeSound {
}


+ (void)petAnAnimal:(Animal*) animal {
    [animal makeSound];
}


- (Animal*)initWithName:(NSString*) n {
    name = n;
    return self;
}

@end

@interface Dog : Animal {
    NSString* breed;
}

- (Dog*)initWithName:(NSString*) n andBreed:(NSString*) b;
@end

@implementation Dog

- (Dog*)initWithName:(NSString*) n andBreed:(NSString*) b {
    self = [self initWithName: n];
    breed = b;
    return self;
}


- (void)makeSound {
    NSLog(@"The Dog %@ (%@) has barked\n", name, breed);
}

@end

Can you interoperate with this from Dart? Yep, you easily can. You take ffigen and you generate bindings for it, which allow you to write code like this:

 final dog = Dog.alloc()
   .initWithName_andBreed_(
     objc.NSString('Boxer'),
     objc.NSString('pug'),
   );

 dog.makeSound();
 Animal.petAnAnimal_(dog);

This prints

2024-05-21 14:09:23.663 hello_world[26434:23013753] The Dog Boxer (pug) has barked
2024-05-21 14:09:23.663 hello_world[26434:23013753] The Dog Boxer (pug) has barked

Now the question is: can you extend Animal and make things still work?

Yes, you can - though right now you would have to do it manually. Consider for example that you want to write the following:

class Squirrel extends Animal {
  final String color;
  
  Squirrel({required String color, required String name});
  
  void makeSound() {
    print('Squirrel $name of color $color is making some squirrel sounds!');
  }
}

How would you go about it? Well, you have to do something like this:

class Squirrel extends Animal {

  static ffi.Pointer<objc.ObjCObject> _registerClass() {
    final cls = objc.allocateClassPair(class_Animal, "Squirrel",
        extraBytes: ffi.sizeOf<ffi.Pointer>());
    objc.addInstanceVariable(
      cls,
      "color",
      ffi.sizeOf<ffi.Pointer>(),
      ffi.sizeOf<ffi.Pointer>() == 4 ? 2 : 3,
      "^v",
    );
    _nameVar = objc.class_getInstanceVariable(cls, "name");
    _colorVar = objc.class_getInstanceVariable(cls, "color");

    final makeSoundMethod =
        objc.class_getInstanceMethod(class_Animal, sel_makeSound);
    final types = objc.method_getTypeEncoding(makeSoundMethod);
    objc.class_addMethod(
        cls,
        sel_makeSound,
        ffi.NativeCallable<
                ffi.Void Function(
                  ffi.Pointer<objc.ObjCObject>,
                  ffi.Pointer<objc.ObjCSelector>,
                )>.isolateLocal(_makeSoundTrampoline)
            .nativeFunction
            .cast(),
        types);
    return cls;
  }

  // Objective-C will invoke this method which will then invoke
  // [makeSound] below.
  static void _makeSoundTrampoline(
    ffi.Pointer<objc.ObjCObject> self,
    ffi.Pointer<objc.ObjCSelector> selector,
  ) {
    Squirrel._(self).makeSound();
  }

  void makeSound() {
    print(
        'Squirrel $name of color $color is making some squirrel sounds!');
  }

  factory Squirrel({required String name, required String color}) {
    final _ret = objc_msgSend(_class_Squirrel, sel_alloc);    
    final result = Squirrel._(_ret, retain: false, release: true);
    result.initWithName_(objc.NSString(name));
    result._color = color;
    return result;
  }

  ffi.Pointer<ffi.Pointer<ffi.Void>> get _colorSlot =>
      ffi.Pointer<ffi.Pointer<ffi.Void>>.fromAddress(
          pointer.address + _dartDataOffset);

  String get _color => objc.unwrapPersistentHandle(_colorSlot.value);
  set _color(_SquirrelData data) {
    _colorSlot.value = objc.createPersistentHandle(data);
  }

  objc.NSString get name =>
      objc.NSString.castFromPointer(objc.object_getIvar(pointer, _nameVar));
  String get color => _color;
}

You can then use this code like so:

  final squirrel = Squirrel(name: 'Xyz', color: 'red');
  print('Asking animal to make sound (from Dart)');
  squirrel.makeSound();

  print('Indirectly asking animal to make sound (via Objective-C)');
  Animal.petAnAnimal_(squirrel);

And this will work as expected and print:

flutter: Asking animal to make sound (from Dart)
flutter: Squirrel Xyz of color red is making some squirrel sounds!
flutter: Indirectly asking animal to make sound (via Objective-C)
flutter: Squirrel Xyz of color red is making some squirrel sounds!

Is it too manual? Yep. But most of this is boilerplate that can be just generated e.g. via macros.

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