diff --git a/AUDIT.md b/AUDIT.md index 7c5b25f..2e4ca16 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -39,27 +39,44 @@ Architecture together, so if Android Discord is Bridgeless, iOS Discord almost certainly is too — meaning `RCTCxxBridge` is likely never instantiated on current Discord iOS builds, and the tweak's only injection point silently never fires. -**Fix applied (unverified):** added a second hook, `%hook RCTInstance` / -`-_loadScriptFromSource:`, the Bridgeless-mode equivalent, confirmed against React -Native's actual upstream source -(`ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm`). Both hooks -now share one injection pipeline (`runRevengeInjection`, factored out of the old -single hook body); Logos skips a `%hook` target gracefully if the class isn't -present at runtime, so shipping both is safe regardless of which architecture the -installed Discord build actually uses. +**This is now confirmed, not just theorized.** [RainTweak](https://codeberg.org/raincord/RainTweak) +is the iOS loader for `raincord/rain`, a sibling Revenge/Bunny-derived mod that is +*actively shipping* (last commit 2026-07-17, vs. this repo's Nov 2024). Its `%ctor` +explicitly checks `RCTNewArchEnabled` from Info.plist and refuses to run with +"Please update Discord to v332 or above" if it's off — i.e. current Discord iOS is +Bridgeless-only, exactly as the Android-side evidence predicted, and RainTweak has +real users validating this on-device. -Since `RCTSource` (the type `_loadScriptFromSource:` takes) has no public -initializer, `makeRCTSource()` (`Sources/Utils.m`) builds one by poking its private -ivars directly — the same technique RN's own private `RCTSourceCreate()` uses -internally. +**Fix applied**, ported from RainTweak's verified mechanism (better than the +`RCTInstance -_loadScriptFromSource:` + fake-`RCTSource` approach this fork +originally tried — see below for why that was replaced): -**This has not been run against a real device or the current Discord iOS binary.** -It's a best-effort port grounded in verified upstream RN source and the confirmed -Android hook target, not a tested fix. Needs a jailbreak + Xcode/Theos to validate; -if `_loadScriptFromSource:` turns out to have a different selector name in Discord's -actual build (obfuscation is unlikely for framework code, but app-specific -subclasses could differ), it'll need adjusting via class-dump/Frida against the -live binary. +- `%hook RCTHost` / `-instance:didInitializeRuntime:` (`Headers/RCTHost.h`, + `Sources/Tweak.xm`) — fires once the Bridgeless JS runtime is ready. Discord's own + bundle is left completely untouched via `%orig` first; Revenge's own scripts + (loader payload, downloaded bundle, theme, preloads) are then run directly against + the live `facebook::jsi::Runtime` via `[JSI evaluate:tag:runtime:]` + (`Headers/JSI.h` / `Sources/JSI.mm`, ported near-verbatim from RainTweak's + `JSI.mm`) instead of being smuggled in as fake bundle loads. +- This sidesteps the problem the first attempt ran into: `RCTSource` (what + `-_loadScriptFromSource:` takes) has no public initializer, so faking one meant + poking private ivars — fragile and, per RainTweak's actual production code, not + even the path they use. Evaluating directly against the runtime via JSI is both + simpler and how the proven-working reference implementation does it. +- The legacy `RCTCxxBridge -executeApplicationScript:...` hook is kept as a + no-cost fallback for any host still on the old bridge (Logos skips hooking + classes that aren't present at runtime, and a `didInject` guard stops both hooks + from double-firing if they somehow both resolve). +- `Headers/jsi/{jsi.h,jsi-inl.h,instrumentation.h}` are vendored copies of Meta's + JSI headers (MIT-licensed, same as RN itself) — Theos doesn't ship them, and + RainTweak vendors the identical files for the same reason. + +**Still unverified against a real device or the current Discord binary** — I don't +have a jailbreak or Xcode/Theos here. But unlike the first attempt, this is no +longer a guess extrapolated from RN's public source: the mechanism (`RCTHost` +hook + JSI evaluation) is the exact one running in a comparable, currently-shipping +mod. Confidence is meaningfully higher; it should still be tested before relying on +it, since Discord's specific build could in principle differ. ## Remaining feature gap vs. Android (not attempted here) diff --git a/Headers/JSI.h b/Headers/JSI.h new file mode 100644 index 0000000..27112b3 --- /dev/null +++ b/Headers/JSI.h @@ -0,0 +1,16 @@ +#pragma once + +#import +#import + +/** + * Minimal JSI helper: evaluates a script directly against a live `facebook::jsi::Runtime`, + * instead of re-entering React Native's own bundle-loading machinery. Ported from RainTweak + * (codeberg.org/raincord/RainTweak), an actively-maintained loader for a sibling Revenge/Bunny + * fork that verified this approach works on current (Bridgeless) Discord iOS builds. + */ +@interface JSI : NSObject + ++ (void)evaluate:(NSData *)scriptData tag:(NSString *)tag runtime:(facebook::jsi::Runtime &)runtime; + +@end diff --git a/Headers/RCTHost.h b/Headers/RCTHost.h new file mode 100644 index 0000000..9e5f1e3 --- /dev/null +++ b/Headers/RCTHost.h @@ -0,0 +1,15 @@ +#import +#import + +/** + * Forward declaration of React Native's Bridgeless-mode host class, just enough of it for Logos + * to hook `-instance:didInitializeRuntime:`. React Native ships no public header exposing this + * (it's an internal `ReactCommon/react/runtime` class), so the signature is declared by hand here - + * matches `RCTHost.h` in RainTweak (codeberg.org/raincord/RainTweak), which hooks the same method + * on a currently-shipping Discord iOS build. + */ +@interface RCTHost : NSObject + +- (void)instance:(id)instance didInitializeRuntime:(facebook::jsi::Runtime &)runtime; + +@end diff --git a/Headers/Utils.h b/Headers/Utils.h index 0946ad2..4879610 100644 --- a/Headers/Utils.h +++ b/Headers/Utils.h @@ -5,9 +5,5 @@ NSURL *getPyoncordDirectory(void); UIColor *hexToUIColor(NSString *hex); void showErrorAlert(NSString *title, NSString *message); -/** - * Builds an `RCTSource` (react/runtime's bridgeless script container) by poking its private - * ivars directly, since React Native doesn't expose a public initializer for it. - * Returns nil if the `RCTSource` class isn't present in the running binary. - */ -id makeRCTSource(NSURL *url, NSData *data); \ No newline at end of file +/** Detects the Hermes bytecode magic header ("Herm"), as opposed to plain-text JS. */ +BOOL isHermesBytecode(NSData *data); \ No newline at end of file diff --git a/Headers/jsi/instrumentation.h b/Headers/jsi/instrumentation.h new file mode 100644 index 0000000..4a88951 --- /dev/null +++ b/Headers/jsi/instrumentation.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace facebook { +namespace jsi { + +/// Methods for starting and collecting instrumentation, an \c Instrumentation +/// instance is associated with a particular \c Runtime instance, which it +/// controls the instrumentation of. +/// None of these functions should return newly created jsi values, nor should +/// it modify the values of any jsi values in the heap (although GCs are fine). +class JSI_EXPORT Instrumentation { + public: + /// Additional options controlling what to include when capturing a heap + /// snapshot. + struct HeapSnapshotOptions { + bool captureNumericValue{false}; + }; + + virtual ~Instrumentation() = default; + + /// Returns GC statistics as a JSON-encoded string, with an object containing + /// "type" and "version" fields outermost. "type" is a string, unique to a + /// particular implementation of \c jsi::Instrumentation, and "version" is a + /// number to indicate any revision to that implementation and its output + /// format. + /// + /// \pre This call can only be made on the instrumentation instance of a + /// runtime initialised to collect GC statistics. + /// + /// \post All cumulative measurements mentioned in the output are accumulated + /// across the entire lifetime of the Runtime. + /// + /// \return the GC statistics collected so far, as a JSON-encoded string. + virtual std::string getRecordedGCStats() = 0; + + /// Request statistics about the current state of the runtime's heap. This + /// function can be called at any time, and should produce information that is + /// correct at the instant it is called (i.e, not stale). + /// + /// \return a map from a string key to a number associated with that + /// statistic. + virtual std::unordered_map getHeapInfo( + bool includeExpensive) = 0; + + /// Perform a full garbage collection. + /// \param cause The cause of this collection, as it should be reported in + /// logs. + virtual void collectGarbage(std::string cause) = 0; + + /// A HeapStatsUpdate is a tuple of the fragment index, the number of objects + /// in that fragment, and the number of bytes used by those objects. + /// A "fragment" is a view of all objects allocated within a time slice. + using HeapStatsUpdate = std::tuple; + + /// Start capturing JS stack-traces for all JS heap allocated objects. These + /// can be accessed via \c ::createSnapshotToFile(). + /// \param fragmentCallback If present, invoke this callback every so often + /// with the most recently seen object ID, and a list of fragments that have + /// been updated. This callback will be invoked on the same thread that the + /// runtime is using. + virtual void startTrackingHeapObjectStackTraces( + std::function stats)> fragmentCallback) = 0; + + /// Stop capture JS stack-traces for JS heap allocated objects. + virtual void stopTrackingHeapObjectStackTraces() = 0; + + /// Start a heap sampling profiler that will sample heap allocations, and the + /// stack trace they were allocated at. Reports a summary of which functions + /// allocated the most. + /// \param samplingInterval The number of bytes allocated to wait between + /// samples. This will be used as the expected value of a poisson + /// distribution. + virtual void startHeapSampling(size_t samplingInterval) = 0; + + /// Turns off the heap sampling profiler previously enabled via + /// \c startHeapSampling. Writes the output of the sampling heap profiler to + /// \p os. The output is a JSON formatted string. + virtual void stopHeapSampling(std::ostream& os) = 0; + + /// Captures the heap to a file + /// + /// \param path to save the heap capture. + /// \param options additional options for what to capture. + virtual void createSnapshotToFile( + const std::string& path, + const HeapSnapshotOptions& options = {false}) = 0; + + /// Captures the heap to an output stream + /// + /// \param os output stream to write to. + /// \param options additional options for what to capture. + virtual void createSnapshotToStream( + std::ostream& os, + const HeapSnapshotOptions& options = {false}) = 0; + + /// If the runtime has been created to trace to a temp file, flush + /// any unwritten parts of the trace of bridge traffic to the file, + /// and return the name of the file. Otherwise, return the empty string. + /// Tracing is disabled after this call. + virtual std::string flushAndDisableBridgeTrafficTrace() = 0; + + /// Write basic block profile trace to the given file name. + virtual void writeBasicBlockProfileTraceToFile( + const std::string& fileName) const = 0; + + /// Write the opcode stats to the given stream. + virtual void dumpOpcodeStats(std::ostream& os) const = 0; + + /// Dump external profiler symbols to the given file name. + virtual void dumpProfilerSymbolsToFile(const std::string& fileName) const = 0; +}; + +} // namespace jsi +} // namespace facebook diff --git a/Headers/jsi/jsi-inl.h b/Headers/jsi/jsi-inl.h new file mode 100644 index 0000000..2f70a59 --- /dev/null +++ b/Headers/jsi/jsi-inl.h @@ -0,0 +1,405 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +namespace facebook { +namespace jsi { +namespace detail { + +inline Value toValue(Runtime&, std::nullptr_t) { + return Value::null(); +} +inline Value toValue(Runtime&, bool b) { + return Value(b); +} +inline Value toValue(Runtime&, double d) { + return Value(d); +} +inline Value toValue(Runtime&, float f) { + return Value(static_cast(f)); +} +inline Value toValue(Runtime&, int i) { + return Value(i); +} +inline Value toValue(Runtime& runtime, const char* str) { + return String::createFromAscii(runtime, str); +} +inline Value toValue(Runtime& runtime, const std::string& str) { + return String::createFromUtf8(runtime, str); +} +template +inline Value toValue(Runtime& runtime, const T& other) { + static_assert( + std::is_base_of::value, + "This type cannot be converted to Value"); + return Value(runtime, other); +} +inline Value toValue(Runtime& runtime, const Value& value) { + return Value(runtime, value); +} +inline Value&& toValue(Runtime&, Value&& value) { + return std::move(value); +} + +inline PropNameID toPropNameID(Runtime& runtime, const char* name) { + return PropNameID::forAscii(runtime, name); +} +inline PropNameID toPropNameID(Runtime& runtime, const std::string& name) { + return PropNameID::forUtf8(runtime, name); +} +inline PropNameID&& toPropNameID(Runtime&, PropNameID&& name) { + return std::move(name); +} + +/// Helper to throw while still compiling with exceptions turned off. +template +[[noreturn]] inline void throwOrDie(Args&&... args) { + std::rethrow_exception( + std::make_exception_ptr(E{std::forward(args)...})); +} + +} // namespace detail + +template +inline T Runtime::make(Runtime::PointerValue* pv) { + return T(pv); +} + +inline Runtime::PointerValue* Runtime::getPointerValue(jsi::Pointer& pointer) { + return pointer.ptr_; +} + +inline const Runtime::PointerValue* Runtime::getPointerValue( + const jsi::Pointer& pointer) { + return pointer.ptr_; +} + +inline const Runtime::PointerValue* Runtime::getPointerValue( + const jsi::Value& value) { + return value.data_.pointer.ptr_; +} + +inline void Runtime::setRuntimeData( + const UUID& uuid, + const std::shared_ptr& data) { + auto* dataPtr = new std::shared_ptr(data); + setRuntimeDataImpl(uuid, dataPtr, [](const void* data) { + delete (const std::shared_ptr*)data; + }); +} + +inline std::shared_ptr Runtime::getRuntimeData(const UUID& uuid) { + auto* data = (const std::shared_ptr*)getRuntimeDataImpl(uuid); + return data ? *data : nullptr; +} + +Value Object::getPrototype(Runtime& runtime) const { + return runtime.getPrototypeOf(*this); +} + +inline Value Object::getProperty(Runtime& runtime, const char* name) const { + return getProperty(runtime, String::createFromAscii(runtime, name)); +} + +inline Value Object::getProperty(Runtime& runtime, const String& name) const { + return runtime.getProperty(*this, name); +} + +inline Value Object::getProperty(Runtime& runtime, const PropNameID& name) + const { + return runtime.getProperty(*this, name); +} + +inline Value Object::getProperty(Runtime& runtime, const Value& name) const { + return runtime.getProperty(*this, name); +} + +inline bool Object::hasProperty(Runtime& runtime, const char* name) const { + return hasProperty(runtime, String::createFromAscii(runtime, name)); +} + +inline bool Object::hasProperty(Runtime& runtime, const String& name) const { + return runtime.hasProperty(*this, name); +} + +inline bool Object::hasProperty(Runtime& runtime, const PropNameID& name) + const { + return runtime.hasProperty(*this, name); +} + +inline bool Object::hasProperty(Runtime& runtime, const Value& name) const { + return runtime.hasProperty(*this, name); +} + +template +void Object::setProperty(Runtime& runtime, const char* name, T&& value) const { + setProperty( + runtime, String::createFromAscii(runtime, name), std::forward(value)); +} + +template +void Object::setProperty(Runtime& runtime, const String& name, T&& value) + const { + setPropertyValue( + runtime, name, detail::toValue(runtime, std::forward(value))); +} + +template +void Object::setProperty(Runtime& runtime, const PropNameID& name, T&& value) + const { + setPropertyValue( + runtime, name, detail::toValue(runtime, std::forward(value))); +} + +template +void Object::setProperty(Runtime& runtime, const Value& name, T&& value) const { + setPropertyValue( + runtime, name, detail::toValue(runtime, std::forward(value))); +} + +inline void Object::deleteProperty(Runtime& runtime, const char* name) const { + deleteProperty(runtime, String::createFromAscii(runtime, name)); +} + +inline void Object::deleteProperty(Runtime& runtime, const String& name) const { + runtime.deleteProperty(*this, name); +} + +inline void Object::deleteProperty(Runtime& runtime, const PropNameID& name) + const { + runtime.deleteProperty(*this, name); +} + +inline void Object::deleteProperty(Runtime& runtime, const Value& name) const { + runtime.deleteProperty(*this, name); +} + +inline Array Object::getArray(Runtime& runtime) const& { + assert(runtime.isArray(*this)); + (void)runtime; // when assert is disabled we need to mark this as used + return Array(runtime.cloneObject(ptr_)); +} + +inline Array Object::getArray(Runtime& runtime) && { + assert(runtime.isArray(*this)); + (void)runtime; // when assert is disabled we need to mark this as used + Runtime::PointerValue* value = ptr_; + ptr_ = nullptr; + return Array(value); +} + +inline ArrayBuffer Object::getArrayBuffer(Runtime& runtime) const& { + assert(runtime.isArrayBuffer(*this)); + (void)runtime; // when assert is disabled we need to mark this as used + return ArrayBuffer(runtime.cloneObject(ptr_)); +} + +inline ArrayBuffer Object::getArrayBuffer(Runtime& runtime) && { + assert(runtime.isArrayBuffer(*this)); + (void)runtime; // when assert is disabled we need to mark this as used + Runtime::PointerValue* value = ptr_; + ptr_ = nullptr; + return ArrayBuffer(value); +} + +inline Function Object::getFunction(Runtime& runtime) const& { + assert(runtime.isFunction(*this)); + return Function(runtime.cloneObject(ptr_)); +} + +inline Function Object::getFunction(Runtime& runtime) && { + assert(runtime.isFunction(*this)); + (void)runtime; // when assert is disabled we need to mark this as used + Runtime::PointerValue* value = ptr_; + ptr_ = nullptr; + return Function(value); +} + +template +inline bool Object::isHostObject(Runtime& runtime) const { + return runtime.isHostObject(*this) && + std::dynamic_pointer_cast(runtime.getHostObject(*this)); +} + +template <> +inline bool Object::isHostObject(Runtime& runtime) const { + return runtime.isHostObject(*this); +} + +template +inline std::shared_ptr Object::getHostObject(Runtime& runtime) const { + assert(isHostObject(runtime)); + return std::static_pointer_cast(runtime.getHostObject(*this)); +} + +template +inline std::shared_ptr Object::asHostObject(Runtime& runtime) const { + if (!isHostObject(runtime)) { + detail::throwOrDie( + "Object is not a HostObject of desired type"); + } + return std::static_pointer_cast(runtime.getHostObject(*this)); +} + +template <> +inline std::shared_ptr Object::getHostObject( + Runtime& runtime) const { + assert(runtime.isHostObject(*this)); + return runtime.getHostObject(*this); +} + +template +inline bool Object::hasNativeState(Runtime& runtime) const { + return runtime.hasNativeState(*this) && + std::dynamic_pointer_cast(runtime.getNativeState(*this)); +} + +template <> +inline bool Object::hasNativeState(Runtime& runtime) const { + return runtime.hasNativeState(*this); +} + +template +inline std::shared_ptr Object::getNativeState(Runtime& runtime) const { + assert(hasNativeState(runtime)); + return std::static_pointer_cast(runtime.getNativeState(*this)); +} + +inline void Object::setNativeState( + Runtime& runtime, + std::shared_ptr state) const { + runtime.setNativeState(*this, state); +} + +inline void Object::setExternalMemoryPressure(Runtime& runtime, size_t amt) + const { + runtime.setExternalMemoryPressure(*this, amt); +} + +inline Array Object::getPropertyNames(Runtime& runtime) const { + return runtime.getPropertyNames(*this); +} + +inline Value WeakObject::lock(Runtime& runtime) const { + return runtime.lockWeakObject(*this); +} + +template +void Array::setValueAtIndex(Runtime& runtime, size_t i, T&& value) const { + setValueAtIndexImpl( + runtime, i, detail::toValue(runtime, std::forward(value))); +} + +inline Value Array::getValueAtIndex(Runtime& runtime, size_t i) const { + return runtime.getValueAtIndex(*this, i); +} + +inline Function Function::createFromHostFunction( + Runtime& runtime, + const jsi::PropNameID& name, + unsigned int paramCount, + jsi::HostFunctionType func) { + return runtime.createFunctionFromHostFunction( + name, paramCount, std::move(func)); +} + +inline Value Function::call(Runtime& runtime, const Value* args, size_t count) + const { + return runtime.call(*this, Value::undefined(), args, count); +} + +inline Value Function::call(Runtime& runtime, std::initializer_list args) + const { + return call(runtime, args.begin(), args.size()); +} + +template +inline Value Function::call(Runtime& runtime, Args&&... args) const { + // A more awesome version of this would be able to create raw values + // which can be used directly without wrapping and unwrapping, but + // this will do for now. + return call(runtime, {detail::toValue(runtime, std::forward(args))...}); +} + +inline Value Function::callWithThis( + Runtime& runtime, + const Object& jsThis, + const Value* args, + size_t count) const { + return runtime.call(*this, Value(runtime, jsThis), args, count); +} + +inline Value Function::callWithThis( + Runtime& runtime, + const Object& jsThis, + std::initializer_list args) const { + return callWithThis(runtime, jsThis, args.begin(), args.size()); +} + +template +inline Value Function::callWithThis( + Runtime& runtime, + const Object& jsThis, + Args&&... args) const { + // A more awesome version of this would be able to create raw values + // which can be used directly without wrapping and unwrapping, but + // this will do for now. + return callWithThis( + runtime, jsThis, {detail::toValue(runtime, std::forward(args))...}); +} + +template +inline Array Array::createWithElements(Runtime& runtime, Args&&... args) { + return createWithElements( + runtime, {detail::toValue(runtime, std::forward(args))...}); +} + +template +inline std::vector PropNameID::names( + Runtime& runtime, + Args&&... args) { + return names({detail::toPropNameID(runtime, std::forward(args))...}); +} + +template +inline std::vector PropNameID::names( + PropNameID (&&propertyNames)[N]) { + std::vector result; + result.reserve(N); + for (auto& name : propertyNames) { + result.push_back(std::move(name)); + } + return result; +} + +inline Value Function::callAsConstructor( + Runtime& runtime, + const Value* args, + size_t count) const { + return runtime.callAsConstructor(*this, args, count); +} + +inline Value Function::callAsConstructor( + Runtime& runtime, + std::initializer_list args) const { + return callAsConstructor(runtime, args.begin(), args.size()); +} + +template +inline Value Function::callAsConstructor(Runtime& runtime, Args&&... args) + const { + return callAsConstructor( + runtime, {detail::toValue(runtime, std::forward(args))...}); +} + +String BigInt::toString(Runtime& runtime, int radix) const { + return runtime.bigintToString(*this, radix); +} + +} // namespace jsi +} // namespace facebook diff --git a/Headers/jsi/jsi.h b/Headers/jsi/jsi.h new file mode 100644 index 0000000..be27b95 --- /dev/null +++ b/Headers/jsi/jsi.h @@ -0,0 +1,1867 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#ifndef JSI_EXPORT +#ifdef _MSC_VER +#ifdef CREATE_SHARED_LIBRARY +#define JSI_EXPORT __declspec(dllexport) +#else +#define JSI_EXPORT +#endif // CREATE_SHARED_LIBRARY +#else // _MSC_VER +#define JSI_EXPORT __attribute__((visibility("default"))) +#endif // _MSC_VER +#endif // !defined(JSI_EXPORT) + +class FBJSRuntime; +namespace facebook { +namespace jsi { + +/// UUID version 1 implementation. This should be constructed with constant +/// arguments to identify fixed UUIDs. +class JSI_EXPORT UUID { + public: + // Construct from raw parts + constexpr UUID( + uint32_t timeLow, + uint16_t timeMid, + uint16_t timeHighAndVersion, + uint16_t variantAndClockSeq, + uint64_t node) + : high( + ((uint64_t)(timeLow) << 32) | ((uint64_t)(timeMid) << 16) | + ((uint64_t)(timeHighAndVersion))), + low(((uint64_t)(variantAndClockSeq) << 48) | node) {} + + // Default constructor (zero UUID) + constexpr UUID() : high(0), low(0) {} + + constexpr UUID(const UUID&) = default; + constexpr UUID& operator=(const UUID&) = default; + + constexpr bool operator==(const UUID& other) const { + return high == other.high && low == other.low; + } + constexpr bool operator!=(const UUID& other) const { + return !(*this == other); + } + + // Ordering (for std::map, sorting, etc.) + constexpr bool operator<(const UUID& other) const { + return (high < other.high) || (high == other.high && low < other.low); + } + + // Hash support for UUID (for unordered_map compatibility) + struct Hash { + std::size_t operator()(const UUID& uuid) const noexcept { + return std::hash{}(uuid.high) ^ + (std::hash{}(uuid.low) << 1); + } + }; + + // UUID format: 8-4-4-4-12 + std::string toString() const { + std::string buffer(36, ' '); + std::snprintf( + buffer.data(), + buffer.size() + 1, + "%08x-%04x-%04x-%04x-%012llx", + getTimeLow(), + getTimeMid(), + getTimeHighAndVersion(), + getVariantAndClockSeq(), + (unsigned long long)getNode()); + return buffer; + } + + constexpr uint32_t getTimeLow() const { + return (uint32_t)(high >> 32); + } + + constexpr uint16_t getTimeMid() const { + return (uint16_t)(high >> 16); + } + + constexpr uint16_t getTimeHighAndVersion() const { + return (uint16_t)high; + } + + constexpr uint16_t getVariantAndClockSeq() const { + return (uint16_t)(low >> 48); + } + + constexpr uint64_t getNode() const { + return low & 0xFFFFFFFFFFFF; + } + + private: + uint64_t high; + uint64_t low; +}; + +/// Base interface that all JSI interfaces inherit from. Users should not try to +/// manipulate this base type directly, and should use castInterface to get the +/// appropriate subtype. +struct JSI_EXPORT ICast { + /// If the current object can be cast into the interface specified by \p + /// interfaceUUID, return a pointer to the object. Otherwise, return a null + /// pointer. + /// The returned interface has the same lifetime as the underlying object. It + /// does not need to be released when not needed. + virtual ICast* castInterface(const UUID& interfaceUUID) = 0; + + protected: + /// Interfaces are not destructible, thus the destructor is intentionally + /// protected to prevent delete calls on the interface. + /// Additionally, the destructor is non-virtual to reduce the vtable + /// complexity from inheritance. + ~ICast() = default; +}; + +/// Base class for buffers of data or bytecode that need to be passed to the +/// runtime. The buffer is expected to be fully immutable, so the result of +/// size(), data(), and the contents of the pointer returned by data() must not +/// change after construction. +class JSI_EXPORT Buffer { + public: + virtual ~Buffer(); + virtual size_t size() const = 0; + virtual const uint8_t* data() const = 0; +}; + +class JSI_EXPORT StringBuffer : public Buffer { + public: + StringBuffer(std::string s) : s_(std::move(s)) {} + size_t size() const override { + return s_.size(); + } + const uint8_t* data() const override { + return reinterpret_cast(s_.data()); + } + + private: + std::string s_; +}; + +/// Base class for buffers of data that need to be passed to the runtime. The +/// result of size() and data() must not change after construction. However, the +/// region pointed to by data() may be modified by the user or the runtime. The +/// user must ensure that access to the contents of the buffer is properly +/// synchronised. +class JSI_EXPORT MutableBuffer { + public: + virtual ~MutableBuffer(); + virtual size_t size() const = 0; + virtual uint8_t* data() = 0; +}; + +/// PreparedJavaScript is a base class representing JavaScript which is in a +/// form optimized for execution, in a runtime-specific way. Construct one via +/// jsi::Runtime::prepareJavaScript(). +/// ** This is an experimental API that is subject to change. ** +class JSI_EXPORT PreparedJavaScript { + protected: + PreparedJavaScript() = default; + + public: + virtual ~PreparedJavaScript() = 0; +}; + +class Runtime; +class Pointer; +class PropNameID; +class Symbol; +class BigInt; +class String; +class Object; +class WeakObject; +class Array; +class ArrayBuffer; +class Function; +class Value; +class Instrumentation; +class Scope; +class JSIException; +class JSError; + +/// A function which has this type can be registered as a function +/// callable from JavaScript using Function::createFromHostFunction(). +/// When the function is called, args will point to the arguments, and +/// count will indicate how many arguments are passed. The function +/// can return a Value to the caller, or throw an exception. If a C++ +/// exception is thrown, a JS Error will be created and thrown into +/// JS; if the C++ exception extends std::exception, the Error's +/// message will be whatever what() returns. Note that it is undefined whether +/// HostFunctions may or may not be called in strict mode; that is `thisVal` +/// can be any value - it will not necessarily be coerced to an object or +/// or set to the global object. +using HostFunctionType = std::function< + Value(Runtime& rt, const Value& thisVal, const Value* args, size_t count)>; + +/// An object which implements this interface can be registered as an +/// Object with the JS runtime. +class JSI_EXPORT HostObject { + public: + // The C++ object's dtor will be called when the GC finalizes this + // object. (This may be as late as when the Runtime is shut down.) + // You have no control over which thread it is called on. This will + // be called from inside the GC, so it is unsafe to do any VM + // operations which require a Runtime&. Derived classes' dtors + // should also avoid doing anything expensive. Calling the dtor on + // a jsi object is explicitly ok. If you want to do JS operations, + // or any nontrivial work, you should add it to a work queue, and + // manage it externally. + virtual ~HostObject(); + + // When JS wants a property with a given name from the HostObject, + // it will call this method. If it throws an exception, the call + // will throw a JS \c Error object. By default this returns undefined. + // \return the value for the property. + virtual Value get(Runtime&, const PropNameID& name); + + // When JS wants to set a property with a given name on the HostObject, + // it will call this method. If it throws an exception, the call will + // throw a JS \c Error object. By default this throws a type error exception + // mimicking the behavior of a frozen object in strict mode. + virtual void set(Runtime&, const PropNameID& name, const Value& value); + + // When JS wants a list of property names for the HostObject, it will + // call this method. If it throws an exception, the call will throw a + // JS \c Error object. The default implementation returns empty vector. + virtual std::vector getPropertyNames(Runtime& rt); +}; + +/// Native state (and destructor) that can be attached to any JS object +/// using setNativeState. +class JSI_EXPORT NativeState { + public: + virtual ~NativeState(); +}; + +/// Represents a JS runtime. Movable, but not copyable. Note that +/// this object may not be thread-aware, but cannot be used safely from +/// multiple threads at once. The application is responsible for +/// ensuring that it is used safely. This could mean using the +/// Runtime from a single thread, using a mutex, doing all work on a +/// serial queue, etc. This restriction applies to the methods of +/// this class, and any method in the API which take a Runtime& as an +/// argument. Destructors (all but ~Scope), operators, or other methods +/// which do not take Runtime& as an argument are safe to call from any +/// thread, but it is still forbidden to make write operations on a single +/// instance of any class from more than one thread. In addition, to +/// make shutdown safe, destruction of objects associated with the Runtime +/// must be destroyed before the Runtime is destroyed, or from the +/// destructor of a managed HostObject or HostFunction. Informally, this +/// means that the main source of unsafe behavior is to hold a jsi object +/// in a non-Runtime-managed object, and not clean it up before the Runtime +/// is shut down. If your lifecycle is such that avoiding this is hard, +/// you will probably need to do use your own locks. +class JSI_EXPORT Runtime : public ICast { + public: + virtual ~Runtime(); + + ICast* castInterface(const UUID& interfaceUUID) override; + + /// Evaluates the given JavaScript \c buffer. \c sourceURL is used + /// to annotate the stack trace if there is an exception. The + /// contents may be utf8-encoded JS source code, or binary bytecode + /// whose format is specific to the implementation. If the input + /// format is unknown, or evaluation causes an error, a JSIException + /// will be thrown. + /// Note this function should ONLY be used when there isn't another means + /// through the JSI API. For example, it will be much slower to use this to + /// call a global function than using the JSI APIs to read the function + /// property from the global object and then calling it explicitly. + virtual Value evaluateJavaScript( + const std::shared_ptr& buffer, + const std::string& sourceURL) = 0; + + /// Prepares to evaluate the given JavaScript \c buffer by processing it into + /// a form optimized for execution. This may include pre-parsing, compiling, + /// etc. If the input is invalid (for example, cannot be parsed), a + /// JSIException will be thrown. The resulting object is tied to the + /// particular concrete type of Runtime from which it was created. It may be + /// used (via evaluatePreparedJavaScript) in any Runtime of the same concrete + /// type. + /// The PreparedJavaScript object may be passed to multiple VM instances, so + /// they can all share and benefit from the prepared script. + /// As with evaluateJavaScript(), using JavaScript code should be avoided + /// when the JSI API is sufficient. + virtual std::shared_ptr prepareJavaScript( + const std::shared_ptr& buffer, + std::string sourceURL) = 0; + + /// Evaluates a PreparedJavaScript. If evaluation causes an error, a + /// JSIException will be thrown. + /// As with evaluateJavaScript(), using JavaScript code should be avoided + /// when the JSI API is sufficient. + virtual Value evaluatePreparedJavaScript( + const std::shared_ptr& js) = 0; + + /// Queues a microtask in the JavaScript VM internal Microtask (a.k.a. Job in + /// ECMA262) queue, to be executed when the host drains microtasks in + /// its event loop implementation. + /// + /// \param callback a function to be executed as a microtask. + virtual void queueMicrotask(const jsi::Function& callback) = 0; + + /// Drain the JavaScript VM internal Microtask (a.k.a. Job in ECMA262) queue. + /// + /// \param maxMicrotasksHint a hint to tell an implementation that it should + /// make a best effort not execute more than the given number. It's default + /// to -1 for infinity (unbounded execution). + /// \return true if the queue is drained or false if there is more work to do. + /// + /// When there were exceptions thrown from the execution of microtasks, + /// implementations shall discard the exceptional jobs. An implementation may + /// \throw a \c JSError object to signal the hosts to handle. In that case, an + /// implementation may or may not suspend the draining. + /// + /// Hosts may call this function again to resume the draining if it was + /// suspended due to either exceptions or the \p maxMicrotasksHint bound. + /// E.g. a host may repetitively invoke this function until the queue is + /// drained to implement the "microtask checkpoint" defined in WHATWG HTML + /// event loop: https://html.spec.whatwg.org/C#perform-a-microtask-checkpoint. + /// + /// Note that error propagation is only a concern if a host needs to implement + /// `queueMicrotask`, a recent API that allows enqueueing arbitrary functions + /// (hence may throw) as microtasks. Exceptions from ECMA-262 Promise Jobs are + /// handled internally to VMs and are never propagated to hosts. + /// + /// This API offers some queue management to hosts at its best effort due to + /// different behaviors and limitations imposed by different VMs and APIs. By + /// the time this is written, An implementation may swallow exceptions (JSC), + /// may not pause (V8), and may not support bounded executions. + virtual bool drainMicrotasks(int maxMicrotasksHint = -1) = 0; + + /// \return the global object + virtual Object global() = 0; + + /// \return a short printable description of the instance. It should + /// at least include some human-readable indication of the runtime + /// implementation. This should only be used by logging, debugging, + /// and other developer-facing callers. + virtual std::string description() = 0; + + /// \return whether or not the underlying runtime supports debugging via the + /// Chrome remote debugging protocol. + /// + /// NOTE: the API for determining whether a runtime is debuggable and + /// registering a runtime with the debugger is still in flux, so please don't + /// use this API unless you know what you're doing. + virtual bool isInspectable() = 0; + + /// \return an interface to extract metrics from this \c Runtime. The default + /// implementation of this function returns an \c Instrumentation instance + /// which returns no metrics. + virtual Instrumentation& instrumentation(); + + /// Stores the pointer \p data with the \p uuid in the runtime. This can be + /// used to store some custom data within the runtime. When the runtime is + /// destroyed, or if an entry at an existing key is overwritten, the runtime + /// will release its ownership of the held object. + void setRuntimeData(const UUID& uuid, const std::shared_ptr& data); + + /// Returns the data associated with the \p uuid in the runtime. If there's no + /// data associated with the uuid, return a null pointer. + std::shared_ptr getRuntimeData(const UUID& uuid); + + protected: + friend class Pointer; + friend class PropNameID; + friend class Symbol; + friend class BigInt; + friend class String; + friend class Object; + friend class WeakObject; + friend class Array; + friend class ArrayBuffer; + friend class Function; + friend class Value; + friend class Scope; + friend class JSError; + + /// Stores the pointer \p data with the \p uuid in the runtime. This can be + /// used to store some custom data within the runtime. When the runtime is + /// destroyed, or if an entry at an existing key is overwritten, the runtime + /// will release its ownership by calling \p deleter. + virtual void setRuntimeDataImpl( + const UUID& uuid, + const void* data, + void (*deleter)(const void* data)); + + /// Returns the data associated with the \p uuid in the runtime. If there's no + /// data associated with the uuid, return a null pointer. + virtual const void* getRuntimeDataImpl(const UUID& uuid); + + // Potential optimization: avoid the cloneFoo() virtual dispatch, + // and instead just fix the number of fields, and copy them, since + // in practice they are trivially copyable. Sufficient use of + // rvalue arguments/methods would also reduce the number of clones. + + struct PointerValue { + virtual void invalidate() noexcept = 0; + + protected: + virtual ~PointerValue() = default; + }; + + virtual PointerValue* cloneSymbol(const Runtime::PointerValue* pv) = 0; + virtual PointerValue* cloneBigInt(const Runtime::PointerValue* pv) = 0; + virtual PointerValue* cloneString(const Runtime::PointerValue* pv) = 0; + virtual PointerValue* cloneObject(const Runtime::PointerValue* pv) = 0; + virtual PointerValue* clonePropNameID(const Runtime::PointerValue* pv) = 0; + + virtual PropNameID createPropNameIDFromAscii( + const char* str, + size_t length) = 0; + virtual PropNameID createPropNameIDFromUtf8( + const uint8_t* utf8, + size_t length) = 0; + virtual PropNameID createPropNameIDFromUtf16( + const char16_t* utf16, + size_t length); + virtual PropNameID createPropNameIDFromString(const String& str) = 0; + virtual PropNameID createPropNameIDFromSymbol(const Symbol& sym) = 0; + virtual std::string utf8(const PropNameID&) = 0; + virtual bool compare(const PropNameID&, const PropNameID&) = 0; + + virtual std::string symbolToString(const Symbol&) = 0; + + virtual BigInt createBigIntFromInt64(int64_t) = 0; + virtual BigInt createBigIntFromUint64(uint64_t) = 0; + virtual bool bigintIsInt64(const BigInt&) = 0; + virtual bool bigintIsUint64(const BigInt&) = 0; + virtual uint64_t truncate(const BigInt&) = 0; + virtual String bigintToString(const BigInt&, int) = 0; + + virtual String createStringFromAscii(const char* str, size_t length) = 0; + virtual String createStringFromUtf8(const uint8_t* utf8, size_t length) = 0; + virtual String createStringFromUtf16(const char16_t* utf16, size_t length); + virtual std::string utf8(const String&) = 0; + + // \return a \c Value created from a utf8-encoded JSON string. The default + // implementation creates a \c String and invokes JSON.parse. + virtual Value createValueFromJsonUtf8(const uint8_t* json, size_t length); + + virtual Object createObject() = 0; + virtual Object createObject(std::shared_ptr ho) = 0; + virtual std::shared_ptr getHostObject(const jsi::Object&) = 0; + virtual HostFunctionType& getHostFunction(const jsi::Function&) = 0; + + // Creates a new Object with the custom prototype + virtual Object createObjectWithPrototype(const Value& prototype); + + virtual bool hasNativeState(const jsi::Object&) = 0; + virtual std::shared_ptr getNativeState(const jsi::Object&) = 0; + virtual void setNativeState( + const jsi::Object&, + std::shared_ptr state) = 0; + + virtual void setPrototypeOf(const Object& object, const Value& prototype); + virtual Value getPrototypeOf(const Object& object); + + virtual Value getProperty(const Object&, const PropNameID& name) = 0; + virtual Value getProperty(const Object&, const String& name) = 0; + // Intentionally NON-virtual to match Discord's Hermes vtable, which lacks these + // slots. Making them virtual shifts createFunctionFromHostFunction and crashes + // host-function calls. Do not re-add `virtual` when re-syncing this header. + Value getProperty(const Object&, const Value& name); + virtual bool hasProperty(const Object&, const PropNameID& name) = 0; + virtual bool hasProperty(const Object&, const String& name) = 0; + bool hasProperty(const Object&, const Value& name); + virtual void setPropertyValue( + const Object&, + const PropNameID& name, + const Value& value) = 0; + virtual void + setPropertyValue(const Object&, const String& name, const Value& value) = 0; + void + setPropertyValue(const Object&, const Value& name, const Value& value); + + void deleteProperty(const Object&, const PropNameID& name); + void deleteProperty(const Object&, const String& name); + void deleteProperty(const Object&, const Value& name); + + virtual bool isArray(const Object&) const = 0; + virtual bool isArrayBuffer(const Object&) const = 0; + virtual bool isFunction(const Object&) const = 0; + virtual bool isHostObject(const jsi::Object&) const = 0; + virtual bool isHostFunction(const jsi::Function&) const = 0; + virtual Array getPropertyNames(const Object&) = 0; + + virtual WeakObject createWeakObject(const Object&) = 0; + virtual Value lockWeakObject(const WeakObject&) = 0; + + virtual Array createArray(size_t length) = 0; + virtual ArrayBuffer createArrayBuffer( + std::shared_ptr buffer) = 0; + virtual size_t size(const Array&) = 0; + virtual size_t size(const ArrayBuffer&) = 0; + virtual uint8_t* data(const ArrayBuffer&) = 0; + virtual Value getValueAtIndex(const Array&, size_t i) = 0; + virtual void + setValueAtIndexImpl(const Array&, size_t i, const Value& value) = 0; + + virtual Function createFunctionFromHostFunction( + const PropNameID& name, + unsigned int paramCount, + HostFunctionType func) = 0; + virtual Value call( + const Function&, + const Value& jsThis, + const Value* args, + size_t count) = 0; + virtual Value + callAsConstructor(const Function&, const Value* args, size_t count) = 0; + + // Private data for managing scopes. + struct ScopeState; + virtual ScopeState* pushScope(); + virtual void popScope(ScopeState*); + + virtual bool strictEquals(const Symbol& a, const Symbol& b) const = 0; + virtual bool strictEquals(const BigInt& a, const BigInt& b) const = 0; + virtual bool strictEquals(const String& a, const String& b) const = 0; + virtual bool strictEquals(const Object& a, const Object& b) const = 0; + + virtual bool instanceOf(const Object& o, const Function& f) = 0; + + /// See Object::setExternalMemoryPressure. + virtual void setExternalMemoryPressure( + const jsi::Object& obj, + size_t amount) = 0; + + virtual std::u16string utf16(const String& str); + virtual std::u16string utf16(const PropNameID& sym); + + /// Invokes the provided callback \p cb with the String content in \p str. + /// The callback must take in three arguments: bool ascii, const void* data, + /// and size_t num, respectively. \p ascii indicates whether the \p data + /// passed to the callback should be interpreted as a pointer to a sequence of + /// \p num ASCII characters or UTF16 characters. Depending on the internal + /// representation of the string, the function may invoke the callback + /// multiple times, with a different format on each invocation. The callback + /// must not access runtime functionality, as any operation on the runtime may + /// invalidate the data pointers. + virtual void getStringData( + const jsi::String& str, + void* ctx, + void (*cb)(void* ctx, bool ascii, const void* data, size_t num)); + + /// Invokes the provided callback \p cb with the PropNameID content in \p sym. + /// The callback must take in three arguments: bool ascii, const void* data, + /// and size_t num, respectively. \p ascii indicates whether the \p data + /// passed to the callback should be interpreted as a pointer to a sequence of + /// \p num ASCII characters or UTF16 characters. Depending on the internal + /// representation of the string, the function may invoke the callback + /// multiple times, with a different format on each invocation. The callback + /// must not access runtime functionality, as any operation on the runtime may + /// invalidate the data pointers. + virtual void getPropNameIdData( + const jsi::PropNameID& sym, + void* ctx, + void (*cb)(void* ctx, bool ascii, const void* data, size_t num)); + + // These exist so derived classes can access the private parts of + // Value, Symbol, String, and Object, which are all friends of Runtime. + template + static T make(PointerValue* pv); + static PointerValue* getPointerValue(Pointer& pointer); + static const PointerValue* getPointerValue(const Pointer& pointer); + static const PointerValue* getPointerValue(const Value& value); + + friend class ::FBJSRuntime; + template + friend class RuntimeDecorator; +}; + +// Base class for pointer-storing types. +class JSI_EXPORT Pointer { + protected: + explicit Pointer(Pointer&& other) noexcept : ptr_(other.ptr_) { + other.ptr_ = nullptr; + } + + ~Pointer() { + if (ptr_) { + ptr_->invalidate(); + } + } + + Pointer& operator=(Pointer&& other) noexcept; + + friend class Runtime; + friend class Value; + + explicit Pointer(Runtime::PointerValue* ptr) : ptr_(ptr) {} + + typename Runtime::PointerValue* ptr_; +}; + +/// Represents something that can be a JS property key. Movable, not copyable. +class JSI_EXPORT PropNameID : public Pointer { + public: + using Pointer::Pointer; + + PropNameID(Runtime& runtime, const PropNameID& other) + : Pointer(runtime.clonePropNameID(other.ptr_)) {} + + PropNameID(PropNameID&& other) = default; + PropNameID& operator=(PropNameID&& other) = default; + + /// Create a JS property name id from ascii values. The data is + /// copied. + static PropNameID forAscii(Runtime& runtime, const char* str, size_t length) { + return runtime.createPropNameIDFromAscii(str, length); + } + + /// Create a property name id from a nul-terminated C ascii name. The data is + /// copied. + static PropNameID forAscii(Runtime& runtime, const char* str) { + return forAscii(runtime, str, strlen(str)); + } + + /// Create a PropNameID from a C++ string. The string is copied. + static PropNameID forAscii(Runtime& runtime, const std::string& str) { + return forAscii(runtime, str.c_str(), str.size()); + } + + /// Create a PropNameID from utf8 values. The data is copied. + /// Results are undefined if \p utf8 contains invalid code points. + static PropNameID + forUtf8(Runtime& runtime, const uint8_t* utf8, size_t length) { + return runtime.createPropNameIDFromUtf8(utf8, length); + } + + /// Create a PropNameID from utf8-encoded octets stored in a + /// std::string. The string data is transformed and copied. + /// Results are undefined if \p utf8 contains invalid code points. + static PropNameID forUtf8(Runtime& runtime, const std::string& utf8) { + return runtime.createPropNameIDFromUtf8( + reinterpret_cast(utf8.data()), utf8.size()); + } + + /// Given a series of UTF-16 encoded code units, create a PropNameId. The + /// input may contain unpaired surrogates, which will be interpreted as a code + /// point of the same value. + static PropNameID + forUtf16(Runtime& runtime, const char16_t* utf16, size_t length) { + return runtime.createPropNameIDFromUtf16(utf16, length); + } + + /// Given a series of UTF-16 encoded code units stored inside std::u16string, + /// create a PropNameId. The input may contain unpaired surrogates, which + /// will be interpreted as a code point of the same value. + static PropNameID forUtf16(Runtime& runtime, const std::u16string& str) { + return runtime.createPropNameIDFromUtf16(str.data(), str.size()); + } + + /// Create a PropNameID from a JS string. + static PropNameID forString(Runtime& runtime, const jsi::String& str) { + return runtime.createPropNameIDFromString(str); + } + + /// Create a PropNameID from a JS symbol. + static PropNameID forSymbol(Runtime& runtime, const jsi::Symbol& sym) { + return runtime.createPropNameIDFromSymbol(sym); + } + + // Creates a vector of PropNameIDs constructed from given arguments. + template + static std::vector names(Runtime& runtime, Args&&... args); + + // Creates a vector of given PropNameIDs. + template + static std::vector names(PropNameID (&&propertyNames)[N]); + + /// Copies the data in a PropNameID as utf8 into a C++ string. + std::string utf8(Runtime& runtime) const { + return runtime.utf8(*this); + } + + /// Copies the data in a PropNameID as utf16 into a C++ string. + std::u16string utf16(Runtime& runtime) const { + return runtime.utf16(*this); + } + + /// Invokes the user provided callback to process the content in PropNameId. + /// The callback must take in three arguments: bool ascii, const void* data, + /// and size_t num, respectively. \p ascii indicates whether the \p data + /// passed to the callback should be interpreted as a pointer to a sequence of + /// \p num ASCII characters or UTF16 characters. The function may invoke the + /// callback multiple times, with a different format on each invocation. The + /// callback must not access runtime functionality, as any operation on the + /// runtime may invalidate the data pointers. + template + void getPropNameIdData(Runtime& runtime, CB& cb) const { + runtime.getPropNameIdData( + *this, &cb, [](void* ctx, bool ascii, const void* data, size_t num) { + (*((CB*)ctx))(ascii, data, num); + }); + } + + static bool compare( + Runtime& runtime, + const jsi::PropNameID& a, + const jsi::PropNameID& b) { + return runtime.compare(a, b); + } + + friend class Runtime; + friend class Value; +}; + +/// Represents a JS Symbol (es6). Movable, not copyable. +/// TODO T40778724: this is a limited implementation sufficient for +/// the debugger not to crash when a Symbol is a property in an Object +/// or element in an array. Complete support for creating will come +/// later. +class JSI_EXPORT Symbol : public Pointer { + public: + using Pointer::Pointer; + + Symbol(Symbol&& other) = default; + Symbol& operator=(Symbol&& other) = default; + + /// \return whether a and b refer to the same symbol. + static bool strictEquals(Runtime& runtime, const Symbol& a, const Symbol& b) { + return runtime.strictEquals(a, b); + } + + /// Converts a Symbol into a C++ string as JS .toString would. The output + /// will look like \c Symbol(description) . + std::string toString(Runtime& runtime) const { + return runtime.symbolToString(*this); + } + + friend class Runtime; + friend class Value; +}; + +/// Represents a JS BigInt. Movable, not copyable. +class JSI_EXPORT BigInt : public Pointer { + public: + using Pointer::Pointer; + + BigInt(BigInt&& other) = default; + BigInt& operator=(BigInt&& other) = default; + + /// Create a BigInt representing the signed 64-bit \p value. + static BigInt fromInt64(Runtime& runtime, int64_t value) { + return runtime.createBigIntFromInt64(value); + } + + /// Create a BigInt representing the unsigned 64-bit \p value. + static BigInt fromUint64(Runtime& runtime, uint64_t value) { + return runtime.createBigIntFromUint64(value); + } + + /// \return whether a === b. + static bool strictEquals(Runtime& runtime, const BigInt& a, const BigInt& b) { + return runtime.strictEquals(a, b); + } + + /// \returns This bigint truncated to a signed 64-bit integer. + int64_t getInt64(Runtime& runtime) const { + return runtime.truncate(*this); + } + + /// \returns Whether this bigint can be losslessly converted to int64_t. + bool isInt64(Runtime& runtime) const { + return runtime.bigintIsInt64(*this); + } + + /// \returns This bigint truncated to a signed 64-bit integer. Throws a + /// JSIException if the truncation is lossy. + int64_t asInt64(Runtime& runtime) const; + + /// \returns This bigint truncated to an unsigned 64-bit integer. + uint64_t getUint64(Runtime& runtime) const { + return runtime.truncate(*this); + } + + /// \returns Whether this bigint can be losslessly converted to uint64_t. + bool isUint64(Runtime& runtime) const { + return runtime.bigintIsUint64(*this); + } + + /// \returns This bigint truncated to an unsigned 64-bit integer. Throws a + /// JSIException if the truncation is lossy. + uint64_t asUint64(Runtime& runtime) const; + + /// \returns this BigInt converted to a String in base \p radix. Throws a + /// JSIException if radix is not in the [2, 36] range. + inline String toString(Runtime& runtime, int radix = 10) const; + + friend class Runtime; + friend class Value; +}; + +/// Represents a JS String. Movable, not copyable. +class JSI_EXPORT String : public Pointer { + public: + using Pointer::Pointer; + + String(String&& other) = default; + String& operator=(String&& other) = default; + + /// Create a JS string from ascii values. The string data is + /// copied. + static String + createFromAscii(Runtime& runtime, const char* str, size_t length) { + return runtime.createStringFromAscii(str, length); + } + + /// Create a JS string from a nul-terminated C ascii string. The + /// string data is copied. + static String createFromAscii(Runtime& runtime, const char* str) { + return createFromAscii(runtime, str, strlen(str)); + } + + /// Create a JS string from a C++ string. The string data is + /// copied. + static String createFromAscii(Runtime& runtime, const std::string& str) { + return createFromAscii(runtime, str.c_str(), str.size()); + } + + /// Create a JS string from utf8-encoded octets. The string data is + /// transformed and copied. Results are undefined if \p utf8 contains invalid + /// code points. + static String + createFromUtf8(Runtime& runtime, const uint8_t* utf8, size_t length) { + return runtime.createStringFromUtf8(utf8, length); + } + + /// Create a JS string from utf8-encoded octets stored in a + /// std::string. The string data is transformed and copied. Results are + /// undefined if \p utf8 contains invalid code points. + static String createFromUtf8(Runtime& runtime, const std::string& utf8) { + return runtime.createStringFromUtf8( + reinterpret_cast(utf8.data()), utf8.length()); + } + + /// Given a series of UTF-16 encoded code units, create a JS String. The input + /// may contain unpaired surrogates, which will be interpreted as a code point + /// of the same value. + static String + createFromUtf16(Runtime& runtime, const char16_t* utf16, size_t length) { + return runtime.createStringFromUtf16(utf16, length); + } + + /// Given a series of UTF-16 encoded code units stored inside std::u16string, + /// create a JS String. The input may contain unpaired surrogates, which will + /// be interpreted as a code point of the same value. + static String createFromUtf16(Runtime& runtime, const std::u16string& utf16) { + return runtime.createStringFromUtf16(utf16.data(), utf16.length()); + } + + /// \return whether a and b contain the same characters. + static bool strictEquals(Runtime& runtime, const String& a, const String& b) { + return runtime.strictEquals(a, b); + } + + /// Copies the data in a JS string as utf8 into a C++ string. + std::string utf8(Runtime& runtime) const { + return runtime.utf8(*this); + } + + /// Copies the data in a JS string as utf16 into a C++ string. + std::u16string utf16(Runtime& runtime) const { + return runtime.utf16(*this); + } + + /// Invokes the user provided callback to process content in String. The + /// callback must take in three arguments: bool ascii, const void* data, and + /// size_t num, respectively. \p ascii indicates whether the \p data passed to + /// the callback should be interpreted as a pointer to a sequence of \p num + /// ASCII characters or UTF16 characters. The function may invoke the callback + /// multiple times, with a different format on each invocation. The callback + /// must not access runtime functionality, as any operation on the runtime may + /// invalidate the data pointers. + template + void getStringData(Runtime& runtime, CB& cb) const { + runtime.getStringData( + *this, &cb, [](void* ctx, bool ascii, const void* data, size_t num) { + (*((CB*)ctx))(ascii, data, num); + }); + } + + friend class Runtime; + friend class Value; +}; + +class Array; +class Function; + +/// Represents a JS Object. Movable, not copyable. +class JSI_EXPORT Object : public Pointer { + public: + using Pointer::Pointer; + + Object(Object&& other) = default; + Object& operator=(Object&& other) = default; + + /// Creates a new Object instance, like '{}' in JS. + explicit Object(Runtime& runtime) : Object(runtime.createObject()) {} + + static Object createFromHostObject( + Runtime& runtime, + std::shared_ptr ho) { + return runtime.createObject(ho); + } + + /// Creates a new Object with the custom prototype + static Object create(Runtime& runtime, const Value& prototype) { + return runtime.createObjectWithPrototype(prototype); + } + + /// \return whether this and \c obj are the same JSObject or not. + static bool strictEquals(Runtime& runtime, const Object& a, const Object& b) { + return runtime.strictEquals(a, b); + } + + /// \return the result of `this instanceOf ctor` in JS. + bool instanceOf(Runtime& rt, const Function& ctor) const { + return rt.instanceOf(*this, ctor); + } + + /// Sets \p prototype as the prototype of the object. The prototype must be + /// either an Object or null. If the prototype was not set successfully, this + /// method will throw. + void setPrototype(Runtime& runtime, const Value& prototype) const { + return runtime.setPrototypeOf(*this, prototype); + } + + /// \return the prototype of the object + inline Value getPrototype(Runtime& runtime) const; + + /// \return the property of the object with the given ascii name. + /// If the name isn't a property on the object, returns the + /// undefined value. + Value getProperty(Runtime& runtime, const char* name) const; + + /// \return the property of the object with the String name. + /// If the name isn't a property on the object, returns the + /// undefined value. + Value getProperty(Runtime& runtime, const String& name) const; + + /// \return the property of the object with the given JS PropNameID + /// name. If the name isn't a property on the object, returns the + /// undefined value. + Value getProperty(Runtime& runtime, const PropNameID& name) const; + + /// \return the Property of the object with the given JS Value name. If the + /// name isn't a property on the object, returns the undefined value.This + /// attempts to convert the JS Value to convert to a property key. If the + /// conversion fails, this method may throw. + Value getProperty(Runtime& runtime, const Value& name) const; + + /// \return true if and only if the object has a property with the + /// given ascii name. + bool hasProperty(Runtime& runtime, const char* name) const; + + /// \return true if and only if the object has a property with the + /// given String name. + bool hasProperty(Runtime& runtime, const String& name) const; + + /// \return true if and only if the object has a property with the + /// given PropNameID name. + bool hasProperty(Runtime& runtime, const PropNameID& name) const; + + /// \return true if and only if the object has a property with the given + /// JS Value name. This attempts to convert the JS Value to convert to a + /// property key. If the conversion fails, this method may throw. + bool hasProperty(Runtime& runtime, const Value& name) const; + + /// Sets the property value from a Value or anything which can be + /// used to make one: nullptr_t, bool, double, int, const char*, + /// String, or Object. + template + void setProperty(Runtime& runtime, const char* name, T&& value) const; + + /// Sets the property value from a Value or anything which can be + /// used to make one: nullptr_t, bool, double, int, const char*, + /// String, or Object. + template + void setProperty(Runtime& runtime, const String& name, T&& value) const; + + /// Sets the property value from a Value or anything which can be + /// used to make one: nullptr_t, bool, double, int, const char*, + /// String, or Object. + template + void setProperty(Runtime& runtime, const PropNameID& name, T&& value) const; + + /// Sets the property value from a Value or anything which can be + /// used to make one: nullptr_t, bool, double, int, const char*, + /// String, or Object. This takes a JS Value as the property name, and + /// attempts to convert to a property key. If the conversion fails, this + /// method may throw. + template + void setProperty(Runtime& runtime, const Value& name, T&& value) const; + + /// Delete the property with the given ascii name. Throws if the deletion + /// failed. + void deleteProperty(Runtime& runtime, const char* name) const; + + /// Delete the property with the given String name. Throws if the deletion + /// failed. + void deleteProperty(Runtime& runtime, const String& name) const; + + /// Delete the property with the given PropNameID name. Throws if the deletion + /// failed. + void deleteProperty(Runtime& runtime, const PropNameID& name) const; + + /// Delete the property with the given Value name. Throws if the deletion + /// failed. + void deleteProperty(Runtime& runtime, const Value& name) const; + + /// \return true iff JS \c Array.isArray() would return \c true. If + /// so, then \c getArray() will succeed. + bool isArray(Runtime& runtime) const { + return runtime.isArray(*this); + } + + /// \return true iff the Object is an ArrayBuffer. If so, then \c + /// getArrayBuffer() will succeed. + bool isArrayBuffer(Runtime& runtime) const { + return runtime.isArrayBuffer(*this); + } + + /// \return true iff the Object is callable. If so, then \c + /// getFunction will succeed. + bool isFunction(Runtime& runtime) const { + return runtime.isFunction(*this); + } + + /// \return true iff the Object was initialized with \c createFromHostObject + /// and the HostObject passed is of type \c T. If returns \c true then + /// \c getHostObject will succeed. + template + bool isHostObject(Runtime& runtime) const; + + /// \return an Array instance which refers to the same underlying + /// object. If \c isArray() would return false, this will assert. + Array getArray(Runtime& runtime) const&; + + /// \return an Array instance which refers to the same underlying + /// object. If \c isArray() would return false, this will assert. + Array getArray(Runtime& runtime) &&; + + /// \return an Array instance which refers to the same underlying + /// object. If \c isArray() would return false, this will throw + /// JSIException. + Array asArray(Runtime& runtime) const&; + + /// \return an Array instance which refers to the same underlying + /// object. If \c isArray() would return false, this will throw + /// JSIException. + Array asArray(Runtime& runtime) &&; + + /// \return an ArrayBuffer instance which refers to the same underlying + /// object. If \c isArrayBuffer() would return false, this will assert. + ArrayBuffer getArrayBuffer(Runtime& runtime) const&; + + /// \return an ArrayBuffer instance which refers to the same underlying + /// object. If \c isArrayBuffer() would return false, this will assert. + ArrayBuffer getArrayBuffer(Runtime& runtime) &&; + + /// \return a Function instance which refers to the same underlying + /// object. If \c isFunction() would return false, this will assert. + Function getFunction(Runtime& runtime) const&; + + /// \return a Function instance which refers to the same underlying + /// object. If \c isFunction() would return false, this will assert. + Function getFunction(Runtime& runtime) &&; + + /// \return a Function instance which refers to the same underlying + /// object. If \c isFunction() would return false, this will throw + /// JSIException. + Function asFunction(Runtime& runtime) const&; + + /// \return a Function instance which refers to the same underlying + /// object. If \c isFunction() would return false, this will throw + /// JSIException. + Function asFunction(Runtime& runtime) &&; + + /// \return a shared_ptr which refers to the same underlying + /// \c HostObject that was used to create this object. If \c isHostObject + /// is false, this will assert. Note that this does a type check and will + /// assert if the underlying HostObject isn't of type \c T + template + std::shared_ptr getHostObject(Runtime& runtime) const; + + /// \return a shared_ptr which refers to the same underlying + /// \c HostObject that was used to create this object. If \c isHostObject + /// is false, this will throw. + template + std::shared_ptr asHostObject(Runtime& runtime) const; + + /// \return whether this object has native state of type T previously set by + /// \c setNativeState. + template + bool hasNativeState(Runtime& runtime) const; + + /// \return a shared_ptr to the state previously set by \c setNativeState. + /// If \c hasNativeState is false, this will assert. Note that this does a + /// type check and will assert if the native state isn't of type \c T + template + std::shared_ptr getNativeState(Runtime& runtime) const; + + /// Set the internal native state property of this object, overwriting any old + /// value. Creates a new shared_ptr to the object managed by \p state, which + /// will live until the value at this property becomes unreachable. + /// + /// Throws a type error if this object is a proxy or host object. + void setNativeState(Runtime& runtime, std::shared_ptr state) + const; + + /// \return same as \c getProperty(name).asObject(), except with + /// a better exception message. + Object getPropertyAsObject(Runtime& runtime, const char* name) const; + + /// \return similar to \c + /// getProperty(name).getObject().getFunction(), except it will + /// throw JSIException instead of asserting if the property is + /// not an object, or the object is not callable. + Function getPropertyAsFunction(Runtime& runtime, const char* name) const; + + /// \return an Array consisting of all enumerable property names in + /// the object and its prototype chain. All values in the return + /// will be isString(). (This is probably not optimal, but it + /// works. I only need it in one place.) + Array getPropertyNames(Runtime& runtime) const; + + /// Inform the runtime that there is additional memory associated with a given + /// JavaScript object that is not visible to the GC. This can be used if an + /// object is known to retain some native memory, and may be used to guide + /// decisions about when to run garbage collection. + /// This method may be invoked multiple times on an object, and subsequent + /// calls will overwrite any previously set value. Once the object is garbage + /// collected, the associated external memory will be considered freed and may + /// no longer factor into GC decisions. + void setExternalMemoryPressure(Runtime& runtime, size_t amt) const; + + protected: + void setPropertyValue( + Runtime& runtime, + const String& name, + const Value& value) const { + return runtime.setPropertyValue(*this, name, value); + } + + void setPropertyValue( + Runtime& runtime, + const PropNameID& name, + const Value& value) const { + return runtime.setPropertyValue(*this, name, value); + } + + void setPropertyValue(Runtime& runtime, const Value& name, const Value& value) + const { + return runtime.setPropertyValue(*this, name, value); + } + + friend class Runtime; + friend class Value; +}; + +/// Represents a weak reference to a JS Object. If the only reference +/// to an Object are these, the object is eligible for GC. Method +/// names are inspired by C++ weak_ptr. Movable, not copyable. +class JSI_EXPORT WeakObject : public Pointer { + public: + using Pointer::Pointer; + + WeakObject(WeakObject&& other) = default; + WeakObject& operator=(WeakObject&& other) = default; + + /// Create a WeakObject from an Object. + WeakObject(Runtime& runtime, const Object& o) + : WeakObject(runtime.createWeakObject(o)) {} + + /// \return a Value representing the underlying Object if it is still valid; + /// otherwise returns \c undefined. Note that this method has nothing to do + /// with threads or concurrency. The name is based on std::weak_ptr::lock() + /// which serves a similar purpose. + Value lock(Runtime& runtime) const; + + friend class Runtime; +}; + +/// Represents a JS Object which can be efficiently used as an array +/// with integral indices. +class JSI_EXPORT Array : public Object { + public: + Array(Array&&) = default; + /// Creates a new Array instance, with \c length undefined elements. + Array(Runtime& runtime, size_t length) : Array(runtime.createArray(length)) {} + + Array& operator=(Array&&) = default; + + /// \return the size of the Array, according to its length property. + /// (C++ naming convention) + size_t size(Runtime& runtime) const { + return runtime.size(*this); + } + + /// \return the size of the Array, according to its length property. + /// (JS naming convention) + size_t length(Runtime& runtime) const { + return size(runtime); + } + + /// \return the property of the array at index \c i. If there is no + /// such property, returns the undefined value. If \c i is out of + /// range [ 0..\c length ] throws a JSIException. + Value getValueAtIndex(Runtime& runtime, size_t i) const; + + /// Sets the property of the array at index \c i. The argument + /// value behaves as with Object::setProperty(). If \c i is out of + /// range [ 0..\c length ] throws a JSIException. + template + void setValueAtIndex(Runtime& runtime, size_t i, T&& value) const; + + /// There is no current API for changing the size of an array once + /// created. We'll probably need that eventually. + + /// Creates a new Array instance from provided values + template + static Array createWithElements(Runtime&, Args&&... args); + + /// Creates a new Array instance from initializer list. + static Array createWithElements( + Runtime& runtime, + std::initializer_list elements); + + private: + friend class Object; + friend class Value; + friend class Runtime; + + void setValueAtIndexImpl(Runtime& runtime, size_t i, const Value& value) + const { + return runtime.setValueAtIndexImpl(*this, i, value); + } + + Array(Runtime::PointerValue* value) : Object(value) {} +}; + +/// Represents a JSArrayBuffer +class JSI_EXPORT ArrayBuffer : public Object { + public: + ArrayBuffer(ArrayBuffer&&) = default; + ArrayBuffer& operator=(ArrayBuffer&&) = default; + + ArrayBuffer(Runtime& runtime, std::shared_ptr buffer) + : ArrayBuffer(runtime.createArrayBuffer(std::move(buffer))) {} + + /// \return the size of the ArrayBuffer storage. This is not affected by + /// overriding the byteLength property. + /// (C++ naming convention) + size_t size(Runtime& runtime) const { + return runtime.size(*this); + } + + size_t length(Runtime& runtime) const { + return runtime.size(*this); + } + + uint8_t* data(Runtime& runtime) const { + return runtime.data(*this); + } + + private: + friend class Object; + friend class Value; + friend class Runtime; + + ArrayBuffer(Runtime::PointerValue* value) : Object(value) {} +}; + +/// Represents a JS Object which is guaranteed to be Callable. +class JSI_EXPORT Function : public Object { + public: + Function(Function&&) = default; + Function& operator=(Function&&) = default; + + /// Create a function which, when invoked, calls C++ code. If the + /// function throws an exception, a JS Error will be created and + /// thrown. + /// \param name the name property for the function. + /// \param paramCount the length property for the function, which + /// may not be the number of arguments the function is passed. + /// \note The std::function's dtor will be called when the GC finalizes this + /// function. As with HostObject, this may be as late as when the Runtime is + /// shut down, and may occur on an arbitrary thread. If the function contains + /// any captured values, you are responsible for ensuring that their + /// destructors are safe to call on any thread. + static Function createFromHostFunction( + Runtime& runtime, + const jsi::PropNameID& name, + unsigned int paramCount, + jsi::HostFunctionType func); + + /// Calls the function with \c count \c args. The \c this value of the JS + /// function will not be set by the C++ caller, similar to calling + /// Function.prototype.apply(undefined, args) in JS. + /// \b Note: as with Function.prototype.apply, \c this may not always be + /// \c undefined in the function itself. If the function is non-strict, + /// \c this will be set to the global object. + Value call(Runtime& runtime, const Value* args, size_t count) const; + + /// Calls the function with a \c std::initializer_list of Value + /// arguments. The \c this value of the JS function will not be set by the + /// C++ caller, similar to calling Function.prototype.apply(undefined, args) + /// in JS. + /// \b Note: as with Function.prototype.apply, \c this may not always be + /// \c undefined in the function itself. If the function is non-strict, + /// \c this will be set to the global object. + Value call(Runtime& runtime, std::initializer_list args) const; + + /// Calls the function with any number of arguments similarly to + /// Object::setProperty(). The \c this value of the JS function will not be + /// set by the C++ caller, similar to calling + /// Function.prototype.call(undefined, ...args) in JS. + /// \b Note: as with Function.prototype.call, \c this may not always be + /// \c undefined in the function itself. If the function is non-strict, + /// \c this will be set to the global object. + template + Value call(Runtime& runtime, Args&&... args) const; + + /// Calls the function with \c count \c args and \c jsThis value passed + /// as the \c this value. + Value callWithThis( + Runtime& Runtime, + const Object& jsThis, + const Value* args, + size_t count) const; + + /// Calls the function with a \c std::initializer_list of Value + /// arguments and \c jsThis passed as the \c this value. + Value callWithThis( + Runtime& runtime, + const Object& jsThis, + std::initializer_list args) const; + + /// Calls the function with any number of arguments similarly to + /// Object::setProperty(), and with \c jsThis passed as the \c this value. + template + Value callWithThis(Runtime& runtime, const Object& jsThis, Args&&... args) + const; + + /// Calls the function as a constructor with \c count \c args. Equivalent + /// to calling `new Func` where `Func` is the js function reqresented by + /// this. + Value callAsConstructor(Runtime& runtime, const Value* args, size_t count) + const; + + /// Same as above `callAsConstructor`, except use an initializer_list to + /// supply the arguments. + Value callAsConstructor(Runtime& runtime, std::initializer_list args) + const; + + /// Same as above `callAsConstructor`, but automatically converts/wraps + /// any argument with a jsi Value. + template + Value callAsConstructor(Runtime& runtime, Args&&... args) const; + + /// Returns whether this was created with Function::createFromHostFunction. + /// If true then you can use getHostFunction to get the underlying + /// HostFunctionType. + bool isHostFunction(Runtime& runtime) const { + return runtime.isHostFunction(*this); + } + + /// Returns the underlying HostFunctionType iff isHostFunction returns true + /// and asserts otherwise. You can use this to use std::function<>::target + /// to get the object that was passed to create the HostFunctionType. + /// + /// Note: The reference returned is borrowed from the JS object underlying + /// \c this, and thus only lasts as long as the object underlying + /// \c this does. + HostFunctionType& getHostFunction(Runtime& runtime) const { + assert(isHostFunction(runtime)); + return runtime.getHostFunction(*this); + } + + private: + friend class Object; + friend class Value; + friend class Runtime; + + Function(Runtime::PointerValue* value) : Object(value) {} +}; + +/// Represents any JS Value (undefined, null, boolean, number, symbol, +/// string, or object). Movable, or explicitly copyable (has no copy +/// ctor). +class JSI_EXPORT Value { + public: + /// Default ctor creates an \c undefined JS value. + Value() noexcept : Value(UndefinedKind) {} + + /// Creates a \c null JS value. + /* implicit */ Value(std::nullptr_t) : kind_(NullKind) {} + + /// Creates a boolean JS value. + /* implicit */ Value(bool b) : Value(BooleanKind) { + data_.boolean = b; + } + + /// Creates a number JS value. + /* implicit */ Value(double d) : Value(NumberKind) { + data_.number = d; + } + + /// Creates a number JS value. + /* implicit */ Value(int i) : Value(NumberKind) { + data_.number = i; + } + + /// Moves a Symbol, String, or Object rvalue into a new JS value. + template < + typename T, + typename = std::enable_if_t< + std::is_base_of::value || + std::is_base_of::value || + std::is_base_of::value || + std::is_base_of::value>> + /* implicit */ Value(T&& other) : Value(kindOf(other)) { + new (&data_.pointer) T(std::move(other)); + } + + /// Value("foo") will treat foo as a bool. This makes doing that a + /// compile error. + template + Value(const char*) { + static_assert( + !std::is_same::value, + "Value cannot be constructed directly from const char*"); + } + + Value(Value&& other) noexcept; + + /// Copies a Symbol lvalue into a new JS value. + Value(Runtime& runtime, const Symbol& sym) : Value(SymbolKind) { + new (&data_.pointer) Symbol(runtime.cloneSymbol(sym.ptr_)); + } + + /// Copies a BigInt lvalue into a new JS value. + Value(Runtime& runtime, const BigInt& bigint) : Value(BigIntKind) { + new (&data_.pointer) BigInt(runtime.cloneBigInt(bigint.ptr_)); + } + + /// Copies a String lvalue into a new JS value. + Value(Runtime& runtime, const String& str) : Value(StringKind) { + new (&data_.pointer) String(runtime.cloneString(str.ptr_)); + } + + /// Copies a Object lvalue into a new JS value. + Value(Runtime& runtime, const Object& obj) : Value(ObjectKind) { + new (&data_.pointer) Object(runtime.cloneObject(obj.ptr_)); + } + + /// Creates a JS value from another Value lvalue. + Value(Runtime& runtime, const Value& value); + + /// Value(rt, "foo") will treat foo as a bool. This makes doing + /// that a compile error. + template + Value(Runtime&, const char*) { + static_assert( + !std::is_same::value, + "Value cannot be constructed directly from const char*"); + } + + ~Value(); + // \return the undefined \c Value. + static Value undefined() { + return Value(); + } + + // \return the null \c Value. + static Value null() { + return Value(nullptr); + } + + // \return a \c Value created from a utf8-encoded JSON string. + static Value + createFromJsonUtf8(Runtime& runtime, const uint8_t* json, size_t length) { + return runtime.createValueFromJsonUtf8(json, length); + } + + /// \return according to the Strict Equality Comparison algorithm, see: + /// https://262.ecma-international.org/11.0/#sec-strict-equality-comparison + static bool strictEquals(Runtime& runtime, const Value& a, const Value& b); + + Value& operator=(Value&& other) noexcept { + this->~Value(); + new (this) Value(std::move(other)); + return *this; + } + + bool isUndefined() const { + return kind_ == UndefinedKind; + } + + bool isNull() const { + return kind_ == NullKind; + } + + bool isBool() const { + return kind_ == BooleanKind; + } + + bool isNumber() const { + return kind_ == NumberKind; + } + + bool isString() const { + return kind_ == StringKind; + } + + bool isBigInt() const { + return kind_ == BigIntKind; + } + + bool isSymbol() const { + return kind_ == SymbolKind; + } + + bool isObject() const { + return kind_ == ObjectKind; + } + + /// \return the boolean value, or asserts if not a boolean. + bool getBool() const { + assert(isBool()); + return data_.boolean; + } + + /// \return the boolean value, or throws JSIException if not a + /// boolean. + bool asBool() const; + + /// \return the number value, or asserts if not a number. + double getNumber() const { + assert(isNumber()); + return data_.number; + } + + /// \return the number value, or throws JSIException if not a + /// number. + double asNumber() const; + + /// \return the Symbol value, or asserts if not a symbol. + Symbol getSymbol(Runtime& runtime) const& { + assert(isSymbol()); + return Symbol(runtime.cloneSymbol(data_.pointer.ptr_)); + } + + /// \return the Symbol value, or asserts if not a symbol. + /// Can be used on rvalue references to avoid cloning more symbols. + Symbol getSymbol(Runtime&) && { + assert(isSymbol()); + auto ptr = data_.pointer.ptr_; + data_.pointer.ptr_ = nullptr; + return static_cast(ptr); + } + + /// \return the Symbol value, or throws JSIException if not a + /// symbol + Symbol asSymbol(Runtime& runtime) const&; + Symbol asSymbol(Runtime& runtime) &&; + + /// \return the BigInt value, or asserts if not a bigint. + BigInt getBigInt(Runtime& runtime) const& { + assert(isBigInt()); + return BigInt(runtime.cloneBigInt(data_.pointer.ptr_)); + } + + /// \return the BigInt value, or asserts if not a bigint. + /// Can be used on rvalue references to avoid cloning more bigints. + BigInt getBigInt(Runtime&) && { + assert(isBigInt()); + auto ptr = data_.pointer.ptr_; + data_.pointer.ptr_ = nullptr; + return static_cast(ptr); + } + + /// \return the BigInt value, or throws JSIException if not a + /// bigint + BigInt asBigInt(Runtime& runtime) const&; + BigInt asBigInt(Runtime& runtime) &&; + + /// \return the String value, or asserts if not a string. + String getString(Runtime& runtime) const& { + assert(isString()); + return String(runtime.cloneString(data_.pointer.ptr_)); + } + + /// \return the String value, or asserts if not a string. + /// Can be used on rvalue references to avoid cloning more strings. + String getString(Runtime&) && { + assert(isString()); + auto ptr = data_.pointer.ptr_; + data_.pointer.ptr_ = nullptr; + return static_cast(ptr); + } + + /// \return the String value, or throws JSIException if not a + /// string. + String asString(Runtime& runtime) const&; + String asString(Runtime& runtime) &&; + + /// \return the Object value, or asserts if not an object. + Object getObject(Runtime& runtime) const& { + assert(isObject()); + return Object(runtime.cloneObject(data_.pointer.ptr_)); + } + + /// \return the Object value, or asserts if not an object. + /// Can be used on rvalue references to avoid cloning more objects. + Object getObject(Runtime&) && { + assert(isObject()); + auto ptr = data_.pointer.ptr_; + data_.pointer.ptr_ = nullptr; + return static_cast(ptr); + } + + /// \return the Object value, or throws JSIException if not an + /// object. + Object asObject(Runtime& runtime) const&; + Object asObject(Runtime& runtime) &&; + + // \return a String like JS .toString() would do. + String toString(Runtime& runtime) const; + + private: + friend class Runtime; + + enum ValueKind { + UndefinedKind, + NullKind, + BooleanKind, + NumberKind, + SymbolKind, + BigIntKind, + StringKind, + ObjectKind, + PointerKind = SymbolKind, + }; + + union Data { + // Value's ctor and dtor will manage the lifecycle of the contained Data. + Data() { + static_assert( + sizeof(Data) == sizeof(uint64_t), + "Value data should fit in a 64-bit register"); + } + ~Data() {} + + // scalars + bool boolean; + double number; + // pointers + Pointer pointer; // Symbol, String, Object, Array, Function + }; + + Value(ValueKind kind) : kind_(kind) {} + + constexpr static ValueKind kindOf(const Symbol&) { + return SymbolKind; + } + constexpr static ValueKind kindOf(const BigInt&) { + return BigIntKind; + } + constexpr static ValueKind kindOf(const String&) { + return StringKind; + } + constexpr static ValueKind kindOf(const Object&) { + return ObjectKind; + } + + ValueKind kind_; + Data data_; + + // In the future: Value becomes NaN-boxed. See T40538354. +}; + +/// Not movable and not copyable RAII marker advising the underlying +/// JavaScript VM to track resources allocated since creation until +/// destruction so that they can be recycled eagerly when the Scope +/// goes out of scope instead of floating in the air until the next +/// garbage collection or any other delayed release occurs. +/// +/// This API should be treated only as advice, implementations can +/// choose to ignore the fact that Scopes are created or destroyed. +/// +/// This class is an exception to the rule allowing destructors to be +/// called without proper synchronization (see Runtime documentation). +/// The whole point of this class is to enable all sorts of clean ups +/// when the destructor is called and this proper synchronization is +/// required at that time. +/// +/// Instances of this class are intended to be created as automatic stack +/// variables in which case destructor calls don't require any additional +/// locking, provided that the lock (if any) is managed with RAII helpers. +class JSI_EXPORT Scope { + public: + explicit Scope(Runtime& rt) : rt_(rt), prv_(rt.pushScope()) {} + ~Scope() { + rt_.popScope(prv_); + } + + Scope(const Scope&) = delete; + Scope(Scope&&) = delete; + + Scope& operator=(const Scope&) = delete; + Scope& operator=(Scope&&) = delete; + + template + static auto callInNewScope(Runtime& rt, F f) -> decltype(f()) { + Scope s(rt); + return f(); + } + + private: + Runtime& rt_; + Runtime::ScopeState* prv_; +}; + +/// Base class for jsi exceptions +class JSI_EXPORT JSIException : public std::exception { + protected: + JSIException() {} + JSIException(std::string what) : what_(std::move(what)) {} + + public: + JSIException(const JSIException&) = default; + + virtual const char* what() const noexcept override { + return what_.c_str(); + } + + virtual ~JSIException() override; + + protected: + std::string what_; +}; + +/// This exception will be thrown by API functions on errors not related to +/// JavaScript execution. +class JSI_EXPORT JSINativeException : public JSIException { + public: + JSINativeException(std::string what) : JSIException(std::move(what)) {} + + JSINativeException(const JSINativeException&) = default; + + virtual ~JSINativeException(); +}; + +/// This exception will be thrown by API functions whenever a JS +/// operation causes an exception as described by the spec, or as +/// otherwise described. +class JSI_EXPORT JSError : public JSIException { + public: + /// Creates a JSError referring to provided \c value + JSError(Runtime& r, Value&& value); + + /// Creates a JSError referring to new \c Error instance capturing current + /// JavaScript stack. The error message property is set to given \c message. + JSError(Runtime& rt, std::string message); + + /// Creates a JSError referring to new \c Error instance capturing current + /// JavaScript stack. The error message property is set to given \c message. + JSError(Runtime& rt, const char* message) + : JSError(rt, std::string(message)) {} + + /// Creates a JSError referring to a JavaScript Object having message and + /// stack properties set to provided values. + JSError(Runtime& rt, std::string message, std::string stack); + + /// Creates a JSError referring to provided value and what string + /// set to provided message. This argument order is a bit weird, + /// but necessary to avoid ambiguity with the above. + JSError(std::string what, Runtime& rt, Value&& value); + + /// Creates a JSError referring to the provided value, message and stack. This + /// constructor does not take a Runtime parameter, and therefore cannot result + /// in recursively invoking the JSError constructor. + JSError(Value&& value, std::string message, std::string stack); + + JSError(const JSError&) = default; + + virtual ~JSError(); + + const std::string& getStack() const { + return stack_; + } + + const std::string& getMessage() const { + return message_; + } + + const jsi::Value& value() const { + assert(value_); + return *value_; + } + + private: + // This initializes the value_ member and does some other + // validation, so it must be called by every branch through the + // constructors. + void setValue(Runtime& rt, Value&& value); + + // This needs to be on the heap, because throw requires the object + // be copyable, and Value is not. + std::shared_ptr value_; + std::string message_; + std::string stack_; +}; + +/// Helper function to cast the object pointed to by \p ptr into an interface +/// specified by \c U. If cast is successful, return a pointer to the object +/// as a raw pointer of \c U. Otherwise, return nullptr. +/// The returned interface same lifetime as the object referenced by \p ptr. +template +U* castInterface(T* ptr) { + if (ptr) { + return static_cast(ptr->castInterface(U::uuid)); + } + return nullptr; +} + +/// Helper function to cast the object managed by the shared_ptr \p ptr into an +/// interface specified by \c U. If the cast is successful, return a shared_ptr +/// of type \c U to the object. Otherwise, return an empty pointer. +/// The returned shared_ptr shares ownership of the object with \p ptr. +template +std::shared_ptr dynamicInterfaceCast(T&& ptr) { + auto* p = ptr->castInterface(U::uuid); + U* res = static_cast(p); + if (res) { + return std::shared_ptr(std::forward(ptr), res); + } + return nullptr; +} + +} // namespace jsi +} // namespace facebook + +#include diff --git a/Makefile b/Makefile index c8e6c8a..d1456e6 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,9 @@ include $(THEOS)/makefiles/common.mk TWEAK_NAME = Revenge BUNDLE_NAME = RevengeResources -Revenge_FILES = $(wildcard Sources/*.x Sources/*.m Sources/**/*.x Sources/**/*.m) +Revenge_FILES = $(wildcard Sources/*.x Sources/*.xm Sources/*.m Sources/*.mm Sources/**/*.x Sources/**/*.xm Sources/**/*.m Sources/**/*.mm) Revenge_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/Headers +Revenge_CCFLAGS = -std=c++17 -Wno-c++11-narrowing -I$(THEOS_PROJECT_DIR)/Headers Revenge_FRAMEWORKS = Foundation UIKit CoreGraphics CoreText CoreFoundation RevengeResources_INSTALL_PATH = "/Library/Application\ Support/" diff --git a/Sources/JSI.mm b/Sources/JSI.mm new file mode 100644 index 0000000..4ba5855 --- /dev/null +++ b/Sources/JSI.mm @@ -0,0 +1,49 @@ +#import "JSI.h" +#import "Logger.h" +#import "Utils.h" + +using namespace facebook; + +namespace { + +class NSDataBuffer : public jsi::Buffer { +public: + explicit NSDataBuffer(NSData *data) : data_(data) {} + + size_t size() const override { + return data_.length; + } + + const uint8_t *data() const override { + return static_cast(data_.bytes); + } + +private: + NSData *data_; +}; + +} + +@implementation JSI + ++ (void)evaluate:(NSData *)scriptData tag:(NSString *)tag runtime:(jsi::Runtime &)runtime { + if (scriptData.length == 0) return; + + try { + if (isHermesBytecode(scriptData)) { + auto buffer = std::make_shared(scriptData); + auto prepared = runtime.prepareJavaScript(buffer, std::string(tag.UTF8String)); + runtime.evaluatePreparedJavaScript(prepared); + } else { + std::string source((const char *)scriptData.bytes, scriptData.length); + auto buffer = std::make_shared(std::move(source)); + runtime.evaluateJavaScript(buffer, std::string(tag.UTF8String)); + } + } catch (const jsi::JSError &e) { + RevengeLog(@"[JSI] evaluate error: JSError for '%@': %s", tag, e.what()); + } catch (const std::exception &e) { + RevengeLog(@"[JSI] evaluate error: exception for '%@': %s", tag, e.what()); + } +} + +@end diff --git a/Sources/Tweak.x b/Sources/Tweak.xm similarity index 76% rename from Sources/Tweak.x rename to Sources/Tweak.xm index 8a7eb5c..2cbbde6 100644 --- a/Sources/Tweak.x +++ b/Sources/Tweak.xm @@ -1,27 +1,36 @@ #import #import +#import #import "Utils.h" #import "Logger.h" #import "Theme.h" #import "Fonts.h" #import "LoaderConfig.h" +#import "JSI.h" +#import "RCTHost.h" + +using namespace facebook; static NSURL *source; static BOOL isJailbroken; static NSString *revengePatchesBundlePath; static NSURL *pyoncordDirectory; static LoaderConfig *loaderConfig; +static BOOL didInject = NO; /** * Runs the full Revenge injection sequence (loader payload, downloaded bundle, stored theme, - * fonts, preloads) and hands each script off to `execute`, then finally executes the host's - * own `script`/`url`/`async` once everything else has run. + * fonts, preloads) and hands each script's data off to `execute`. * - * Factored out so it can be driven by either the legacy-bridge hook (RCTCxxBridge, used when - * the host app runs React Native's old bridge) or the bridgeless-mode hook (RCTInstance, used - * under React Native's New Architecture) below - both eventually funnel through here. + * Historically this fed straight into `RCTCxxBridge -executeApplicationScript:url:async:` + * (React Native's legacy bridge), which is what `execute` still does below for old-arch hosts. + * On current Discord iOS builds (Bridgeless / New Architecture, confirmed via RainTweak - + * codeberg.org/raincord/RainTweak, an actively-shipping loader for a sibling Revenge/Bunny fork + * that explicitly requires "Discord v332 or above" for Bridgeless), that hook point never fires + * at all, so `execute` is instead wired to `JSI evaluate:` against the live runtime - see the + * `RCTHost` hook below, which is the verified entry point. */ -static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^execute)(NSData *data, NSURL *url, BOOL async)) { +static BOOL runRevengeInjection(BOOL async, void (^execute)(NSData *data, BOOL async)) { NSBundle *revengePatchesBundle = [NSBundle bundleWithPath:revengePatchesBundlePath]; if (!revengePatchesBundle) { RevengeLog(@"Failed to load RevengePatches bundle from path: %@", revengePatchesBundlePath); @@ -38,7 +47,7 @@ static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^e NSData *patchData = [NSData dataWithContentsOfURL:patchPath]; RevengeLog(@"Injecting loader"); - execute(patchData, source, YES); + execute(patchData, YES); __block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]]; @@ -92,7 +101,7 @@ static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^e error:nil]; if (themeString) { NSString *jsCode = [NSString stringWithFormat:@"globalThis.__PYON_LOADER__.storedTheme=%@", themeString]; - execute([jsCode dataUsingEncoding:NSUTF8StringEncoding], source, async); + execute([jsCode dataUsingEncoding:NSUTF8StringEncoding], async); } NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]]; @@ -107,7 +116,7 @@ static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^e if (bundle) { RevengeLog(@"Executing JS bundle"); - execute(bundle, source, async); + execute(bundle, async); } NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"]; @@ -123,7 +132,7 @@ static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^e RevengeLog(@"Executing preload JS file %@", fileURL.absoluteString); NSData *data = [NSData dataWithContentsOfURL:fileURL]; if (data) { - execute(data, source, async); + execute(data, async); } } } @@ -135,52 +144,48 @@ static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^e return YES; } -%hook RCTCxxBridge +%hook RCTHost -// Legacy-bridge entry point. Fires when the host app runs React Native's old (non-Bridgeless) bridge. -- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async { - if (![url.absoluteString containsString:@"main.jsbundle"]) { - return %orig; - } +/** + * Bridgeless-mode entry point (React Native's New Architecture) - fires once the JS runtime is + * ready, letting Discord's own bundle load completely untouched via %orig first. Revenge's own + * scripts are then evaluated directly against the same runtime via JSI, rather than trying to + * masquerade as another RCTSource bundle load (which has no public constructor). This mirrors + * RainTweak's verified approach - see comment on `runRevengeInjection` above. + */ +- (void)instance:(id)instance didInitializeRuntime:(jsi::Runtime &)runtime { + %orig; - BOOL injected = runRevengeInjection(script, url, async, ^(NSData *data, NSURL *scriptUrl, BOOL scriptAsync) { - %orig(data, scriptUrl, scriptAsync); + if (didInject) return; + didInject = YES; + + runRevengeInjection(YES, ^(NSData *data, BOOL async) { + [JSI evaluate:data tag:@"revenge" runtime:runtime]; }); - if (!injected) return %orig; - - %orig(script, url, async); } %end -%hook RCTInstance +%hook RCTCxxBridge -/** - * Bridgeless-mode (React Native's New Architecture) entry point. Discord's Android build moved - * to this architecture (see RevengeXposed's `com.facebook.react.runtime.ReactInstance$loadJSBundle$1` - * and `BridgelessDevSupportManager` hook targets), and the JS bundle now pins react-native 0.81, - * which defaults to it too - so this is likely the entry point that actually fires on current - * Discord iOS builds, not `RCTCxxBridge` above. - * - * `RCTInstance` has no public initializer for `RCTSource`, so `makeRCTSource` pokes its private - * ivars directly to build synthetic sources for the loader payload/bundle/theme/preload scripts. - * UNVERIFIED against a real device/current Discord binary - test before shipping. - */ -- (void)_loadScriptFromSource:(id)source { - NSURL *url = [source valueForKey:@"url"]; - NSData *script = [source valueForKey:@"data"]; +// Legacy-bridge entry point. Fires when the host app runs React Native's old (non-Bridgeless) +// bridge. Kept for hosts that haven't moved to Bridgeless; harmless if RCTCxxBridge is never +// instantiated (Logos skips hooking classes that aren't present at runtime). +- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async { + if (![url.absoluteString containsString:@"main.jsbundle"] || didInject) { + return %orig; + } + didInject = YES; - if (![url.absoluteString containsString:@"main.jsbundle"]) { + BOOL injected = runRevengeInjection(async, ^(NSData *data, BOOL scriptAsync) { + %orig(data, source, scriptAsync); + }); + if (!injected) { + didInject = NO; return %orig; } - BOOL injected = runRevengeInjection(script, url, YES, ^(NSData *data, NSURL *scriptUrl, BOOL scriptAsync) { - id syntheticSource = makeRCTSource(scriptUrl, data); - if (syntheticSource) %orig(syntheticSource); - }); - if (!injected) return %orig; - - %orig(source); + %orig(script, url, async); } %end @@ -213,10 +218,10 @@ static BOOL runRevengeInjection(NSData *script, NSURL *url, BOOL async, void (^e RevengeLog(@"Bundle contents: %@", bundleContents); } -pyoncordDirectory = getPyoncordDirectory(); + pyoncordDirectory = getPyoncordDirectory(); loaderConfig = [[LoaderConfig alloc] init]; [loaderConfig loadConfig]; - + %init; } } diff --git a/Sources/Utils.m b/Sources/Utils.m index f24c2b7..574371a 100644 --- a/Sources/Utils.m +++ b/Sources/Utils.m @@ -1,35 +1,10 @@ #import "Utils.h" -#import -id makeRCTSource(NSURL *url, NSData *data) { - Class RCTSourceClass = NSClassFromString(@"RCTSource"); - if (!RCTSourceClass) return nil; - - id source = [RCTSourceClass new]; - - // RCTSource has no public initializer (see RCTJavaScriptLoader.mm's private - // RCTSourceCreate()), so its private ivars are poked directly instead. - Ivar urlIvar = class_getInstanceVariable(RCTSourceClass, "_url"); - Ivar dataIvar = class_getInstanceVariable(RCTSourceClass, "_data"); - Ivar lengthIvar = class_getInstanceVariable(RCTSourceClass, "_length"); - Ivar filesChangedCountIvar = class_getInstanceVariable(RCTSourceClass, "_filesChangedCount"); - - if (urlIvar) object_setIvar(source, urlIvar, url); - if (dataIvar) object_setIvar(source, dataIvar, data); - - // _length and _filesChangedCount are scalars (NSUInteger / NSInteger), not objects, - // so they need direct memory writes at the ivar's offset instead of object_setIvar. - if (lengthIvar) { - ptrdiff_t offset = ivar_getOffset(lengthIvar); - *(NSUInteger *)((uint8_t *)(__bridge void *)source + offset) = data.length; - } - if (filesChangedCountIvar) { - ptrdiff_t offset = ivar_getOffset(filesChangedCountIvar); - // -2 == RCTSourceFilesChangedCountNotBuiltByBundler, matching sources not built by Metro. - *(NSInteger *)((uint8_t *)(__bridge void *)source + offset) = -2; - } - - return source; +BOOL isHermesBytecode(NSData *data) { + if (!data || data.length < 4) return NO; + const uint8_t *bytes = (const uint8_t *)data.bytes; + // Hermes bytecode magic: "Herm" + return bytes[0] == 0x48 && bytes[1] == 0x65 && bytes[2] == 0x72 && bytes[3] == 0x6d; } NSURL *getPyoncordDirectory(void) {