modify-jsi-features
Guía para agregar nueva funcionalidad JSI a la capa de Interfaz JavaScript (JSI). Úsala cuando el usuario solicite añadir, crear o implementar nuevos métodos o características en…
npx skills add https://github.com/facebook/hermes --skill modify-jsi-featuresAdding New JSI Functionality
When adding new functionality (methods) to the JSI Runtime interface, you must modify a specific set of files across multiple layers. This skill describes each file, the patterns to follow, and important conventions.
Architecture Overview
JSI (JavaScript Interface) is an abstraction layer that allows C++ code to interact with JavaScript runtimes. The architecture consists of:
- JSI Core (
xplat/jsi/jsi/) — The abstract interface definitions - Hermes Implementation (
xplat/static_h/API/hermes/) — Hermes-specific implementation - SynthTrace (
xplat/static_h/API/hermes/) — Recording and replay infrastructure for debugging
Checklist of Files to Modify
JSI Core Files
xplat/jsi/jsi/jsi.h— Add pure virtual method declaration toIRuntimeinterface AND override declaration inRuntimeclassxplat/jsi/jsi/jsi.cpp— Add default implementation (if providing one)xplat/jsi/jsi/jsi-inl.h— Add inline helper methods (if needed)xplat/jsi/jsi/decorator.h— Add method overrides toRuntimeDecoratorandWithRuntimeDecoratorxplat/jsi/jsi/test/testlib.cpp— Add tests for the JSI API
Hermes Implementation Files
xplat/static_h/API/hermes/hermes.cpp— Add Hermes-specific implementation inHermesRuntimeImplxplat/static_h/unittests/API/APITest.cpp— Add Hermes-specific tests
SynthTrace Files (Required for Hermes)
xplat/static_h/API/hermes/SynthTrace.h— Add new Record typesxplat/static_h/API/hermes/SynthTrace.cpp— Implement Record serializationxplat/static_h/API/hermes/TracingRuntime.h— Declare tracing method overridesxplat/static_h/API/hermes/TracingRuntime.cpp— Implement tracing logicxplat/static_h/API/hermes/SynthTraceParser.cpp— Add parsing for new recordsxplat/static_h/API/hermes/TraceInterpreter.cpp— Add replay logicxplat/static_h/unittests/API/SynthTraceTest.cpp— Add replay testsxplat/static_h/unittests/API/SynthTraceSerializationTest.cpp— Add serialization testsxplat/static_h/unittests/API/SynthTraceParserTest.cpp— Add parser tests
Implementation Strategy
When adding new features (methods) to the JSI Runtime, you have two options:
Option A: Default Implementation (Recommended)
Provide a default implementation in jsi::Runtime that works via JavaScript
calls. This allows all runtimes (JSC, V8, etc.) to work without modification.
// jsi.h - Add virtual method with default implementation
virtual void myNewMethod(const Object& obj, const Value& val);
// jsi.cpp - Implement default using JavaScript
void Runtime::myNewMethod(const Object& obj, const Value& val) {
auto myFn = global()
.getPropertyAsObject(*this, "Object")
.getPropertyAsFunction(*this, "myMethod");
myFn.call(*this, obj, val);
}
Option B: Pure Virtual (Requires All Runtime Implementations)
Make the method pure virtual, requiring all runtimes (JSCRuntime, V8Runtime, HermesRuntime) to implement it. Only use this when a JavaScript-based default is not possible.
Step-by-Step Guide
1. Add to jsi.h (JSI Core Interface)
Add the pure virtual method declaration to the IRuntime interface, and the override declaration to the Runtime class:
// In IRuntime interface (around line 580)
class JSI_EXPORT IRuntime : public ICast {
// ... existing methods ...
/// Brief description of what the method does.
/// \param obj Description of the object parameter.
/// \param val Description of the value parameter.
/// \return Description of return value (if any).
virtual void myNewMethod(const Object& obj, const Value& val) = 0;
// For methods returning Value, use this pattern:
virtual Value myNewGetter(const Object& obj) = 0;
};
// In Runtime class (around line 730) - add override declarations
class JSI_EXPORT Runtime : public IRuntime {
// ... existing methods ...
void myNewMethod(const Object& obj, const Value& val) override;
Value myNewGetter(const Object& obj) override;
};
Important: The IRuntime interface contains the pure virtual declarations (= 0),
while the Runtime class provides the default implementation with override keyword.
This separation allows other runtimes to implement the interface differently while
Runtime provides a JavaScript-based fallback.
If adding convenience methods to JSI types (like `Object`), add them too:
```cpp
class JSI_EXPORT Object : public Pointer {
// ... existing methods ...
/// Sets something on the object.
void setMyThing(IRuntime& runtime, const Value& val) const {
return runtime.myNewMethod(*this, val);
}
/// Gets something from the object.
inline Value getMyThing(Runtime& runtime) const;
};
2. Add Default Implementation to jsi.cpp
Implement the default using JavaScript APIs when possible:
void Runtime::myNewMethod(const Object& obj, const Value& val) {
// Get the JavaScript function
auto myMethodFn = global()
.getPropertyAsObject(*this, "Object")
.getPropertyAsFunction(*this, "myMethod");
// Call it with the appropriate arguments
myMethodFn.call(*this, obj, val);
}
Value Runtime::myNewGetter(const Object& obj) {
auto myGetterFn = global()
.getPropertyAsObject(*this, "Object")
.getPropertyAsFunction(*this, "myGetter");
return myGetterFn.call(*this, obj);
}
3. Add Inline Methods to jsi-inl.h (If Needed)
For methods that return incomplete types at declaration time:
Value Object::getMyThing(Runtime& runtime) const {
return runtime.myNewGetter(*this);
}
4. Update decorator.h
Add method overrides to both RuntimeDecorator and WithRuntimeDecorator:
// In RuntimeDecorator class (around line 282)
void myNewMethod(const Object& obj, const Value& val) override {
plain_.myNewMethod(obj, val);
}
Value myNewGetter(const Object& obj) override {
return plain_.myNewGetter(obj);
}
// In WithRuntimeDecorator class (around line 760)
void myNewMethod(const Object& obj, const Value& val) override {
Around around{with_};
RD::myNewMethod(obj, val);
}
Value myNewGetter(const Object& obj) override {
Around around{with_};
return RD::myNewGetter(obj);
}
5. Add JSI Core Tests in test/testlib.cpp
Test the default implementation using a RuntimeDecorator:
TEST_P(JSITest, MyNewFeature) {
// Use a RuntimeDecorator to test the default implementation
class RD : public RuntimeDecorator<Runtime, Runtime> {
public:
explicit RD(Runtime& rt) : RuntimeDecorator(rt) {}
void myNewMethod(const Object& obj, const Value& val) override {
return Runtime::myNewMethod(obj, val);
}
Value myNewGetter(const Object& obj) override {
return Runtime::myNewGetter(obj);
}
};
RD rd = RD(rt);
Object obj(rd);
// Test the functionality
obj.setMyThing(rd, Value(123));
EXPECT_EQ(obj.getMyThing(rd).getNumber(), 123);
}
6. Add Hermes Implementation in hermes.cpp
Implement the optimized version using Hermes VM APIs inside HermesRuntimeImpl:
// In HermesRuntimeImpl class declaration (around line 673)
void myNewMethod(const jsi::Object& obj, const jsi::Value& val) override;
jsi::Value myNewGetter(const jsi::Object& obj) override;
// Implementation (around line 2074)
void HermesRuntimeImpl::myNewMethod(
const jsi::Object& obj,
const jsi::Value& val) {
// Validate input if needed
if (!val.isObject() && !val.isNull()) {
throw jsi::JSError(*this, "Invalid argument type");
}
// Use Hermes VM APIs directly
auto cr = vm::JSObject::myVMMethod(
vm::vmcast<vm::JSObject>(phv(obj)),
runtime_,
hvFromValue(val),
vm::PropOpFlags().plusThrowOnError());
checkStatus(cr.getStatus());
}
jsi::Value HermesRuntimeImpl::myNewGetter(const jsi::Object& obj) {
vm::CallResult<vm::PseudoHandle<vm::JSObject>> cr =
vm::JSObject::myVMGetter(handle(obj), runtime_);
checkStatus(cr.getStatus());
if (!*cr) {
return jsi::Value::null();
}
return valueFromHermesValue(cr->getHermesValue());
}
7. Add Hermes Tests in APITest.cpp
TEST_P(HermesRuntimeTest, MyNewFeature) {
Object obj(*rt);
// Test normal operation
Value val(*rt, 123);
obj.setMyThing(*rt, val);
EXPECT_EQ(obj.getMyThing(*rt).getNumber(), 123);
// Test edge cases
obj.setMyThing(*rt, Value::null());
EXPECT_TRUE(obj.getMyThing(*rt).isNull());
// Test error cases
EXPECT_THROW(obj.setMyThing(*rt, Value("invalid")), JSError);
}
8. Add SynthTrace Record Types in SynthTrace.h
Add new record types for tracing:
// In the RECORD macro list (around line 210)
RECORD(MyNewMethod) \
RECORD(MyNewGetter) \
// Define the record structs (after existing records)
/// A MyNewMethodRecord is an event where native code calls myNewMethod
struct MyNewMethodRecord : public Record {
static constexpr RecordType type{RecordType::MyNewMethod};
const ObjectID objID_;
const TraceValue value_;
MyNewMethodRecord(TimeSinceStart time, ObjectID objID, TraceValue value)
: Record(time), objID_(objID), value_(value) {}
void toJSONInternal(::hermes::JSONEmitter &json) const override;
RecordType getType() const override {
return type;
}
std::vector<ObjectID> uses() const override {
std::vector<ObjectID> uses{objID_};
pushIfTrackedValue(value_, uses);
return uses;
}
};
/// A MyNewGetterRecord is an event where native code calls myNewGetter
struct MyNewGetterRecord : public Record {
static constexpr RecordType type{RecordType::MyNewGetter};
const ObjectID objID_;
MyNewGetterRecord(TimeSinceStart time, ObjectID objID)
: Record(time), objID_(objID) {}
void toJSONInternal(::hermes::JSONEmitter &json) const override;
RecordType getType() const override {
return type;
}
std::vector<ObjectID> uses() const override {
return {objID_};
}
};
9. Implement Record Serialization in SynthTrace.cpp
void SynthTrace::MyNewMethodRecord::toJSONInternal(JSONEmitter &json) const {
Record::toJSONInternal(json);
json.emitKeyValue("objID", objID_);
json.emitKeyValue("value", encode(value_));
}
void SynthTrace::MyNewGetterRecord::toJSONInternal(JSONEmitter &json) const {
Record::toJSONInternal(json);
json.emitKeyValue("objID", objID_);
}
10. Add Tracing Method Declarations in TracingRuntime.h
class TracingRuntime : public jsi::RuntimeDecorator<jsi::Runtime> {
// ... existing methods ...
void myNewMethod(const jsi::Object& obj, const jsi::Value& val) override;
jsi::Value myNewGetter(const jsi::Object& obj) override;
11. Implement Tracing Logic in TracingRuntime.cpp
void TracingRuntime::myNewMethod(
const jsi::Object& obj,
const jsi::Value& val) {
trace_.emplace_back<SynthTrace::MyNewMethodRecord>(
getTimeSinceStart(), useObjectID(obj), useTraceValue(val));
RD::myNewMethod(obj, val);
}
jsi::Value TracingRuntime::myNewGetter(const jsi::Object& obj) {
trace_.emplace_back<SynthTrace::MyNewGetterRecord>(
getTimeSinceStart(), useObjectID(obj));
auto result = RD::myNewGetter(obj);
trace_.emplace_back<SynthTrace::ReturnToNativeRecord>(
getTimeSinceStart(), defTraceValue(result));
return result;
}
12. Add Parsing in SynthTraceParser.cpp
case RecordType::MyNewMethod: {
trace.emplace_back<SynthTrace::MyNewMethodRecord>(
timeFromStart,
objID->getValue(),
SynthTrace::decode(propValue->c_str()));
break;
}
case RecordType::MyNewGetter:
trace.emplace_back<SynthTrace::MyNewGetterRecord>(
timeFromStart, objID->getValue());
break;
13. Add Replay Logic in TraceInterpreter.cpp
case RecordType::MyNewMethod: {
const auto &record =
static_cast<const SynthTrace::MyNewMethodRecord &>(*rec);
auto obj = getJSIValueForUse(record.objID_).getObject(rt_);
obj.setMyThing(rt_, traceValueToJSIValue(record.value_));
break;
}
case RecordType::MyNewGetter: {
const auto &record =
static_cast<const SynthTrace::MyNewGetterRecord &>(*rec);
auto obj = getJSIValueForUse(record.objID_).getObject(rt_);
auto result = obj.getMyThing(rt_);
retval = std::move(result);
break;
}
14. Add SynthTrace Tests
In SynthTraceSerializationTest.cpp:
TEST_F(SynthTraceSerializationTest, MyNewMethodTest) {
EXPECT_EQ(
R"({"type":"MyNewMethodRecord","time":0,"objID":1,"value":"null:"})",
to_string(SynthTrace::MyNewMethodRecord(
dummyTime, 1, SynthTrace::encodeNull())));
}
TEST_F(SynthTraceSerializationTest, MyNewGetterTest) {
EXPECT_EQ(
R"({"type":"MyNewGetterRecord","time":0,"objID":1})",
to_string(SynthTrace::MyNewGetterRecord(dummyTime, 1)));
}
In SynthTraceTest.cpp:
TEST_F(SynthTraceReplayTest, MyNewFeatureReplay) {
{
auto &rt = *traceRt;
jsi::Object obj(rt);
obj.setMyThing(rt, jsi::Value(123));
rt.global().setProperty(rt, "obj", obj);
auto result = obj.getMyThing(rt);
rt.global().setProperty(rt, "result", result);
}
replay();
{
auto &rt = *replayRt;
auto obj = rt.global().getProperty(rt, "obj").getObject(rt);
EXPECT_EQ(obj.getMyThing(rt).getNumber(), 123);
auto result = rt.global().getProperty(rt, "result");
EXPECT_EQ(result.getNumber(), 123);
}
}
In SynthTraceParserTest.cpp:
TEST_F(SynthTraceParserTest, ParseMyNewMethodRecord) {
const char *src = R"(
{
"version": 5,
"globalObjID": 258,
"runtimeConfig": {
"gcConfig": {
"initHeapSize": 33554432,
"maxHeapSize": 536870912
}
},
"trace": [
{
"type": "MyNewMethodRecord",
"time": 1234,
"objID": 1,
"value": "number:123"
}
]
}
)";
auto parseResult = parseSynthTrace(bufFromStr(src));
SynthTrace &trace = std::get<0>(parseResult);
auto record = dynamic_cast<const SynthTrace::MyNewMethodRecord &>(
*trace.records().at(0));
ASSERT_EQ(record.objID_, 1);
}
Key Conventions
-
Default Implementations: Always provide a default implementation in
jsi::Runtimewhen possible. This allows JSCRuntime and V8Runtime to work without modification. -
Hermes Optimization: For Hermes, override the default with an optimized implementation using Hermes VM APIs directly.
-
SynthTrace is Required: For Hermes, you MUST add SynthTrace support. SynthTrace allows recording and replaying of JSI interactions, which is critical for debugging and testing. Without it, replays will fail.
-
Consistent Naming: Use consistent naming across all files. If the method is
setPrototypeOfin JSI, useSetPrototypefor the record type name. -
Error Handling: Use
jsi::JSErrorfor JavaScript-level errors in JSI code. UsecheckStatus()for Hermes VM call results. -
Testing: Test at multiple levels:
- JSI core tests verify the abstract interface and default implementation
- Hermes API tests verify the optimized implementation
- SynthTrace tests verify recording, serialization, parsing, and replay
Testing
Running JSI Tests
To run all JSI tests defined in testlib.cpp:
buck test xplat/jsi:jsi
This runs the JSI test suite against multiple runtime implementations (Hermes, StaticHermes, etc.).
Running Hermes API Tests
To run the Hermes-specific API tests:
buck test xplat/static_h:HermesUnitTests
To run a specific test:
buck test xplat/static_h:HermesUnitTests -- MyTestName
Reference Diffs
For a complete example of adding JSI functionality, refer to these diffs:
- D66562549: JSI core changes for
getPrototypeOf/setPrototypeOf - D66729169: Hermes implementation
- D66792338: SynthTrace support
Note: These diffs predate the IRuntime interface, but the patterns remain the
same.