Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,18 @@ static int32_t getUniqueId()
static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value)
{
jsi::Object result = jsi::Object(runtime);
for (NSString *k in value) {
result.setProperty(runtime, convertNSStringToJSIString(runtime, k), convertObjCObjectToJSIValue(runtime, value[k]));
for (id k in value) {
// Number keys become strings, and other keys that aren't strings are skipped, as
// convertIdToFollyDynamic does.
NSString *key = k;
if (![k isKindOfClass:[NSString class]]) {
if (![k isKindOfClass:[NSNumber class]] || [k isKindOfClass:[@YES class]]) {
continue;
}
key = [k stringValue];
}
result.setProperty(
runtime, convertNSStringToJSIString(runtime, key), convertObjCObjectToJSIValue(runtime, value[k]));
}
return result;
}
Expand Down
143 changes: 98 additions & 45 deletions packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -309,60 +309,113 @@ void ReactInstance::loadScript(
});
}

namespace {

/*
* Calls a method on a JS module that has been registered with
* `registerCallableModule`. Used to invoke a JS function from platform code.
* `registerCallableModule`, with the arguments that `addArgs` adds.
*/
void ReactInstance::callFunctionOnModule(
template <typename AddArgs>
void callCallableModuleMethod(
jsi::Runtime& runtime,
std::unordered_map<std::string, std::variant<jsi::Function, jsi::Object>>&
callableModules,
const std::string& moduleName,
const std::string& methodName,
folly::dynamic&& args) {
bufferedRuntimeExecutor_->execute([this,
moduleName = moduleName,
methodName = methodName,
args = std::move(args)](
jsi::Runtime& runtime) {
TraceSection s(
"ReactInstance::callFunctionOnModule",
"moduleName",
moduleName,
"methodName",
methodName);
auto it = callableModules_.find(moduleName);
if (it == callableModules_.end()) {
std::ostringstream knownModules;
int i = 0;
for (it = callableModules_.begin(); it != callableModules_.end();
it++, i++) {
const char* space = (i > 0 ? ", " : " ");
knownModules << space << it->first;
}
throw jsi::JSError(
runtime,
"Failed to call into JavaScript module method " + moduleName + "." +
methodName +
"(). Module has not been registered as callable. Registered callable JavaScript modules (n = " +
std::to_string(callableModules_.size()) +
"):" + knownModules.str() +
". Did you forget to call `registerCallableModule`?");
const AddArgs& addArgs) {
TraceSection s(
"ReactInstance::callFunctionOnModule",
"moduleName",
moduleName,
"methodName",
methodName);
auto it = callableModules.find(moduleName);
if (it == callableModules.end()) {
std::ostringstream knownModules;
int i = 0;
for (it = callableModules.begin(); it != callableModules.end(); it++, i++) {
const char* space = (i > 0 ? ", " : " ");
knownModules << space << it->first;
}
throw jsi::JSError(
runtime,
"Failed to call into JavaScript module method " + moduleName + "." +
methodName +
"(). Module has not been registered as callable. Registered callable JavaScript modules (n = " +
std::to_string(callableModules.size()) + "):" + knownModules.str() +
". Did you forget to call `registerCallableModule`?");
}

if (std::holds_alternative<jsi::Function>(it->second)) {
auto module =
std::get<jsi::Function>(it->second).call(runtime).asObject(runtime);
it->second = std::move(module);
}
if (std::holds_alternative<jsi::Function>(it->second)) {
auto module =
std::get<jsi::Function>(it->second).call(runtime).asObject(runtime);
it->second = std::move(module);
}

auto& module = std::get<jsi::Object>(it->second);
auto method = module.getPropertyAsFunction(runtime, methodName.c_str());
auto& module = std::get<jsi::Object>(it->second);
auto method = module.getPropertyAsFunction(runtime, methodName.c_str());

std::vector<jsi::Value> jsArgs;
for (auto& arg : args) {
jsArgs.push_back(jsi::valueFromDynamic(runtime, arg));
}
method.callWithThis(
runtime, module, (const jsi::Value*)jsArgs.data(), jsArgs.size());
});
std::vector<jsi::Value> jsArgs;
addArgs(jsArgs);
method.callWithThis(
runtime, module, (const jsi::Value*)jsArgs.data(), jsArgs.size());
}

} // namespace

/*
* Calls a method on a JS module that has been registered with
* `registerCallableModule`. Used to invoke a JS function from platform code.
*/
void ReactInstance::callFunctionOnModule(
const std::string& moduleName,
const std::string& methodName,
folly::dynamic&& args) {
if (moduleName.empty() || methodName.empty()) {
return;
}
bufferedRuntimeExecutor_->execute(
[this,
moduleName = moduleName,
methodName = methodName,
args = std::move(args)](jsi::Runtime& runtime) {
callCallableModuleMethod(
runtime,
callableModules_,
moduleName,
methodName,
[&](std::vector<jsi::Value>& jsArgs) {
jsArgs.reserve(args.size());
for (auto& arg : args) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsArgs.reserve?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javache done, thanks! 843adb2

jsArgs.push_back(jsi::valueFromDynamic(runtime, arg));
}
});
});
}

void ReactInstance::callFunctionOnModule(
const std::string& moduleName,
const std::string& methodName,
ArgFactory&& argFactory) {
if (moduleName.empty() || methodName.empty()) {
return;
}
bufferedRuntimeExecutor_->execute(
[this,
moduleName = moduleName,
methodName = methodName,
argFactory = std::move(argFactory)](jsi::Runtime& runtime) {
callCallableModuleMethod(
runtime,
callableModules_,
moduleName,
methodName,
[&](std::vector<jsi::Value>& jsArgs) {
if (argFactory) {
argFactory(runtime, jsArgs);
}
});
});
}

void ReactInstance::registerSegment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace facebook::react {
class ReactInstance final : private jsinspector_modern::InstanceTargetDelegate {
public:
using BindingsInstallFunc = std::function<void(jsi::Runtime &runtime)>;
using ArgFactory = std::function<void(jsi::Runtime &runtime, std::vector<jsi::Value> &args)>;

ReactInstance(
std::unique_ptr<JSRuntime> runtime,
Expand Down Expand Up @@ -71,6 +72,12 @@ class ReactInstance final : private jsinspector_modern::InstanceTargetDelegate {

void callFunctionOnModule(const std::string &moduleName, const std::string &methodName, folly::dynamic &&args);

/**
* Like the overload above, but `argFactory` adds the arguments on the JS thread, so platform
* values can be converted to JS values directly.
*/
void callFunctionOnModule(const std::string &moduleName, const std::string &methodName, ArgFactory &&argFactory);

void handleMemoryPressureJs(int pressureLevel);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/runtime/ReactInstance.h>
#import <react/utils/ContextContainer.h>
#import <react/utils/FollyConvert.h>
#import <react/utils/ManagedObjectWrapper.h>

#import "ObjCTimerRegistry.h"
Expand Down Expand Up @@ -173,14 +172,21 @@ - (void)dealloc

- (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method args:(NSArray *)args
{
// The arguments are converted on the JS thread, like the values of codegen's AsyncEventEmitter.
NSArray *callArgs = [args copy];
// This is called from arbitrary threads, while -invalidate destroys _reactInstance on
// the JS thread, so checking _valid and dereferencing have to happen as one step.
std::lock_guard<std::mutex> lock(_invalidationMutex);
if (!_valid || !_reactInstance) {
return;
}
_reactInstance->callFunctionOnModule(
[moduleName UTF8String] ?: "", [method UTF8String] ?: "", convertIdToFollyDynamic(args ? args : @[]));
[moduleName UTF8String], [method UTF8String], [callArgs](jsi::Runtime &runtime, std::vector<jsi::Value> &jsArgs) {
jsArgs.reserve(callArgs.count);
for (id arg in callArgs) {
jsArgs.push_back(TurboModuleConvertUtils::convertObjCObjectToJSIValue(runtime, arg));
}
});
}

- (void)invalidate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -821,4 +821,86 @@ function getResult() {
EXPECT_EQ(result.getNumber(), 1);
}

TEST_F(ReactInstanceTest, testCallFunctionOnModule_withArgFactory) {
initializeRuntimeWithScript(R"xyz123(
let result;
const module = {
bar: (a, b) => {
result = a + ':' + b;
},
};
RN$registerCallableModule('foo', () => module);
function getResult() {
return result;
}
)xyz123");

instance_->callFunctionOnModule(
"foo", "bar", [](jsi::Runtime& runtime, std::vector<jsi::Value>& args) {
args.emplace_back(jsi::String::createFromAscii(runtime, "baz"));
args.emplace_back(2);
});
step();

auto result = runtime_->global()
.getPropertyAsFunction(*runtime_, "getResult")
.call(*runtime_);
EXPECT_EQ(result.getString(*runtime_).utf8(*runtime_), "baz:2");
}

TEST_F(ReactInstanceTest, testCallFunctionOnModule_withEmptyArgFactory) {
initializeRuntimeWithScript(R"xyz123(
let result;
const module = {
bar: (...args) => {
result = args.length;
},
};
RN$registerCallableModule('foo', () => module);
function getResult() {
return result;
}
)xyz123");

instance_->callFunctionOnModule("foo", "bar", ReactInstance::ArgFactory{});
step();

auto result = runtime_->global()
.getPropertyAsFunction(*runtime_, "getResult")
.call(*runtime_);
EXPECT_EQ(result.getNumber(), 0);
}

TEST_F(ReactInstanceTest, testCallFunctionOnModule_withEmptyNames) {
initializeRuntimeWithScript(R"xyz123(
let calls = 0;
const module = {
bar: () => {
calls++;
},
};
RN$registerCallableModule('foo', () => module);
function getCalls() {
return calls;
}
)xyz123");

bool argFactoryCalled = false;
auto argFactory = [&](jsi::Runtime& /*runtime*/,
std::vector<jsi::Value>& /*args*/) {
argFactoryCalled = true;
};
instance_->callFunctionOnModule("", "bar", argFactory);
instance_->callFunctionOnModule("foo", "", argFactory);
instance_->callFunctionOnModule("", "bar", folly::dynamic::array());
instance_->callFunctionOnModule("foo", "", folly::dynamic::array());
step();

EXPECT_FALSE(argFactoryCalled);
auto calls = runtime_->global()
.getPropertyAsFunction(*runtime_, "getCalls")
.call(*runtime_);
EXPECT_EQ(calls.getNumber(), 0);
}

} // namespace facebook::react
2 changes: 2 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -3346,7 +3346,9 @@ class facebook::react::ReactInstance {
public facebook::react::RuntimeExecutor getUnbufferedRuntimeExecutor() noexcept;
public std::shared_ptr<facebook::react::CallInvoker> createJSCallInvoker() noexcept;
public std::shared_ptr<facebook::react::RuntimeScheduler> getRuntimeScheduler() noexcept;
public using ArgFactory = std::function<void(facebook::jsi::Runtime& runtime, std::vector<facebook::jsi::Value>& args)>;
public using BindingsInstallFunc = std::function<void(facebook::jsi::Runtime& runtime)>;
public void callFunctionOnModule(const std::string& moduleName, const std::string& methodName, facebook::react::ReactInstance::ArgFactory&& argFactory);
public void callFunctionOnModule(const std::string& moduleName, const std::string& methodName, folly::dynamic&& args);
public void handleMemoryPressureJs(int pressureLevel);
public void initializeRuntime(facebook::react::ReactInstance::JSRuntimeFlags options, facebook::react::ReactInstance::BindingsInstallFunc bindingsInstallFunc) noexcept;
Expand Down
2 changes: 2 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -5562,7 +5562,9 @@ class facebook::react::ReactInstance {
public facebook::react::RuntimeExecutor getUnbufferedRuntimeExecutor() noexcept;
public std::shared_ptr<facebook::react::CallInvoker> createJSCallInvoker() noexcept;
public std::shared_ptr<facebook::react::RuntimeScheduler> getRuntimeScheduler() noexcept;
public using ArgFactory = std::function<void(facebook::jsi::Runtime& runtime, std::vector<facebook::jsi::Value>& args)>;
public using BindingsInstallFunc = std::function<void(facebook::jsi::Runtime& runtime)>;
public void callFunctionOnModule(const std::string& moduleName, const std::string& methodName, facebook::react::ReactInstance::ArgFactory&& argFactory);
public void callFunctionOnModule(const std::string& moduleName, const std::string& methodName, folly::dynamic&& args);
public void handleMemoryPressureJs(int pressureLevel);
public void initializeRuntime(facebook::react::ReactInstance::JSRuntimeFlags options, facebook::react::ReactInstance::BindingsInstallFunc bindingsInstallFunc) noexcept;
Expand Down
2 changes: 2 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -1964,7 +1964,9 @@ class facebook::react::ReactInstance {
public facebook::react::RuntimeExecutor getUnbufferedRuntimeExecutor() noexcept;
public std::shared_ptr<facebook::react::CallInvoker> createJSCallInvoker() noexcept;
public std::shared_ptr<facebook::react::RuntimeScheduler> getRuntimeScheduler() noexcept;
public using ArgFactory = std::function<void(facebook::jsi::Runtime& runtime, std::vector<facebook::jsi::Value>& args)>;
public using BindingsInstallFunc = std::function<void(facebook::jsi::Runtime& runtime)>;
public void callFunctionOnModule(const std::string& moduleName, const std::string& methodName, facebook::react::ReactInstance::ArgFactory&& argFactory);
public void callFunctionOnModule(const std::string& moduleName, const std::string& methodName, folly::dynamic&& args);
public void handleMemoryPressureJs(int pressureLevel);
public void initializeRuntime(facebook::react::ReactInstance::JSRuntimeFlags options, facebook::react::ReactInstance::BindingsInstallFunc bindingsInstallFunc) noexcept;
Expand Down
Loading