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
#import "ObjcGlobal.h" #import <React/RCTBridge+Private.h> @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
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), []);
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”.
-9223372036854775808); jsi::BigInt::createBigIntFromUint64(*rt, 18446744073709551615); • Symbol // I think you can only clone them from the JS context - not create them afresh.
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 );
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
facebook; class JSI_EXPORT HostObjectObjc: public jsi::HostObject { public: jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override; std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override; }; Making `objc` into a subclass of jsi::HostObject
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<jsi::PropNameID> HostObjectObjc::getPropertyNames(jsi::Runtime& rt) { std::vector<jsi::PropNameID> result; // For now, we'll return an empty array. return result; } Making `objc` into a subclass of jsi::HostObject
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<HostObjectObjc>()) );
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
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.
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
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! 🎉
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!
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<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override; }; Exposing a non-serialisable API Part 1/11: HostObjectObjc.h - the interface
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<jsi::PropNameID> 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
*runtime, "objc", - jsi::Object::createFromHostObject(*runtime, std::make_shared<HostObjectObjc>()) + jsi::Object::createFromHostObject(*runtime, std::make_shared<HostObjectObjc>((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
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<HostObjectObjc>(*((void**)value), false) ); jsi::Value::undefined(); } // … } Part 6/11: HostObjectObjc.mm - implementing global variable getters for GLOBAL
{ // … 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
every case that we handle in the get() method. std::vector<jsi::PropNameID> HostObjectObjc::getPropertyNames(jsi::Runtime& rt) { std::vector<jsi::PropNameID> 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()
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!
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
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?
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
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