Skip to content

Make IRuntime public#56770

Closed
zoontek wants to merge 2 commits into
facebook:mainfrom
zoontek:public-jsi-iruntime
Closed

Make IRuntime public#56770
zoontek wants to merge 2 commits into
facebook:mainfrom
zoontek:public-jsi-iruntime

Conversation

@zoontek
Copy link
Copy Markdown
Contributor

@zoontek zoontek commented May 11, 2026

Summary:

jsi::IRuntime's destructor is declared protected:. Swift's C++ interop requires an accessible destructor before it will import a class, and that check runs before APINotes, so SwiftImportAs: reference cannot rescue the type, and every method taking IRuntime& (Value::getString, String::createFromUtf8, PropNameID::forUtf8, IRuntime::global, …) is unreachable from Swift.

Without this change, ExpoModulesJSI (Expo's Swift/C++ JSI wrapper, used by every Expo SDK module on Apple platforms) fails to build against RN 0.86.

Moving virtual ~IRuntime() = default; from protected: to public: is enough: it's already virtual, so there's no ABI or vtable change and Runtime's override stays valid — the destructor just becomes reachable, which is what Swift's importer needs.

Changelog:

[GENERAL] [CHANGED] - Make jsi::IRuntime's destructor public so the type is importable from Swift via C++ interop

Test Plan:

  • Built react-native on iOS — no ABI/vtable change since the destructor was already virtual, and Runtime's override still compiles.
  • Built ExpoModulesJSI against this change: Swift can now import IRuntime and call methods that take IRuntime& (previously rejected by Swift's C++ importer due to the inaccessible destructor).

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 11, 2026
@facebook-github-tools facebook-github-tools Bot added p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels May 11, 2026
@zoontek zoontek force-pushed the public-jsi-iruntime branch from 85a8df8 to a9e883b Compare May 11, 2026 15:37
@zoontek zoontek changed the title Make IRuntime destructor public Make IRuntime public May 11, 2026
@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented May 11, 2026

@javache has imported this pull request. If you are a Meta employee, you can view this in D104681153.

@tmikov
Copy link
Copy Markdown

tmikov commented May 11, 2026

(Comment generated by Claude Code with human supervision)

Please don't do this.

The premise — "Swift's importer won't accept this header, therefore the header is wrong" — has the direction of causality backwards. JSI is a stable, widely-consumed C++ public interface. Its I* types deliberately use a non-virtual protected: destructor to express, at the type-system level, that interfaces are not destructible by their consumers — only the concrete object that produced them owns its lifetime. That is a correct, intentional, and load-bearing piece of the design; the comment right above ~ICast() = default in jsi.h says so. The same shape is repeated across ICast, IRuntime, ISerialization, IHermes, IHermesSHUnit, and is the contract that every C++ consumer of JSI relies on.

Making ~IRuntime public to satisfy a single downstream language binding:

  1. Lets any C++ consumer write delete iruntime; against an interface they do not own — the compiler will no longer catch it.
  2. Teaches the Swift importer that IRuntime is a value type whose destructor Swift may call when a value goes out of scope, which is precisely the behavior the original design exists to forbid.
  3. Sets a precedent that the C++ public API is fair game whenever a non-C++ binding is inconvenient. The next non-C++ consumer will ask for the next concession.

A correct C++ public interface should not be weakened to accommodate a tooling limitation in another language, especially when that other language already provides the right escape hatch.

What to do instead

Swift's own diagnostic spells out the right answer: "does this type have reference semantics?" Yes — JSI interfaces are reference types, and Swift has a first-class import mode for exactly that case.

Best — zero C++ changes (Swift API Notes). Drop a JSI.apinotes file next to the modulemap; Swift's clang importer auto-discovers it. The C++ headers stay byte-identical:

# JSI.apinotes — sits next to module.modulemap
Name: JSI
Tags:
  - Name: ICast
    SwiftImportAs: reference
    SwiftReleaseOp: immortal
    SwiftRetainOp: immortal
  - Name: IRuntime
    SwiftImportAs: reference
    SwiftReleaseOp: immortal
    SwiftRetainOp: immortal
  # ... and one entry per other I* interface

Crucially, this file does not have to live in React Native. ExpoModulesJSI can ship its own apinotes overlay in its own build inputs and unblock itself today — zero coordination, zero risk to any other JSI consumer.

Second-best — non-invasive inline annotation. If for some reason apinotes are unworkable, a Swift-only attribute can be added inline. It is gated so non-Swift builds see nothing and the C++ semantics — including the protected destructor — are entirely unchanged:

#if __has_attribute(swift_attr)
#define JSI_SWIFT_REFERENCE                                \
  __attribute__((swift_attr("import_reference")))          \
  __attribute__((swift_attr("retain:immortal")))           \
  __attribute__((swift_attr("release:immortal")))
#else
#define JSI_SWIFT_REFERENCE
#endif

class JSI_SWIFT_REFERENCE IRuntime : public ICast { /* ... unchanged ... */ };

This still preserves the invariant — every C++ consumer keeps the compile-time protection, and Swift gets idiomatic reference-type semantics.

Verification

Both approaches verified end-to-end with Apple Swift 6.3 against a pristine mirror of the JSI hierarchy: Swift code calls runScript(rt, "...") and rt.castInterface(uuid) with no &/inout and no destructor exposure. Full standalone repro: https://github.com/tmikov/jsi-swift-apinotes-demo.

@zoontek
Copy link
Copy Markdown
Contributor Author

zoontek commented May 12, 2026

@tmikov I already updated Swift API Notes in expo/expo#45538, to add IRuntime, but unfortunately this is not enough:

❌  (../../packages/expo-modules-jsi/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift:12:92)

  10 |    Creates an instance of this type from the given `facebook.jsi.Value` in `facebook.jsi.IRuntime`.
  11 |    */
> 12 |   static func fromJSIValue(_ value: borrowing facebook.jsi.Value, in runtime: facebook.jsi.IRuntime) -> Self
     |                                                                                            ^ 'IRuntime' is not a member type of enum '__ObjC.facebook.jsi'
  13 |   /**
  14 |    Creates a JSI value representing this value in the given JSI runtime.
  15 |    */


❌  (../../packages/expo-modules-jsi/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift:16:44)

  14 |    Creates a JSI value representing this value in the given JSI runtime.
  15 |    */
> 16 |   func toJSIValue(in runtime: facebook.jsi.IRuntime) -> facebook.jsi.Value
     |                                            ^ 'IRuntime' is not a member type of enum '__ObjC.facebook.jsi'
  17 | }
  18 |
  19 | internal extension JSIRepresentable {
  
  …

@tmikov
Copy link
Copy Markdown

tmikov commented May 12, 2026

@tmikov I already updated Swift API Notes in expo/expo#45538, to add IRuntime, but unfortunately this is not enough:

Thanks for trying the apinotes route. We can't debug your Swift / ExpoModulesJSI build setup - that's downstream of JSI and outside the scope for us. The standalone demo I linked shows that SwiftImportAs: reference works for the JSI interface hierarchy with no C++ changes; reproducing your specific failure is on the ExpoModulesJSI side.

If you remain convinced that no Swift-side configuration can make this work, please produce a minimal standalone repro — fork the demo at https://github.com/tmikov/jsi-swift-apinotes-demo, modify it until it exhibits the failure, and link it here. Until then, the burden of proof for breaking a stable C++ public API has not been met.

The objection to this PR is unchanged: a correct C++ public interface should not be weakened to work around a downstream binding's build configuration.

@zoontek
Copy link
Copy Markdown
Contributor Author

zoontek commented May 13, 2026

@tmikov we investigated on our side, editing Swift API Notes indeed works (it was a cache issue), sorry about that.

@zoontek zoontek closed this May 13, 2026
@zoontek zoontek deleted the public-jsi-iruntime branch May 13, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants