modify-jsi-features

bởi facebook

Guide for adding new JSI functionality to the JavaScript Interface (JSI) layer. Use when the user asks to add, create, or implement new methods or features in…

npx skills add https://github.com/facebook/hermes --skill modify-jsi-features

Adding 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:

  1. JSI Core (xplat/jsi/jsi/) — The abstract interface definitions
  2. Hermes Implementation (xplat/static_h/API/hermes/) — Hermes-specific implementation
  3. SynthTrace (xplat/static_h/API/hermes/) — Recording and replay infrastructure for debugging

Checklist of Files to Modify

JSI Core Files

  1. xplat/jsi/jsi/jsi.h — Add pure virtual method declaration to IRuntime interface AND override declaration in Runtime class
  2. xplat/jsi/jsi/jsi.cpp — Add default implementation (if providing one)
  3. xplat/jsi/jsi/jsi-inl.h — Add inline helper methods (if needed)
  4. xplat/jsi/jsi/decorator.h — Add method overrides to RuntimeDecorator and WithRuntimeDecorator
  5. xplat/jsi/jsi/test/testlib.cpp — Add tests for the JSI API

Hermes Implementation Files

  1. xplat/static_h/API/hermes/hermes.cpp — Add Hermes-specific implementation in HermesRuntimeImpl
  2. xplat/static_h/unittests/API/APITest.cpp — Add Hermes-specific tests

SynthTrace Files (Required for Hermes)

  1. xplat/static_h/API/hermes/SynthTrace.h — Add new Record types
  2. xplat/static_h/API/hermes/SynthTrace.cpp — Implement Record serialization
  3. xplat/static_h/API/hermes/TracingRuntime.h — Declare tracing method overrides
  4. xplat/static_h/API/hermes/TracingRuntime.cpp — Implement tracing logic
  5. xplat/static_h/API/hermes/SynthTraceParser.cpp — Add parsing for new records
  6. xplat/static_h/API/hermes/TraceInterpreter.cpp — Add replay logic
  7. xplat/static_h/unittests/API/SynthTraceTest.cpp — Add replay tests
  8. xplat/static_h/unittests/API/SynthTraceSerializationTest.cpp — Add serialization tests
  9. xplat/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

  1. Default Implementations: Always provide a default implementation in jsi::Runtime when possible. This allows JSCRuntime and V8Runtime to work without modification.

  2. Hermes Optimization: For Hermes, override the default with an optimized implementation using Hermes VM APIs directly.

  3. 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.

  4. Consistent Naming: Use consistent naming across all files. If the method is setPrototypeOf in JSI, use SetPrototype for the record type name.

  5. Error Handling: Use jsi::JSError for JavaScript-level errors in JSI code. Use checkStatus() for Hermes VM call results.

  6. 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.

Thêm skills từ facebook

binary-size-analysis
facebook
Phân tích thay đổi kích thước tệp nhị phân theo từng commit của thư viện dùng chung hermesvm trong một phạm vi commit git. Tạo báo cáo markdown với kích thước theo từng commit và bảng tóm tắt các mức tăng giảm đáng kể.
official
non-interactive-git-rebase
facebook
Sử dụng khi cần sắp xếp lại, tách, bỏ hoặc sửa đổi các commit git không phải là commit trên cùng, mà không cần truy cập trình chỉnh sửa tương tác. Bao gồm rebase theo chương trình thông qua…
official
click-target
facebook
Find and click a target object in XR. Use when testing UI interactions, clicking buttons, or verifying interactable elements work correctly.
official
iwsdk-planner
facebook
IWSDK project planning and best practices guide. Use when planning new IWSDK features, designing systems/components, reviewing IWSDK code architecture, or when…
official
iwsdk-ui-panel
facebook
Develop and iterate on IWSDK UI panels efficiently. Use when working on PanelUI components, debugging UI layout, or improving UI design in IWSDK applications.
official
test-all
facebook
Parallel test orchestrator. Runs all 9 test suites concurrently via Task sub-agents and the iwsdk CLI. Handles build, example setup, dev servers, agent launch,…
official
test-audio
facebook
Test audio system (AudioSource loading, playback state, stop, spatial audio) against the audio example using the iwsdk CLI.
official
test-ecs-core
facebook
Test ECS core functionality (system registration, components, Transform sync, pause/step/resume, system toggle, entity discovery, snapshots) against the poke…
official