Skip to content

Instantly share code, notes, and snippets.

@alloy
Last active May 12, 2018 21:49
Show Gist options
  • Save alloy/8452461 to your computer and use it in GitHub Desktop.
Save alloy/8452461 to your computer and use it in GitHub Desktop.

How to wrap Objective-C APIs that take arbitrary block types for RubyMotion.

In this example –that focusses on YapDatabase– we wrap a Objective-C method that takes C-blocks of arbitrary types:

- (id)initWithSetup:(YapDatabaseSecondaryIndexSetup *)setup
              block:(YapDatabaseSecondaryIndexBlock)block
          blockType:(YapDatabaseSecondaryIndexBlockType)blockType;

The block type is normally specified at runtime with by the blockType parameter, which is one of the following (specifically the bottom YapDatabaseSecondaryIndexBlockType list):

typedef id YapDatabaseSecondaryIndexBlock;

typedef void (^YapDatabaseSecondaryIndexWithKeyBlock)      \
                            (NSMutableDictionary *dict,
                            NSString *collection,
                            NSString *key);
typedef void (^YapDatabaseSecondaryIndexWithObjectBlock)   \
                            (NSMutableDictionary *dict,
                            NSString *collection,
                            NSString *key,
                            id object);
typedef void (^YapDatabaseSecondaryIndexWithMetadataBlock) \
                            (NSMutableDictionary *dict,
                            NSString *collection,
                            NSString *key,
                            id metadata);
typedef void (^YapDatabaseSecondaryIndexWithRowBlock)      \
                            (NSMutableDictionary *dict,
                            NSString *collection,
                            NSString *key,
                            id object,
                            id metadata);

typedef enum {
	YapDatabaseSecondaryIndexBlockTypeWithKey       = 1031,
	YapDatabaseSecondaryIndexBlockTypeWithObject    = 1032,
	YapDatabaseSecondaryIndexBlockTypeWithMetadata  = 1033,
	YapDatabaseSecondaryIndexBlockTypeWithRow       = 1034
} YapDatabaseSecondaryIndexBlockType;

While arbitrary block types are a great way in C/Objective-C to make one method take blocks with varying signatures, they are problematic for RubyMotion’s compiler because it can’t know which type of block will be used until at runtime. Using this API as-is will lead to a crash, because the Ruby block has not been properly compiled with the expected signature.

What the RubyMotion compiler expects you to pass for the block parameter is a plain object, as indicated by the YapDatabaseSecondaryIndexBlock which is an alias for id, the type that means ‘object of any class’. While a Ruby block is indeed an object, it needs extra metadata about how many arguments to expect, of which type, and the return value type.

To remedy this, we hint the compiler about which type is required by defining a Objective-C ‘category’ (which extends an existing class) and create a method for the specific block type that it takes (in this case YapDatabaseSecondaryIndexWithObjectBlock). This way, when calling this wrapper method, the compiler will exactly know how to compile the Ruby block and sanity is restored.

app.vendor_project('YapDatabaseRubyMotion', :static, :bridgesupport_cflags => '-I../vendor/Pods/Headers -fobjc-arc', :cflags => '-I../vendor/Pods/Headers -fobjc-arc')
#import <YapDatabase/YapDatabaseSecondaryIndex.h>
@interface YapDatabaseSecondaryIndex (RubyMotionBlockTypeWrapper)
// Here we define the prototype of our wrapper method that explicitly
// states what the type signature of the block argument will be.
//
// This will give the RubyMotion compiler enough information to ‘do
// the right thing’.
- (id)initWithSetup:(YapDatabaseSecondaryIndexSetup *)setup
objectBlock:(YapDatabaseSecondaryIndexWithObjectBlock)block;
@end
#import "YapDatabaseRubyMotion.h"
#if ! __has_feature(objc_arc)
#error This file must be compiled with ARC. Use the `-fobjc-arc` flag.
#endif
@implementation YapDatabaseSecondaryIndex (RubyMotionBlockTypeWrapper)
// Here we define the implementation that does nothing else than forward
// the method call to the normal library’s API. You could say we are
// ‘aliasing’ the method (although we do change the interface).
- (id)initWithSetup:(YapDatabaseSecondaryIndexSetup *)setup
objectBlock:(YapDatabaseSecondaryIndexWithObjectBlock)block;
{
return [self initWithSetup:setup
block:block
blockType:YapDatabaseSecondaryIndexBlockTypeWithObject];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment