Skip to content

Commit

Permalink
Add ConsoleHost functions to allow Promise polyfilling.
Browse files Browse the repository at this point in the history
Summary:
The `Promise` polyfill requires `setTimeout` and `setImmediate`
to function properly. Add simple versions of these which simply
enqueue onto a job queue which is repeatedly drained until empty.

This allows us to make Hermes aware of `Promise` without changing
any actual functionality in any existing hosts (i.e. RN).

Note that because these functions are only set on ConsoleHost,
users such as RN still use their existing event loop and timer structure,
which allows us to simply swap to a Promise implementation that Hermes
is aware of without incurring any extra cost of changing code outside
of this one specific polyfill.

Reviewed By: tmikov

Differential Revision: D20225268

fbshipit-source-id: fd4a518cf7adaab6bf860f8d44b8190a866116af
  • Loading branch information
Huxpro authored and facebook-github-bot committed Oct 15, 2020
1 parent 5346754 commit 0812b40
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 6 deletions.
52 changes: 49 additions & 3 deletions include/hermes/ConsoleHost/ConsoleHost.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,60 @@
#include "hermes/CompilerDriver/CompilerDriver.h"
#include "hermes/ConsoleHost/MemorySizeParser.h"
#include "hermes/Public/RuntimeConfig.h"
#include "hermes/VM/Runtime.h"
#include "hermes/VM/instrumentation/StatSamplingThread.h"

#include "llvh/ADT/MapVector.h"

#include <memory>

