Skip to content

Commit

Permalink
Implement Runtime.callFunctionOn
Browse files Browse the repository at this point in the history
Summary:
[Hermes][Inspector] Implement the CDP API for calling a function on an object.

Changelog: [Internal]

Reviewed By: avp

Differential Revision: D33722301

fbshipit-source-id: da26e865cf29920be77c5c602dde1b443b4c64da
  • Loading branch information
jpporto authored and facebook-github-bot committed Feb 2, 2022
1 parent cdfddb4 commit eb08af5
Show file tree
Hide file tree
Showing 5 changed files with 693 additions and 10 deletions.
320 changes: 319 additions & 1 deletion ReactCommon/hermes/inspector/chrome/Connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <folly/Conv.h>
#include <folly/Executor.h>
#include <folly/Function.h>
#include <folly/json.h>
#include <glog/logging.h>
#include <hermes/inspector/Inspector.h>
#include <hermes/inspector/chrome/MessageConverters.h>
Expand Down Expand Up @@ -100,11 +101,15 @@ class Connection::Impl : public inspector::InspectorObserver,
void handle(
const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) override;
void handle(const m::heapProfiler::GetHeapObjectIdRequest &req) override;
void handle(const m::runtime::CallFunctionOnRequest &req) override;
void handle(const m::runtime::EvaluateRequest &req) override;
void handle(const m::runtime::GetPropertiesRequest &req) override;
void handle(const m::runtime::RunIfWaitingForDebuggerRequest &req) override;

