modify-jsi-features

Leitfaden zum Hinzufügen neuer JSI-Funktionalität zur JavaScript Interface (JSI)-Ebene. Verwenden, wenn der Benutzer darum bittet, neue Methoden oder Funktionen in… hinzuzufügen, zu erstellen oder zu implementieren.

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.

Mehr Skills von facebook

binary-size-analysis
facebook
Analysiere die Änderungen der Binärgröße der hermesvm Shared Library pro Commit über einen Git-Commit-Bereich hinweg. Erstellt einen Markdown-Bericht mit Größenangaben pro Commit sowie zusammenfassenden Tabellen zu signifikanten Zunahmen und Abnahmen.
official
non-interactive-git-rebase
facebook
Verwenden, wenn Git-Commits, die nicht der oberste Commit sind, neu angeordnet, aufgeteilt, gelöscht oder geändert werden müssen, ohne Zugriff auf einen interaktiven Editor. Deckt programmatisches Rebase über… ab.
official
click-target
facebook
Finde und klicke ein Zielobjekt in XR an. Verwende dies beim Testen von UI-Interaktionen, Klicken von Schaltflächen oder Überprüfen, ob interaktive Elemente korrekt funktionieren.
official
iwsdk-planner
facebook
IWSDK-Projektplanungs- und Best-Practices-Leitfaden. Verwenden Sie diesen bei der Planung neuer IWSDK-Funktionen, beim Entwerfen von Systemen/Komponenten, beim Überprüfen der IWSDK-Codearchitektur oder wenn...
official
iwsdk-ui-panel
facebook
IWSDK-UI-Panels effizient entwickeln und iterieren. Verwenden Sie dies, wenn Sie an PanelUI-Komponenten arbeiten, das UI-Layout debuggen oder das UI-Design in IWSDK-Anwendungen verbessern.
official
test-all
facebook
Paralleler Test-Orchestrator. Führt alle 9 Test-Suiten gleichzeitig über Task-Sub-Agenten und die iwsdk CLI aus. Übernimmt Build, Beispiel-Setup, Entwicklungsserver, Agentenstart,…
official
test-audio
facebook
Teste das Audiosystem (AudioSource-Laden, Wiedergabestatus, Stopp, räumliches Audio) gegen das Audio-Beispiel mit der iwsdk CLI.
official
test-ecs-core
facebook
Teste die ECS-Kernfunktionalität (Systemregistrierung, Komponenten, Transform-Sync, Pause/Schritt/Fortsetzen, System-Umschaltung, Entitätserkennung, Snapshots) gegen den poke…
official