namespace hermes {

namespace vm {
class Runtime;
}
/// Stores state used in certain NativeFunctions in the ConsoleHost.
/// `ConsoleHostContext *` can be passed as the ctx pointer to NativeFunctions.
/// In particular, it stores the job queue which powers setTimeout and friends,
/// which are only available by default in Hermes when using the ConsoleHost.
class ConsoleHostContext {
/// Queue of jobs that have been added via setTimeout and clearTimeout.
/// Values are marked by registering a custom roots function with the Runtime.
llvh::MapVector<uint32_t, vm::Callable *> queuedJobs_{};

/// Next job ID to be allocated by queueJob.
uint32_t nextJobId_{1};

public:
/// Registers the ConsoleHostContext roots with \p runtime.
ConsoleHostContext(vm::Runtime *runtime);

/// \return true when there are no queued jobs remaining.
bool jobsEmpty() const {
return queuedJobs_.empty();
}

/// Enqueue a job for setTimeout.
/// \return the ID of the job.
uint32_t queueJob(vm::PseudoHandle<vm::Callable> job) {
queuedJobs_.insert({nextJobId_, job.get()});
job.invalidate();
return nextJobId_++;
}

/// \param id the job to clear from the queue.
void clearJob(uint32_t id) {
queuedJobs_.erase(id);
}

/// Remove the first job from the queue.
/// \return the Callable which represents the queued job, None if empty queue.
llvh::Optional<vm::PseudoHandle<vm::Callable>> dequeueJob() {
if (queuedJobs_.empty())
return llvh::None;
vm::PseudoHandle<vm::Callable> result =
createPseudoHandle(queuedJobs_.front().second);
queuedJobs_.erase(queuedJobs_.begin());
return result;
}
};

/// Installs console host functions in Runtime \p runtime.
/// Host functions installed:
Expand All @@ -35,6 +80,7 @@ class Runtime;
/// Used to find the other segments to be loaded at runtime.
void installConsoleBindings(
vm::Runtime *runtime,
ConsoleHostContext &ctx,
vm::StatSamplingThread *statSampler = nullptr,
#ifdef HERMESVM_SERIALIZE
const std::string *serializePath = nullptr,
Expand Down
8 changes: 6 additions & 2 deletions include/hermes/VM/Handle.h
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,12 @@ inline PseudoHandle<> createPseudoHandle(HermesValue value) {
/// TODO(T40600161): when we next change LLVM version, see if this is
/// still necessary.
namespace llvh {
template <>
struct isPodLike<hermes::vm::PseudoHandle<hermes::vm::HermesValue>> {

// Instantiating Optional with a T "isPodLike" will result in a specialized
// OptionalStorage class without move ctor and would only copy T. Since the
// PseudoHandle is not copyable we spcialized the trait to be always false.
template <typename T>
struct isPodLike<hermes::vm::PseudoHandle<T>> {
static const bool value = false;
};
} // namespace llvh
Expand Down
93 changes: 93 additions & 0 deletions lib/ConsoleHost/ConsoleHost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@

namespace hermes {

ConsoleHostContext::ConsoleHostContext(vm::Runtime *runtime) {
runtime->addCustomRootsFunction([this](vm::GC *, vm::SlotAcceptor &acceptor) {
for (auto &entry : queuedJobs_) {
acceptor.acceptPtr(entry.second);
}
});
}

/// Raises an uncatchable quit exception.
static vm::CallResult<vm::HermesValue>
quit(void *, vm::Runtime *runtime, vm::NativeArgs) {
Expand Down Expand Up @@ -110,6 +118,32 @@ loadSegment(void *ctx, vm::Runtime *runtime, vm::NativeArgs args) {
return HermesValue::encodeUndefinedValue();
}

static vm::CallResult<vm::HermesValue>
setTimeout(void *ctx, vm::Runtime *runtime, vm::NativeArgs args) {
ConsoleHostContext *consoleHost = (ConsoleHostContext *)ctx;
using namespace hermes::vm;
Handle<Callable> callable = args.dyncastArg<Callable>(0);
if (!callable) {
return runtime->raiseTypeError("Argument to setTimeout must be a function");
}
CallResult<HermesValue> boundFunction = BoundFunction::create(
runtime, callable, args.getArgCount() - 1, args.begin() + 1);
uint32_t jobId = consoleHost->queueJob(
PseudoHandle<Callable>::vmcast(createPseudoHandle(*boundFunction)));
return HermesValue::encodeNumberValue(jobId);
}

static vm::CallResult<vm::HermesValue>
clearTimeout(void *ctx, vm::Runtime *runtime, vm::NativeArgs args) {
ConsoleHostContext *consoleHost = (ConsoleHostContext *)ctx;
using namespace hermes::vm;
if (!args.getArg(0).isNumber()) {
return runtime->raiseTypeError("Argument to clearTimeout must be a number");
}
consoleHost->clearJob(args.getArg(0).getNumberAs<uint32_t>());
return HermesValue::encodeUndefinedValue();
}

#ifdef HERMESVM_SERIALIZE
static std::vector<void *> getNativeFunctionPtrs();

Expand Down Expand Up @@ -178,12 +212,15 @@ static std::vector<void *> getNativeFunctionPtrs() {
res.push_back((void *)createHeapSnapshot);
res.push_back((void *)serializeVM);
res.push_back((void *)loadSegment);
res.push_back((void *)setTimeout);
res.push_back((void *)clearTimeout);
return res;
}
#endif

void installConsoleBindings(
vm::Runtime *runtime,
ConsoleHostContext &ctx,
vm::StatSamplingThread *statSampler,
#ifdef HERMESVM_SERIALIZE
const std::string *serializePath,
Expand Down Expand Up @@ -248,6 +285,41 @@ void installConsoleBindings(
loadSegment,
reinterpret_cast<void *>(const_cast<std::string *>(filename)),
2);

defineGlobalFunc(
runtime
->ignoreAllocationFailure(
runtime->getIdentifierTable().getSymbolHandle(
runtime, llvh::createASCIIRef("setTimeout")))
.get(),
setTimeout,
&ctx,
2);
defineGlobalFunc(
runtime
->ignoreAllocationFailure(
runtime->getIdentifierTable().getSymbolHandle(
runtime, llvh::createASCIIRef("clearTimeout")))
.get(),
clearTimeout,
&ctx,
1);

// Define `setImmediate` to be the same as `setTimeout` here.
// `setTimeout` doesn't use the time provided to it, and due to this
// being CLI code, we don't have an event loop.
// This allows the Promise polyfill to work enough for testing in the
// terminal, though other hosts should provide their own implementation of the
// event loop.
defineGlobalFunc(
runtime
->ignoreAllocationFailure(
runtime->getIdentifierTable().getSymbolHandle(
runtime, llvh::createASCIIRef("setImmediate")))
.get(),
setTimeout,
&ctx,
1);
}

// If a function body might throw C++ exceptions other than
Expand Down Expand Up @@ -349,8 +421,11 @@ bool executeHBCBytecodeImpl(
}

vm::GCScope scope(runtime.get());
ConsoleHostContext ctx{runtime.get()};

installConsoleBindings(
runtime.get(),
ctx,
statSampler.get(),
#ifdef HERMESVM_SERIALIZE
options.SerializeVMPath.empty() ? nullptr : &options.SerializeVMPath,
Expand Down Expand Up @@ -403,6 +478,24 @@ bool executeHBCBytecodeImpl(
llvh::errs(), runtime->makeHandle(runtime->getThrownValue()));
}

if (!threwException && !ctx.jobsEmpty()) {
vm::GCScopeMarkerRAII marker{scope};
// Run the jobs until there are no more.
vm::MutableHandle<vm::Callable> job{runtime.get()};
while (auto optJob = ctx.dequeueJob()) {
job = std::move(*optJob);
auto callRes = vm::Callable::executeCall0(
job, runtime.get(), vm::Runtime::getUndefinedValue(), false);
if (LLVM_UNLIKELY(callRes == vm::ExecutionStatus::EXCEPTION)) {
threwException = true;
llvh::outs().flush();
runtime->printException(
llvh::errs(), runtime->makeHandle(runtime->getThrownValue()));
break;
}
}
}

if (options.timeLimit > 0) {
vm::TimeLimitMonitor::getInstance().unwatchRuntime(runtime.get());
}
Expand Down
29 changes: 28 additions & 1 deletion tools/hermes/repl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ int repl(const vm::RuntimeConfig &config) {
auto runtime = vm::Runtime::create(config);

vm::GCScope gcScope(runtime.get());
installConsoleBindings(runtime.get());
ConsoleHostContext ctx{runtime.get()};
installConsoleBindings(runtime.get(), ctx);

std::string code;
code.reserve(256);
Expand Down Expand Up @@ -402,6 +403,8 @@ int repl(const vm::RuntimeConfig &config) {
// Ensure we don't keep accumulating handles.
vm::GCScopeMarkerRAII gcMarker{runtime.get()};

bool threwException = false;

if ((callRes = evaluateLineFn->executeCall2(
evaluateLineFn,
runtime.get(),
Expand All @@ -416,6 +419,30 @@ int repl(const vm::RuntimeConfig &config) {
runtime->makeHandle(runtime->getThrownValue()));
llvh::outs().resetColor();
code.clear();
threwException = true;
}

if (!ctx.jobsEmpty()) {
// Run the jobs until there are no more.
vm::MutableHandle<vm::Callable> job{runtime.get()};
while (auto optJob = ctx.dequeueJob()) {
job = std::move(*optJob);
auto callRes = vm::Callable::executeCall0(
job, runtime.get(), vm::Runtime::getUndefinedValue(), false);
if (LLVM_UNLIKELY(callRes == vm::ExecutionStatus::EXCEPTION)) {
threwException = true;
runtime->printException(
hasColors
? llvh::outs().changeColor(llvh::raw_ostream::Colors::RED)
: llvh::outs(),
runtime->makeHandle(runtime->getThrownValue()));
llvh::outs().resetColor();
code.clear();
}
}
}

if (threwException) {
continue;
}

Expand Down

0 comments on commit 0812b40

Please sign in to comment.