private:
// The execution context id reported back by the ExecutionContextCreated
// notification. We only ever expect this execution context id.
static constexpr int32_t kHermesExecutionContextId = 1;
std::vector<m::runtime::PropertyDescriptor> makePropsFromScope(
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
const std::string &objectGroup,
Expand Down Expand Up @@ -291,7 +296,7 @@ void Connection::Impl::onContextCreated(Inspector &inspector) {
// Right now, Hermes only has the notion of one JS context per VM instance,
// so we just always name the single JS context with id=1 and name=hermes.
m::runtime::ExecutionContextCreatedNotification note;
note.context.id = 1;
note.context.id = kHermesExecutionContextId;
note.context.name = "hermes";

sendNotificationToClientViaExecutor(note);
Expand Down Expand Up @@ -370,6 +375,8 @@ void Connection::Impl::onScriptParsed(
m::debugger::ScriptParsedNotification note;
note.scriptId = folly::to<std::string>(info.fileId);
note.url = info.fileName;
// TODO(jpporto): fix test cases sending invalid context id.
// note.executionContextId = kHermesExecutionContextId;

if (!info.sourceMappingUrl.empty()) {
note.sourceMapURL = info.sourceMappingUrl;
Expand Down Expand Up @@ -397,6 +404,8 @@ void Connection::Impl::onMessageAdded(
const ConsoleMessageInfo &info) {
m::runtime::ConsoleAPICalledNotification apiCalledNote;
apiCalledNote.type = info.level;
// TODO(jpporto): fix test cases sending invalid context id.
// apiCalledNote.executionContextId = kHermesExecutionContextId;

size_t argsSize = info.args.size(getRuntime());
for (size_t index = 0; index < argsSize; ++index) {
Expand Down Expand Up @@ -711,6 +720,315 @@ void Connection::Impl::handle(
.thenError<std::exception>(sendErrorToClient(req.id));
}

namespace {
/// Runtime.CallArguments can have their values specified "inline", or they can
/// have remote references. The inline values are eval'd together with the
/// Runtime.CallFunctionOn.functionDeclaration (see CallFunctionOnBuilder
/// below), while remote object Ids need to be resolved outside of the VM.
class CallFunctionOnArgument {
public:
explicit CallFunctionOnArgument(
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId)
: maybeObjectId_(std::move(maybeObjectId)) {}

/// Computes the real value for this argument, which can be an object
/// referenced by maybeObjectId_, or the given evaldValue. Throws if
/// maybeObjectId_ is not empty but references an unknown object.
jsi::Value value(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
jsi::Value evaldValue) const {
if (maybeObjectId_) {
assert(evaldValue.isUndefined() && "expected undefined placeholder");
return getValueFromId(rt, objTable, *maybeObjectId_);
}

return evaldValue;
}

private:
/// Returns the jsi::Object for the given objId. Throws if such object can't
/// be found.
static jsi::Value getValueFromId(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
m::runtime::RemoteObjectId objId) {
if (const jsi::Value *ptr = objTable.getValue(objId)) {
return jsi::Value(rt, *ptr);
}

throw std::runtime_error("unknown object id " + objId);
}

folly::Optional<m::runtime::RemoteObjectId> maybeObjectId_;
};

/// Functor that should be used to run the result of eval-ing a CallFunctionOn
/// request.
class CallFunctionOnRunner {
public:
static constexpr size_t kJsThisIndex = 0;
static constexpr size_t kFirstArgIndex = 1;

// N.B.: constexpr char[] broke react-native-oss-android.
static const char *kJsThisArgPlaceholder;

CallFunctionOnRunner() = default;
CallFunctionOnRunner(CallFunctionOnRunner &&) = default;
CallFunctionOnRunner &operator=(CallFunctionOnRunner &&) = default;

/// Performs the actual Runtime.CallFunctionOn request. It assumes.
/// \p evalResult is the result of invoking the Inspector's evaluate() method
/// on the expression built by the CallFunctionOnBuilder below.
jsi::Value operator()(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
const facebook::hermes::debugger::EvalResult &evalResult) {
// The eval result is an array [a0, a1, ..., an, func] (see
// CallFunctionOnBuilder below).
auto argsAndFunc = evalResult.value.getObject(rt).getArray(rt);
assert(
argsAndFunc.length(rt) == thisAndArguments_.size() + 1 &&
"Unexpected result size");

// now resolve the arguments to the call, including "this".
std::vector<jsi::Value> arguments(thisAndArguments_.size() - 1);

jsi::Object jsThis =
getJsThis(rt, objTable, argsAndFunc.getValueAtIndex(rt, kJsThisIndex));

int i = kFirstArgIndex;
for (/*i points to the first param*/; i < thisAndArguments_.size(); ++i) {
arguments[i - kFirstArgIndex] = thisAndArguments_[i].value(
rt, objTable, argsAndFunc.getValueAtIndex(rt, i));
}

// i is now func's index.
jsi::Function func =
argsAndFunc.getValueAtIndex(rt, i).getObject(rt).getFunction(rt);

return func.callWithThis(
rt,
std::move(jsThis),
static_cast<const jsi::Value *>(arguments.data()),
arguments.size());
}

private:
friend class CallFunctionOnBuilder;

CallFunctionOnRunner(const CallFunctionOnRunner &) = delete;
CallFunctionOnRunner &operator=(const CallFunctionOnRunner &) = delete;

CallFunctionOnRunner(
std::vector<CallFunctionOnArgument> thisAndArguments,
folly::Optional<m::runtime::ExecutionContextId> executionContextId)
: thisAndArguments_(std::move(thisAndArguments)),
executionContextId_(std::move(executionContextId)) {}

/// Resolves the js "this" for the request, which lives in
/// thisAndArguments_[kJsThisIndex]. \p evaldThis should either be undefined,
/// or the placeholder indicating that globalThis should be used.
jsi::Object getJsThis(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
jsi::Value evaldThis) const {
// In the future we may support multiple execution context ids; for now,
// there's only one.
(void)executionContextId_;

// Either evaldThis is undefined (because the request had an object id
// specifying "this"), or it should be a string (i.e., the placeholder
// kJsThisArgPlaceholder).
assert(
(evaldThis.isUndefined() ||
(evaldThis.isString() &&
evaldThis.getString(rt).utf8(rt) == kJsThisArgPlaceholder)) &&
"unexpected value for jsThis argument placeholder");

// Need to save this information because of the std::move() below.
const bool useGlobalThis = evaldThis.isString();
jsi::Value value = thisAndArguments_[kJsThisIndex].value(
rt, objTable, std::move(evaldThis));

return useGlobalThis ? rt.global() : value.getObject(rt);
}

std::vector<CallFunctionOnArgument> thisAndArguments_;
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
};

/*static*/ const char *CallFunctionOnRunner::kJsThisArgPlaceholder =
"jsThis is Execution Context";

/// Returns true if \p str is a number-like string value (e.g., Infinity),
/// and false otherwise.
bool unserializableValueLooksLikeNumber(const std::string &str) {
return str == "Infinity" || str == "-Infinity" || str == "NaN";
}

/// Helper class that processes a Runtime.CallFunctionOn request, and
/// builds an expression string that, once eval()d, yields an Array with the
/// CallArguments as well as the function to run. The generated array is
///
/// [JsThis, P0, P1, P2, P3, Pn, F]
///
/// where:
/// * F is the functionDeclaration in the request
/// * JsThis is either:
/// * undefined (if the request has an object ID); or
/// * the placeholder kJsThisArgPlaceholder
/// * Pi is either:
/// * the string in CallArgument[i].unserializableValue; or
/// * the string in CallArgument[i].value; or
/// * arguments[j] (i.e., the j-th argument passed to the newly built
/// function), j being the j-th CallArgument with an ObjectId. This is
/// needed because there's no easy way to express the objects referred
/// to by object ids by name.
class CallFunctionOnBuilder {
public:
explicit CallFunctionOnBuilder(const m::runtime::CallFunctionOnRequest &req)
: executionContextId_(req.executionContextId) {
out_ << "[";
thisAndArguments_.emplace_back(CallFunctionOnArgument(req.objectId));
if (req.objectId) {
out_ << "undefined, ";
} else {
out_ << '\'' << CallFunctionOnRunner::kJsThisArgPlaceholder << "', ";
}

addParams(req.arguments);
out_ << req.functionDeclaration;
out_ << "]";
};

/// Extracts the functions that handles the CallFunctionOn requests, as well
/// as the list of object ids that must be passed when calling it.
std::pair<std::string, CallFunctionOnRunner> expressionAndRunner() && {
return std::make_pair(
std::move(out_).str(),
CallFunctionOnRunner(
std::move(thisAndArguments_), std::move(executionContextId_)));
}

private:
void addParams(const folly::Optional<std::vector<m::runtime::CallArgument>>
&maybeArguments) {
if (maybeArguments) {
for (const auto &ca : *maybeArguments) {
addParam(ca);
thisAndArguments_.emplace_back(CallFunctionOnArgument(ca.objectId));
out_ << ", ";
}
}
}

void addParam(const m::runtime::CallArgument &ca) {
if (ca.objectId) {
out_ << "undefined";
} else if (ca.value) {
// TODO: this may throw if ca.value is a CBOR (see RFC 8949), but the
// chrome debugger doesn't seem to send those.
out_ << "(" << folly::toJson(*ca.value) << ")";
} else if (ca.unserializableValue) {
if (unserializableValueLooksLikeNumber(*ca.unserializableValue)) {
out_ << "+(" << *ca.unserializableValue << ")";
} else {
out_ << *ca.unserializableValue;
}
} else {
throw std::runtime_error("unknown payload for CallParam");
}
}

std::ostringstream out_;

std::vector<CallFunctionOnArgument> thisAndArguments_;
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
};

} // namespace

void Connection::Impl::handle(const m::runtime::CallFunctionOnRequest &req) {
std::string expression;
CallFunctionOnRunner runner;

auto validateAndParseRequest =
[&expression, &runner](const m::runtime::CallFunctionOnRequest &req)
-> folly::Optional<std::string> {
if (req.objectId.hasValue() == req.executionContextId.hasValue()) {
return std::string(
"The request must specify either object id or execution context id.");
}

if (!req.objectId) {
assert(
req.executionContextId &&
"should not be here if both object id and execution context id are missing");
if (*req.executionContextId != kHermesExecutionContextId) {
return "unknown execution context id " +
std::to_string(*req.executionContextId);
}
}

try {
std::tie(expression, runner) =
CallFunctionOnBuilder(req).expressionAndRunner();
} catch (const std::exception &e) {
return std::string(e.what());
}

return {};
};

if (auto errMsg = validateAndParseRequest(req)) {
sendErrorToClientViaExecutor(req.id, *errMsg);
return;
}

auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
inspector_
->evaluate(
0, // Top of the stackframe
expression,
[this,
remoteObjPtr,
objectGroup = req.objectGroup,
jsThisId = req.objectId,
executionContextId = req.executionContextId,
byValue = req.returnByValue.value_or(false),
runner =
std::move(runner)](const facebook::hermes::debugger::EvalResult
&evalResult) mutable {
if (evalResult.isException) {
return;
}

*remoteObjPtr = m::runtime::makeRemoteObject(
getRuntime(),
runner(getRuntime(), objTable_, evalResult),
objTable_,
objectGroup.value_or("ConsoleObjectGroup"),
byValue);
})
.via(executor_.get())
.thenValue(
[this, id = req.id, remoteObjPtr](debugger::EvalResult result) {
m::debugger::EvaluateOnCallFrameResponse resp;
resp.id = id;

if (result.isException) {
resp.exceptionDetails =
m::runtime::makeExceptionDetails(result.exceptionDetails);
} else {
resp.result = *remoteObjPtr;
}

sendResponseToClient(resp);
})
.thenError<std::exception>(sendErrorToClient(req.id));
}

void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) {
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();

Expand Down
Loading

0 comments on commit eb08af5

Please sign in to comment.