Slide 1

Slide 1 text

React Native EU 2022 How to access all the Objective‑C APIs using JSI by Jamie Birch

Slide 2

Slide 2 text

Contents • About me • Background: Accessing native APIs is a pain • How to use JSI • Accessing a serialisable API from Obj-C • Accessing arbitrary non-serialisable APIs from Obj-C • Conclusion

Slide 3

Slide 3 text

Who’s talking? Jamie Birch, all-platform dev
 🌐 Software Engineer at Birchill Maker of LinguaBrowse NativeScript TSC Enemy of native code

Slide 4

Slide 4 text

Accessing native APIs is a pain • It’s boilerplate city • May involve installing (and possibly forking) an npm module • Requires rebuilding your app • Requires switching context

Slide 5

Slide 5 text

How could it be better?

Slide 6

Slide 6 text

We could call all native APIs directly from JS! (C#, but same concept) (by Phonegap) (by DiDi) DynamicCocoa (by bang590) JSPatch function callPhoneNumber(phoneNumber: string): void { const url: NSURL = NSURL.URLWithString(`tel://${phoneNumber}`); if(!url || !UIApplication.sharedApplication.canOpenURL(url)){ throw new Error('Phone number invalid, or permissions missing.'); } UIApplication.sharedApplication.openURLOptionsCompletionHandler( url, NSDictionary.alloc().init(), (success: boolean) => console.log(success), ); }

Slide 7

Slide 7 text

Why should we care?

Slide 8

Slide 8 text

How should we approach this?

Slide 9

Slide 9 text

Sounds like a job for JSI. 🚦 Synchronous 🏎 Fast 🔢 Supports native data types

Slide 10

Slide 10 text

Let’s set up an app that uses JSI.

Slide 11

Slide 11 text

App setup We’ll create a standard React Native project from the TypeScript template. 1. Create a fresh project: npx react-native init rnobjc --template
 react-native-template-typescript 2. Open the Xcode workspace: open rnobjc/ios/rnobjc.xcworkspace 3. Run the target scheme, rnobjc. … If this Hello World app fails to run, you’ll need to fi x your environment! Part 1: Hello World

Slide 12

Slide 12 text

App setup Part 2: Native module fi le setup Alongside your AppDelegate fi les, let’s create a folder for our native module, starting with a couple of empty fi les.

Slide 13

Slide 13 text

App setup Part 3: Adding the fi les to your Xcode project

Slide 14

Slide 14 text

JSI module setup Part 1/7: ObjcRuntime.h - the interface #import @interface ObjcRuntime : NSObject @end

Slide 15

Slide 15 text

JSI module setup Part 2/7: ObjcRuntime.mm - stub implementation #import “ObjcRuntime.h" #import @implementation ObjcRuntime RCT_EXPORT_MODULE() + (BOOL)requiresMainQueueSetup { return YES; } // The installation lifecycle - (void)setBridge:(RCTBridge *)bridge {} // The cleanup lifecycle - (void)invalidate {} @end

Slide 16

Slide 16 text

JSI module setup Part 3/7: ObjcRuntime.mm - grabbing the bridge #import "ObjcGlobal.h" #import @implementation ObjcRuntime @synthesize bridge = m_bridge; // 1⃣ Create a setter named `m_bridge` for `self.bridge`. RCT_EXPORT_MODULE() + (BOOL)requiresMainQueueSetup { return YES; } // The installation lifecycle - (void)setBridge:(RCTBridge *)bridge { // 2⃣ Store the bridge so that we can access it later in the `invalidate` method. m_bridge = bridge; } // The cleanup lifecycle - (void)invalidate {} @end

Slide 17

Slide 17 text

JSI module setup Part 4/7: ObjcRuntime.mm - grabbing the JSI runtime #import "ObjcRuntime.h" #import #import // 1⃣ Imports all the JSI APIs we need, e.g. `facebook::jsi`. using namespace facebook; // 2⃣ Shortens `facebook::jsi::Runtime` to `jsi::Runtime`. @implementation ObjcRuntime @synthesize bridge = m_bridge; RCT_EXPORT_MODULE() + (BOOL)requiresMainQueueSetup { return YES; } // The installation lifecycle - (void)setBridge:(RCTBridge *)bridge { m_bridge = bridge; // 3⃣ Grab the JSI runtime. RCTCxxBridge *cxxBridge = (RCTCxxBridge *)m_bridge; jsi::Runtime *runtime = (jsi::Runtime *)cxxBridge.runtime; if (!runtime) { return; } } // …

Slide 18

Slide 18 text

JSI module setup Part 5/7: ObjcRuntime.mm - setting a property on the global object // … // The installation lifecycle - (void)setBridge:(RCTBridge *)bridge { m_bridge = bridge; RCTCxxBridge *cxxBridge = (RCTCxxBridge *)m_bridge; jsi::Runtime *runtime = (jsi::Runtime *)cxxBridge.runtime; if (!runtime) { return; } // 1⃣ Create a JSI string from a C string. jsi::String jsiString = jsi::String::createFromAscii(*runtime, "A C string!"); // 2⃣ Set global.objc = jsiString. runtime->global().setProperty(*runtime, "objc", jsiString); } // …

Slide 19

Slide 19 text

JSI module setup Part 6/7: ObjcRuntime.mm - cleanup // … // The cleanup lifecycle - (void)invalidate { // Grab the JSI runtime. RCTCxxBridge *cxxBridge = (RCTCxxBridge *)m_bridge; jsi::Runtime *runtime = (jsi::Runtime *)cxxBridge.runtime; if (!runtime) { return; } // Set global.objc = undefined. runtime->global().setProperty(*runtime, "objc", jsi::Value::undefined()); } // …

Slide 20

Slide 20 text

JSI module setup Part 7/7: App.tsx - call it! // 1⃣ Declare this at the top level of your file declare const objc: any; // 2⃣ Use this effect in your App component. React.useEffect(() => console.log('objc:', objc), []);

Slide 21

Slide 21 text

JSI in action Hopefully, at this point you see the text: objc: A C string! If not, you may have run into a startup race condition (resolvable by refreshing the JS bundle). For a workaround, see margelo/ react-native-quick-crypto - just search for: “RCT_EXPORT_BLOCKING_SYNCH RONOUS_METHOD”.

Slide 22

Slide 22 text

How do we create a JSI value other than a string?

Slide 23

Slide 23 text

JSI types cheatsheet Part 1/4 • String jsi::String::createFromAscii(*rt, "A C string!"); // createFromUtf8 also works. jsi::String::createFromUtf8(*rt, @"An NSString".UTF8String); • Number jsi::Value(123); // C number jsi::Value([NSNumber numberWithDouble:3.1415926].doubleValue); // NSNumber longhand jsi::Value((@3.1415926).doubleValue); // LLVM shorthand to instantiate an NSNumber • Boolean jsi::Value(TRUE); // Boolean (unsigned char) jsi::Value(YES); // Obj-C BOOL (signed char) // The following do not work: // true, false, @(YES), @(NO), kCFBooleanTrue, kCFBooleanFalse, 0, 1

Slide 24

Slide 24 text

JSI types cheatsheet Part 2/4 • Null jsi::Value(nullptr); jsi::Value::null(); • Undefined jsi::Value::undefined(); • Object jsi::Object(*rt); • HostObject jsi::Object::createFromHostObject(*rt, std::make_shared()); // We’ll come back to HostObject later!

Slide 25

Slide 25 text

JSI types cheatsheet Part 3/4 • BigInt (under development!) jsi::BigInt::createBigIntFromInt64(*rt, -9223372036854775808); jsi::BigInt::createBigIntFromUint64(*rt, 18446744073709551615); • Symbol // I think you can only clone them from the JS context - not create them afresh.

Slide 26

Slide 26 text

JSI types cheatsheet Part 4/4 • Function // The host function auto sum = [] ( jsi::Runtime& rt, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count ) -> jsi::Value { return jsi::Value(arguments[0].asNumber() + arguments[1].asNumber()); }; // The JSI function jsi::Function::createFromHostFunction( *rt, jsi::PropNameID::forAscii(*rt, "sum"), // The name for the function in JS 2, // The number of arguments sum // The host function );

Slide 27

Slide 27 text

// Forked from Marc Rousavy’s react-native-vision-camera; MIT licence. // (Copyright © 2021 mrousavy, all rights reserved). // // Originally extracted from RCTTurboModule.mm. // Copyright (c) Facebook, Inc. and its affiliates. See MIT licence in react-native. // … jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) { return jsi::String::createFromUtf8(runtime, [value UTF8String] ?: ""); } NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) { return [NSString stringWithUTF8String:value.utf8(runtime).c_str()]; } // … JSIUtils.mm - an excerpt Helpers to convert between JSI and Obj-C primitives

Slide 28

Slide 28 text

Let’s start building our Objective ‑ C runtime projection.

Slide 29

Slide 29 text

Objectives • Create a HostObject via JSI, that can be accessed globally from JS as objc. • The HostObject will provide an experience similar to writing Objective-C: • Constant access: objc.NSStringTransformLatinToHiragana • Class access: objc.NSString • Method calls: objc.NSString.alloc() • Param marshalling: objc.NSString.alloc()['initWithString:']('Hello') • Getters (we won’t have time to cover setters):
 const blueColor = objc.UIColor.blueColor

Slide 30

Slide 30 text

First off, let’s change our `objc` value to be a subclass of jsi::HostObject instead.

Slide 31

Slide 31 text

Making `objc` into a subclass of jsi::HostObject • Add two empty fi les, HostObjectObjc.h and HostObjectObjc.mm. Part 1/5: File setup

Slide 32

Slide 32 text

Part 2/5: HostObjectObjc.h - the interface #import using namespace facebook; class JSI_EXPORT HostObjectObjc: public jsi::HostObject { public: jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override; std::vector getPropertyNames(jsi::Runtime& rt) override; }; Making `objc` into a subclass of jsi::HostObject

Slide 33

Slide 33 text

Part 3/5: HostObjectObjc.mm - stub implementation #import "HostObjectObjc.h" // Returns the value for any given property accessed. jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { // For now, we'll return undefined. return jsi::Value::undefined(); } // Returns the list of keys. std::vector HostObjectObjc::getPropertyNames(jsi::Runtime& rt) { std::vector result; // For now, we'll return an empty array. return result; } Making `objc` into a subclass of jsi::HostObject

Slide 34

Slide 34 text

Part 4/5: ObjcRuntime.mm - exposing HostObjectObjc #import "HostObjectObjc.h" Go back to ObjcRuntime.mm and add this import: … and update our runtime->global().setProperty() call so that it sets an instance of our HostObjectObjc on `objc` instead of a string: Making `objc` into a subclass of jsi::HostObject - jsi::String jsiString = jsi::String::createFromAscii(*runtime, "A C string!"); runtime->global().setProperty( *runtime, "objc", - jsi::Object::createFromHostObject(*runtime, jsiString) + jsi::Object::createFromHostObject(*runtime, std::make_shared()) );

Slide 35

Slide 35 text

Now that objc returns a HostObject, let’s have a look at it: console.log('objc:', objc); console.log('Object.keys(objc):', Object.keys(objc)); console.log('objc.toString():', objc.toString()); objc prints as an empty object, {}, with no keys: [] … and toString() just throws an error, as it hasn’t been implemented yet! Making `objc` into a subclass of jsi::HostObject Part 5/5: App.tsx - logging HostObjectObjc

Slide 36

Slide 36 text

Let’s start easy, exposing a known serialisable API from Objective-C.

Slide 37

Slide 37 text

Exposing a serialisable Objective-C API • Here’s the API declaration: const NSStringTransform NSStringTransformLatinToHiragana; • NSStringTransform is just an NSString by another name. • It’s a global variable, so we want to expose it on the JS side via: objc.NSStringTransformLatinToHiragana • We want it to return the string value of that variable.

Slide 38

Slide 38 text

Exposing a serialisable API #import "HostObjectObjc.h" #import // 1⃣ Import Foundation! jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { auto name = propName.utf8(rt); // 2⃣ Add a case to handle NSStringTransformLatinToHiragana. if (name == "NSStringTransformLatinToHiragana"){ return jsi::String::createFromUtf8(rt, NSStringTransformLatinToHiragana.UTF8String); } return jsi::Value::undefined(); } Part 1/3: HostObjectObjc.mm - the getter

Slide 39

Slide 39 text

Exposing a serialisable API std::vector HostObjectObjc::getPropertyNames(jsi::Runtime& rt) { std::vector result; // 1⃣ Add "NSStringTransformLatinToHiragana" as an enumerable key. result.push_back(jsi::PropNameID::forAscii(rt, "NSStringTransformLatinToHiragana")); return result; } Part 2/3: HostObjectObjc.mm - updating the enumerable keys

Slide 40

Slide 40 text

Let’s have a look at the new behaviour: console.log('objc:', objc); console.log('objc.NSStringTransformLatinToHiragana:', objc.NSStringTransformLatinToHiragana); console.log('Object.keys(objc):', Object.keys(objc)); Exposing a serialisable API Part 3/3: App.tsx - the result … Congratulations, you just proxied a native API! 🎉

Slide 41

Slide 41 text

Let’s go the whole way, exposing arbitrary Objective-C APIs, even non-serialisable ones.

Slide 42

Slide 42 text

Exposing non-serialisable APIs We’ll approach this by improving HostObjectObjc to handle any value in the Obj-C runtime. • The constructor will now take: 1. a pointer to a native object, allowing us to “wrap” a native data type; 2. an “isGlobal” param for the special case of the global object. • get() will now proxy through to the underlying native oject. • getPropertyNames() will now list all the available properties. • We’ll skip set() as there’s not much time!

Slide 43

Slide 43 text

#import using namespace facebook; // 1⃣ All the types of native ref that we’ll support. // We check it once at construction time to avoid re-checking. enum HostObjectObjcType { OTHER, CLASS, CLASS_INSTANCE, GLOBAL, }; class JSI_EXPORT HostObjectObjc: public jsi::HostObject { public: HostObjectObjc(void *nativeRef, bool isGlobal); void *m_nativeRef; HostObjectObjcType m_type; jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override; std::vector getPropertyNames(jsi::Runtime& rt) override; }; Exposing a non-serialisable API Part 1/11: HostObjectObjc.h - the interface

Slide 44

Slide 44 text

Exposing a non-serialisable API #import "HostObjectObjc.h" HostObjectObjc::HostObjectObjc(void *nativeRef, bool isGlobal): m_nativeRef(nativeRef) { // 1⃣ Determine the type of nativeRef and initialise m_type. } jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { auto name = propName.utf8(rt); if(m_type == GLOBAL){ // 2⃣ Look up classes/protocols/variables. } else if(m_type == CLASS || m_type == CLASS_INSTANCE){ // 3⃣ Look up methods/properties. } return jsi::Value::undefined(); } std::vector getPropertyNames(jsi::Runtime& rt) { // 4⃣ getPropertyNames(): Return the name for every case that we handle in the get() method above. } Part 2/11: HostObjectObjc.mm - implementation outline

Slide 45

Slide 45 text

// Set global.objc to be an instance of HostObjectObjc. runtime->global().setProperty( *runtime, "objc", - jsi::Object::createFromHostObject(*runtime, std::make_shared()) + jsi::Object::createFromHostObject(*runtime, std::make_shared((void*)NULL, true)) ); Go back to ObjcRuntime.mm and update our runtime->global().setProperty() call to pass in HostObjectObjc’s new construction params: Exposing a non-serialisable API Part 3/11: ObjcRuntime.mm - update the constructor call

Slide 46

Slide 46 text

Exposing a non-serialisable API #import “HostObjectObjc.h” #import // <- Add this import. HostObjectObjc::HostObjectObjc(void *nativeRef, bool isGlobal): m_nativeRef(nativeRef) { // 1⃣ Determine the type of nativeRef and initialise m_type. if(isGlobal){ m_type = GLOBAL; return; } @try { if([(__bridge NSObject *)m_nativeRef isKindOfClass:[NSObject class]]){ m_type = class_isMetaClass(object_getClass((__bridge NSObject *)m_nativeRef)) ? CLASS : CLASS_INSTANCE; return; } } @catch (NSException *exception) { // Handles both ObjC and C++ exceptions as long as it's 64-bit. } m_type = OTHER; } Part 4/11: HostObjectObjc.mm - implementing the constructor

Slide 47

Slide 47 text

Exposing a non-serialisable API // … jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { // … NSString *nameNSString = [NSString stringWithUTF8String:name.c_str()]; if(m_type == GLOBAL){ // 1⃣ Look up classes/protocols. if (Class clazz = NSClassFromString(nameNSString)) { return jsi::Object::createFromHostObject( rt, std::make_shared((__bridge void*)clazz, false) ); } if (Protocol *protocol = NSProtocolFromString(nameNSString)) { return jsi::Object::createFromHostObject( rt, std::make_shared((__bridge void*)protocol, false) ); } // 2⃣ … We’ll continue this block and show global variable lookup on the next slide! } // … } Part 5/11: HostObjectObjc.mm - implementing class/protocol getters for GLOBAL

Slide 48

Slide 48 text

Exposing a non-serialisable API #import // <- Add this import. // … jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { // … if(m_type == GLOBAL){ // 1⃣ … See previous slide for class/protocol lookup. // 2⃣ Look up global variables via the dynamic linker (thanks @develobile for telling me about it). void *value = dlsym(RTLD_MAIN_ONLY, nameNSString.UTF8String); if (!value) { value = dlsym(RTLD_SELF, nameNSString.UTF8String); } if (!value) { value = dlsym(RTLD_DEFAULT, nameNSString.UTF8String); } return value ? jsi::Object::createFromHostObject( rt, std::make_shared(*((void**)value), false) ); jsi::Value::undefined(); } // … } Part 6/11: HostObjectObjc.mm - implementing global variable getters for GLOBAL

Slide 49

Slide 49 text

Exposing a non-serialisable API jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { // … if(m_type == OTHER) return jsi::Value::undefined(); // 1⃣ Look up methods. SEL sel = NSSelectorFromString(nameNSString); if([(__bridge NSObject *)m_nativeRef respondsToSelector:sel]){ // 2⃣ I won’t be presenting how to implement invokeMethod(), as it’s very long - see the repo! return invokeMethod(rt, name, sel); } // 3⃣ … We’ll continue this block and show property lookup on the next slide! // … } Part 7/11: HostObjectObjc.mm - implementing getters for the other types

Slide 50

Slide 50 text

Exposing a non-serialisable API jsi::Value HostObjectObjc::get(jsi::Runtime& rt, const jsi::PropNameID& propName) { // … // … See previous slide for method lookup! // 1⃣ Look up properties. NSObject *nativeRef = (__bridge NSObject *)m_nativeRef; Class clazz = m_type == CLASS ? (Class)nativeRef : [nativeRef class]; objc_property_t property = class_getProperty(clazz, nameNSString.UTF8String); if(property){ const char *propertyName = property_getName(property); if(propertyName){ NSObject *value = [nativeRef valueForKey:[NSString stringWithUTF8String:propertyName]]; return convertObjCObjectToJSIValue(rt, value); } } return jsi::Value::undefined(); } Part 8/11: HostObjectObjc.mm - implementing getters for the other types

Slide 51

Slide 51 text

Exposing a non-serialisable API // 1⃣ Return the name for every case that we handle in the get() method. std::vector HostObjectObjc::getPropertyNames(jsi::Runtime& rt) { std::vector result; NSObject *nativeRef = (__bridge NSObject *)m_nativeRef; Class clazz = m_type == CLASS ? objc_getMetaClass(class_getName((Class)nativeRef)) : [nativeRef class]; // 2⃣ Copy properties. TODO: do the same for subclasses and categories, too. unsigned int pCount; objc_property_t *pList = class_copyPropertyList(clazz, &pCount); for(unsigned int i = 0; i < pCount; i++){ result.push_back(jsi::PropNameID::forUtf8(rt, property_getName(pList[i]))); } free(pList); // 3⃣ Copy methods. unsigned int mCount; Method *mList = class_copyMethodList(clazz, &mCount); for(unsigned int i = 0; i < mCount; i++){ result.push_back(jsi::PropNameID::forUtf8(rt, NSStringFromSelector(method_getName(mList[i])).UTF8String)); } free(mList); return result; } Part 9/11: HostObjectObjc.mm - implementing getPropertyNames()

Slide 52

Slide 52 text

Exposing a non-serialisable API Part 10/11: HostObjectObjc.mm - other bits I won’t go into There won’t be time to cover these, but I’ll at least summarise them. 1. set() - Look up the relevant property setter and send an Obj-C message to it. 2. invokeMethod() - For the given method name, return a JSI::Function in which (in its host function) we prepare and invoke a corresponding NSInvocation. In both cases, we marshal any JS values/params into Obj-C before passing them to the API. This may involve unwrapping (pulling out the nativeRef property from) one of our HostObjectObjc instances.
 
 For full details, see the shirakaba/rnobjc repo!

Slide 53

Slide 53 text

Part 11/11: App.tsx - call it! console.log( 'Transliterate '͠Β͔͹' into Latin via NSString:', objc.NSString.alloc() ['initWithString:']('͠Β͔͹') ['stringByApplyingTransform:reverse:']( objc.kCFStringTransformToLatin, false ) ); LOG Transliterate '͠Β͔͹' into Latin via NSString: shirakaba … Congratulations, you can now arbitrarily access the Obj-C runtime from JS! 🎉 Exposing a non-serialisable API

Slide 54

Slide 54 text

It’s not totally working A confession

Slide 55

Slide 55 text

Worries • Memory management: • Are we losing our native refs (previous slide)? • Are we leaking memory? • Is it secure? • Further work needed for: • Marshalling C values • TypeScript typings • Debugging is hard as everything’s dynamic • Will Apple reject apps using this library?

Slide 56

Slide 56 text

Future ideas • Code review (help!) • Change from dynamic to static access • Use approach as a basis for smaller-scale libraries (e.g. just NSString) • An Android runtime • Delegate to NativeScript: • Use JSI to call NativeScript from React Native, or even better; • Swap React Native JSC/V8 for NativeScript JSC/V8, and call it directly

Slide 57

Slide 57 text

Conclusion • JSI and Obj-C++ are not so scary for simple tasks • JSI is suitable for arbitrary native API access (though we need to get the memory management correct) • I hope you learned a bit more about: • C++ • Obj-C metaprogramming • Building JSI modules in general

Slide 58

Slide 58 text

Thank you! Jamie Birch Tokyo @LinguaBrowse @shirakaba shirakaba/react-native-native-runtime shirakaba/rnobjc Icons by FontAwesome; CC BY 4.0 One last JS bridging metaphor