Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,66 @@ React Native already provides `console`, timers, `queueMicrotask`,
installs only the metadata globals, the object manager and the interop
callbacks. `Worker` is not installed either: a worker needs its own VM, which a
guest has no way to create.

## Windows

The same package runs on React Native for Windows (react-native-windows 0.83 or later, New
Architecture, Hermes). Where Apple platforms give you Objective-C, Windows gives you the whole
Windows Runtime, from NativeScript's Windows runtime
([@nativescript/windows-napi](https://github.com/NativeScript/windows/tree/main/windows-napi)).

```ts
import NativeScript from "@nativescript/react-native";

const device = await NativeScript.runOnUI(() => {
"worklet";
const info = Windows.System.Profile.AnalyticsInfo.VersionInfo;
return `${info.DeviceFamily} ${Windows.System.UserProfile.GlobalizationPreferences.HomeGeographicRegion}`;
});
```

### How it works

On Windows, NativeScript can't bind to React Native's own runtime: a native module only sees it
through react-native-windows' JSI ABI, and NativeScript's Windows runtime binds through Node-API.
react-native-worklets has no Windows implementation either. So the module runs a Hermes runtime
of its own on the UI thread, created through the `jsr_*` API of the `hermes.dll` react-native-windows
already loaded, with `@nativescript/windows-napi` attached to it. `Windows`, `XamlWindow` and
`NSWinRT` are globals there, as in NativeScript's Windows desktop hosts.

`NativeScript.runOnUI(worklet, ...args)` runs a worklet there and resolves with its result:

- The worklet is rebuilt from the code the Worklets Babel plugin embeds in it, so keep
`react-native-worklets/plugin` in your Babel config. No native Worklets library is needed on
Windows.
- Arguments, captured values and the result travel as JSON. A captured name that has no value in
React Native (`Windows`, `XamlWindow`, `NSWinRT`) is that runtime's global. Functions can't be
captured: define them inside the worklet.
- A worklet may be `async`: await WinRT async operations with `await NSWinRT.toPromise(op)`.
- `console` output from the worklet is replayed through React Native's `console` when it returns,
and a thrown error rejects the promise with its stack.

`NativeScript.init()` starts that runtime; NativeScript's globals don't exist in React Native's
own runtime on Windows.

### Setup

Add `@nativescript/windows-napi` (1.0.0-beta.22 or later, the first that can run outside Node) next
to this package. Autolinking adds the `NativeScriptNativeApi` project, which embeds the addon and
its JS helpers (`nswinrt.js`) in its DLL and extracts the addon to
`%LOCALAPPDATA%\nativescript-react-native\` on first use, so it works however the app is packaged.
To embed another build of the addon, pass `/p:NativeScriptWindowsNapiAddon=<path to the .node or
.dll>` and `/p:NativeScriptWindowsNapiJs=<path to nswinrt.js>` to MSBuild.

Keep `react-native-worklets/plugin` in your Babel config. On Windows the package declares
`Windows`, `NSWinRT` and `XamlWindow` in React Native's runtime (as `undefined`) when it's imported,
so a worklet that uses them can capture them; they resolve to the UI runtime's globals.

### How Node-API gets there

`@nativescript/windows-napi` is a Node-API addon. Hermes' `hermes.dll` provides the engine half of
Node-API (`js_native_api.h`) but not Node's (`node_api.h`: threadsafe functions, cleanup hooks,
buffers, async work), which the addon also uses. The module implements Node's half itself
(`windows/NativeScriptNativeApi/NodeApiHost.cpp`) and sets `NAPI_HOST_MODULES` to its own DLL and
`hermes.dll` before loading the addon, which takes each `napi_*` function from the first of them
that exports it.
4 changes: 3 additions & 1 deletion packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"JSI",
"iOS",
"Android",
"Java"
"Java",
"Windows"
],
"repository": {
"type": "git",
Expand Down Expand Up @@ -38,6 +39,7 @@
"types",
"android",
"ios",
"windows",
"metadata",
"native-api",
"NativeScriptNativeApi.podspec",
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
} from "react";
import type { ViewProps } from "react-native";
import NativeScriptNativeApi from "./NativeScriptNativeApi";
import { initWindows, isWindows, runOnUIWindows } from "./windows";
import NativeScriptUIViewNativeComponent from "./NativeScriptUIViewNativeComponent";

declare const require: (id: string) => any;
Expand Down Expand Up @@ -984,6 +985,8 @@ export function installGlobals(): boolean {
}

export function init(metadataPath = "", options: InstallOptions = {}): boolean {
// Windows: the Windows Runtime lives in the UI-thread runtime, reached through runOnUI.
if (isWindows) return initWindows();
const installed =
NativeScriptNativeApi.isInstalled() ||
NativeScriptNativeApi.install(metadataPath);
Expand Down Expand Up @@ -1344,6 +1347,7 @@ export function runOnUI<Args extends unknown[], ReturnValue>(
if (typeof callback !== "function") {
throw new TypeError("NativeScript.runOnUI expects a Worklets callback");
}
if (isWindows) return runOnUIWindows(callback, args);

ensureNativeScriptInstalled();
const worklets = ensureWorkletsInstalled();
Expand Down
88 changes: 88 additions & 0 deletions packages/react-native/src/windows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Platform } from "react-native";
import NativeScriptNativeApi from "./NativeScriptNativeApi";

/**
* React Native for Windows. NativeScript runs in a Hermes runtime of its own on
* the UI thread (see windows/NativeScriptNativeApi/UIRuntime.h): React Native's
* runtime can't host it there, and react-native-worklets has no Windows
* implementation. Worklets passed to runOnUI run in that runtime, rebuilt from
* the code the Worklets Babel plugin embeds in each one; their arguments,
* captured values and results travel as JSON.
*/
export const isWindows = Platform.OS === "windows";

/** The UI runtime's globals (UIRuntime.cpp's prelude). */
const UI_RUNTIME_GLOBALS = ["Windows", "NSWinRT", "XamlWindow"];

// A worklet that uses Windows captures it like any outside name, so making the
// worklet reads `Windows` in React Native's runtime, where it doesn't exist and
// would throw. Declared here as undefined, it's captured as undefined, which
// runOnUI resolves to the UI runtime's own global.
if (isWindows) {
const scope = globalThis as Record<string, unknown>;
for (const name of UI_RUNTIME_GLOBALS) if (!(name in scope)) scope[name] = undefined;
}

interface WindowsModule {
install(metadataPath: string): boolean;
isInstalled(): boolean;
runWorklet(code: string, payload: string): Promise<string>;
}

interface WorkletFunction {
__initData?: { code?: string };
__closure?: Record<string, unknown>;
}

interface WorkletResult {
ok: boolean;
value?: unknown;
error?: string;
logs?: [string, string][];
}

const windowsModule = NativeScriptNativeApi as unknown as WindowsModule;

export function initWindows(): boolean {
return windowsModule.isInstalled() || windowsModule.install("");
}

function workletPayload(callback: WorkletFunction, args: unknown[]): string {
const values: Record<string, unknown> = {};
// Captured names with no value on React Native's side (Windows, XamlWindow,
// NSWinRT) are globals of the UI runtime.
const globals: string[] = [];
for (const [name, value] of Object.entries(callback.__closure ?? {})) {
if (value === undefined) {
globals.push(name);
} else if (typeof value === "function") {
throw new TypeError(
`NativeScript.runOnUI on Windows passes a worklet's captured values as JSON, so it can't capture the function '${name}'. Define it inside the worklet.`,
);
} else {
values[name] = value;
}
}
return JSON.stringify({ args, closure: values, globals });
}

export async function runOnUIWindows<Args extends unknown[], ReturnValue>(
callback: (...args: Args) => ReturnValue | Promise<ReturnValue>,
args: Args,
): Promise<ReturnValue> {
const code = (callback as WorkletFunction).__initData?.code;
if (typeof code !== "string") {
throw new TypeError(
"NativeScript.runOnUI requires a worklet: start the function with 'worklet' and add react-native-worklets/plugin to your Babel plugins (on Windows it only compiles the worklet; no native Worklets library is needed).",
);
}
initWindows();
const json = await windowsModule.runWorklet(code, workletPayload(callback as WorkletFunction, args));
const result = JSON.parse(json) as WorkletResult;
for (const [level, text] of result.logs ?? []) {
const log = (console as unknown as Record<string, (...a: unknown[]) => void>)[level] ?? console.log;
log(text);
}
if (!result.ok) throw new Error(result.error ?? "The worklet failed");
return result.value as ReturnValue;
}
49 changes: 49 additions & 0 deletions packages/react-native/windows/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
*AppPackages*
*BundleArtifacts*

#OS junk files
[Tt]humbs.db
*.DS_Store

#Visual Studio files
*.[Oo]bj
*.user
*.aps
*.pch
*.vspscc
*.vssscc
*_i.c
*_p.c
*.ncb
*.suo
*.tlb
*.tlh
*.bak
*.[Cc]ache
*.ilk
*.log
*.lib
*.sbr
*.sdf
*.opensdf
*.opendb
*.unsuccessfulbuild
ipch/
[Oo]bj/
[Bb]in
[Dd]ebug*/
[Rr]elease*/
Ankh.NoLoad
.vs/
# Visual C++ cache files

#Files generated by the VS build
**/Generated Files/**

#Files generated by MS build
*.binlog
*.err
*.wrn

# NuGet locks the react-native-windows the module was last built with; apps bring their own.
packages.lock.json
33 changes: 33 additions & 0 deletions packages/react-native/windows/ExperimentalFeatures.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup Label="Microsoft.ReactNative Experimental Features">
<!--
Required for building a New Architecture project.

Library projects can change this value to test against Old and New
Architectures when building the library project's local sln file.

Otherwise this value will be decided by the consuming RNW app.

See https://microsoft.github.io/react-native-windows/docs/new-architecture
-->
<RnwNewArch>true</RnwNewArch>

<!--
Changes compilation to assume use of Microsoft.ReactNative NuGet packages
instead of building the framework from source. Defaults to true.

This is set during library project creation and is used when building
the library project's local sln file.

Otherwise this value will be decided by the consuming RNW app.

See https://microsoft.github.io/react-native-windows/docs/nuget
-->
<UseExperimentalNuget>true</UseExperimentalNuget>

<ReactExperimentalFeaturesSet>true</ReactExperimentalFeaturesSet>
</PropertyGroup>

</Project>
43 changes: 43 additions & 0 deletions packages/react-native/windows/NativeScriptNativeApi.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32929.385
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NativeScriptNativeApi", "NativeScriptNativeApi\NativeScriptNativeApi.vcxproj", "{E029AA39-91AA-4750-92D2-438D750ADF6F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Debug|ARM64 = Debug|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
Release|ARM64 = Release|ARM64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|x64.ActiveCfg = Debug|x64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|x64.Build.0 = Debug|x64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|x64.Deploy.0 = Debug|x64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|x86.ActiveCfg = Debug|Win32
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|x86.Build.0 = Debug|Win32
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|x86.Deploy.0 = Debug|Win32
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|ARM64.Build.0 = Debug|ARM64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Debug|ARM64.Deploy.0 = Debug|ARM64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|x64.ActiveCfg = Release|x64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|x64.Build.0 = Release|x64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|x64.Deploy.0 = Release|x64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|x86.ActiveCfg = Release|Win32
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|x86.Build.0 = Release|Win32
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|x86.Deploy.0 = Release|Win32
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|ARM64.ActiveCfg = Release|ARM64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|ARM64.Build.0 = Release|ARM64
{E029AA39-91AA-4750-92D2-438D750ADF6F}.Release|ARM64.Deploy.0 = Release|ARM64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D43FAD39-F619-437D-BB40-04A3982ACB6A}
EndGlobalSection
EndGlobal
Loading
Loading