Skip to content

Commit

Permalink
[modules][ios] Defer the creation of module's JS object until its fir…
Browse files Browse the repository at this point in the history
…st usage (#18863)
  • Loading branch information
tsapeta committed Aug 29, 2022
1 parent 6e1af36 commit fa88aeb
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/expo-modules-core/CHANGELOG.md
Expand Up @@ -22,6 +22,7 @@
- Automatically convert records to dicts when returned by the function. ([#18824](https://github.com/expo/expo/pull/18824) by [@tsapeta](https://github.com/tsapeta))
- Closures passed to definition components are now implicitly capturing `self` on iOS. ([#18831](https://github.com/expo/expo/pull/18831) by [@tsapeta](https://github.com/tsapeta))
- Support for CSS named colors in `UIColor` and `CGColor` convertibles on iOS. ([#18845](https://github.com/expo/expo/pull/18845) by [@tsapeta](https://github.com/tsapeta))
- Lazy load building the module's JavaScript object from the definition on iOS (already implemented on Android). ([#18863](https://github.com/expo/expo/pull/18863) by [@tsapeta](https://github.com/tsapeta))

### 🐛 Bug fixes

Expand Down
20 changes: 20 additions & 0 deletions packages/expo-modules-core/common/cpp/JSIUtils.cpp
@@ -0,0 +1,20 @@
// Copyright 2022-present 650 Industries. All rights reserved.

#include "JSIUtils.h"

namespace expo {

std::vector<jsi::PropNameID> jsiArrayToPropNameIdsVector(jsi::Runtime &runtime, const jsi::Array &array) {
size_t size = array.size(runtime);
std::vector<jsi::PropNameID> vector;

vector.reserve(size);

for (size_t i = 0; i < size; i++) {
jsi::String name = array.getValueAtIndex(runtime, i).getString(runtime);
vector.push_back(jsi::PropNameID::forString(runtime, name));
}
return vector;
}

} // namespace expo
19 changes: 19 additions & 0 deletions packages/expo-modules-core/common/cpp/JSIUtils.h
@@ -0,0 +1,19 @@
// Copyright 2022-present 650 Industries. All rights reserved.

#pragma once
#ifdef __cplusplus

#include <jsi/jsi.h>

namespace jsi = facebook::jsi;

namespace expo {

/**
Converts `jsi::Array` to a vector with prop name ids (`std::vector<jsi::PropNameID>`).
*/
std::vector<jsi::PropNameID> jsiArrayToPropNameIdsVector(jsi::Runtime &runtime, const jsi::Array &array);

} // namespace expo

#endif // __cplusplus
45 changes: 45 additions & 0 deletions packages/expo-modules-core/common/cpp/LazyObject.cpp
@@ -0,0 +1,45 @@
// Copyright 2022-present 650 Industries. All rights reserved.

#include "JSIUtils.h"
#include "LazyObject.h"

namespace expo {

LazyObject::LazyObject(const LazyObjectInitializer initializer) : initializer(initializer) {}

LazyObject::~LazyObject() {
backedObject = nullptr;
}

jsi::Value LazyObject::get(jsi::Runtime &runtime, const jsi::PropNameID &name) {
if (!backedObject) {
if (name.utf8(runtime) == "$$typeof") {
// React Native asks for this property for some reason, we can just ignore it.
return jsi::Value::undefined();
}
backedObject = initializer(runtime);
}
return backedObject ? backedObject->getProperty(runtime, name) : jsi::Value::undefined();
}

void LazyObject::set(jsi::Runtime &runtime, const jsi::PropNameID &name, const jsi::Value &value) {
if (!backedObject) {
backedObject = initializer(runtime);
}
if (backedObject) {
backedObject->setProperty(runtime, name, value);
}
}

std::vector<jsi::PropNameID> LazyObject::getPropertyNames(jsi::Runtime &runtime) {
if (!backedObject) {
backedObject = initializer(runtime);
}
if (backedObject) {
jsi::Array propertyNames = backedObject->getPropertyNames(runtime);
return jsiArrayToPropNameIdsVector(runtime, propertyNames);
}
return {};
}

} // namespace expo
43 changes: 43 additions & 0 deletions packages/expo-modules-core/common/cpp/LazyObject.h
@@ -0,0 +1,43 @@
// Copyright 2022-present 650 Industries. All rights reserved.

#pragma once

#ifdef __cplusplus

#include <jsi/jsi.h>

namespace jsi = facebook::jsi;

namespace expo {

/**
A function that is responsible for initializing the backed object.
*/
typedef std::function<std::shared_ptr<jsi::Object>(jsi::Runtime &)> LazyObjectInitializer;

/**
A host object that defers the creating of the raw object until any property is accessed for the first time.
*/
class JSI_EXPORT LazyObject : public jsi::HostObject {
public:
using Shared = std::shared_ptr<LazyObject>;

LazyObject(const LazyObjectInitializer initializer);

virtual ~LazyObject();

jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) override;

void set(jsi::Runtime &, const jsi::PropNameID &name, const jsi::Value &value) override;

std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime &rt) override;

private:
const LazyObjectInitializer initializer;
std::shared_ptr<jsi::Object> backedObject;

}; // class LazyObject

} // namespace expo

#endif // __cplusplus
1 change: 1 addition & 0 deletions packages/expo-modules-core/ios/JSI/EXJSIInstaller.mm
Expand Up @@ -3,6 +3,7 @@
#import <ExpoModulesCore/EXJSIInstaller.h>
#import <ExpoModulesCore/EXJavaScriptRuntime.h>
#import <ExpoModulesCore/ExpoModulesHostObject.h>
#import <ExpoModulesCore/LazyObject.h>
#import <ExpoModulesCore/Swift.h>

namespace jsi = facebook::jsi;
Expand Down
5 changes: 5 additions & 0 deletions packages/expo-modules-core/ios/JSI/EXJavaScriptObject.h
Expand Up @@ -42,6 +42,11 @@ NS_SWIFT_NAME(JavaScriptObject)
Returns the pointer to the underlying object.
*/
- (nonnull jsi::Object *)get;

/**
Returns the shared pointer to the underlying object.
*/
- (std::shared_ptr<jsi::Object>)getShared;
#endif // __cplusplus

#pragma mark - Accessing object properties
Expand Down
5 changes: 5 additions & 0 deletions packages/expo-modules-core/ios/JSI/EXJavaScriptObject.mm
Expand Up @@ -37,6 +37,11 @@ - (nonnull instancetype)initWith:(std::shared_ptr<jsi::Object>)jsObjectPtr
return _jsObjectPtr.get();
}

- (std::shared_ptr<jsi::Object>)getShared
{
return _jsObjectPtr;
}

#pragma mark - Accessing object properties

- (BOOL)hasProperty:(nonnull NSString *)name
Expand Down
5 changes: 5 additions & 0 deletions packages/expo-modules-core/ios/JSI/ExpoModulesHostObject.h
Expand Up @@ -3,6 +3,7 @@
#ifdef __cplusplus

#import <vector>
#import <unordered_map>
#import <jsi/jsi.h>

namespace jsi = facebook::jsi;
Expand All @@ -11,6 +12,9 @@ namespace jsi = facebook::jsi;

namespace expo {

using SharedJSIObject = std::shared_ptr<jsi::Object>;
using UniqueJSIObject = std::unique_ptr<jsi::Object>;

class JSI_EXPORT ExpoModulesHostObject : public jsi::HostObject {
public:
ExpoModulesHostObject(EXAppContext *appContext);
Expand All @@ -25,6 +29,7 @@ class JSI_EXPORT ExpoModulesHostObject : public jsi::HostObject {

private:
EXAppContext *appContext;
std::unordered_map<std::string, UniqueJSIObject> modulesCache;

}; // class ExpoModulesHostObject

Expand Down
25 changes: 22 additions & 3 deletions packages/expo-modules-core/ios/JSI/ExpoModulesHostObject.mm
Expand Up @@ -2,21 +2,40 @@

#import <ExpoModulesCore/ExpoModulesHostObject.h>
#import <ExpoModulesCore/EXJavaScriptObject.h>
#import <ExpoModulesCore/LazyObject.h>
#import <ExpoModulesCore/Swift.h>

namespace expo {

ExpoModulesHostObject::ExpoModulesHostObject(EXAppContext *appContext) : appContext(appContext) {}

ExpoModulesHostObject::~ExpoModulesHostObject() {
modulesCache.clear();
[appContext setRuntime:nil];
}

jsi::Value ExpoModulesHostObject::get(jsi::Runtime &runtime, const jsi::PropNameID &name) {
NSString *moduleName = [NSString stringWithUTF8String:name.utf8(runtime).c_str()];
EXJavaScriptObject *nativeObject = [appContext getNativeModuleObject:moduleName];
std::string moduleName = name.utf8(runtime);
NSString *nsModuleName = [NSString stringWithUTF8String:moduleName.c_str()];

return nativeObject ? jsi::Value(runtime, *[nativeObject get]) : jsi::Value::undefined();
if (![appContext hasModule:nsModuleName]) {
// The module object can already be cached but no longer registered — we remove it from the cache in that case.
modulesCache.erase(moduleName);
return jsi::Value::undefined();
}
if (UniqueJSIObject &cachedObject = modulesCache[moduleName]) {
return jsi::Value(runtime, *cachedObject);
}

// Create a lazy object for the specific module. It defers initialization of the final module object.
LazyObject::Shared moduleLazyObject = std::make_shared<LazyObject>(^SharedJSIObject(jsi::Runtime &runtime) {
return [[appContext getNativeModuleObject:nsModuleName] getShared];
});

// Save the module's lazy host object for later use.
modulesCache[moduleName] = std::make_unique<jsi::Object>(jsi::Object::createFromHostObject(runtime, moduleLazyObject));

return jsi::Value(runtime, *modulesCache[moduleName]);
}

void ExpoModulesHostObject::set(jsi::Runtime &runtime, const jsi::PropNameID &name, const jsi::Value &value) {
Expand Down
1 change: 1 addition & 0 deletions packages/expo-modules-core/ios/Swift/ModuleHolder.swift
Expand Up @@ -101,6 +101,7 @@ public final class ModuleHolder {
guard let runtime = appContext?.runtime else {
return nil
}
log.info("Creating JS object for module '\(name)'")
return definition.build(inRuntime: runtime)
}

Expand Down

0 comments on commit fa88aeb

Please sign in to comment.