diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 4a38a7a1d..b647f1627 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -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=` and `/p:NativeScriptWindowsNapiJs=` 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. diff --git a/packages/react-native/package.json b/packages/react-native/package.json index f7dddb779..78ee4aadb 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -9,7 +9,8 @@ "JSI", "iOS", "Android", - "Java" + "Java", + "Windows" ], "repository": { "type": "git", @@ -38,6 +39,7 @@ "types", "android", "ios", + "windows", "metadata", "native-api", "NativeScriptNativeApi.podspec", diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index b15d01120..a8599147a 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -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; @@ -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); @@ -1344,6 +1347,7 @@ export function runOnUI( if (typeof callback !== "function") { throw new TypeError("NativeScript.runOnUI expects a Worklets callback"); } + if (isWindows) return runOnUIWindows(callback, args); ensureNativeScriptInstalled(); const worklets = ensureWorkletsInstalled(); diff --git a/packages/react-native/src/windows.ts b/packages/react-native/src/windows.ts new file mode 100644 index 000000000..be0bc3ede --- /dev/null +++ b/packages/react-native/src/windows.ts @@ -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; + 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; +} + +interface WorkletFunction { + __initData?: { code?: string }; + __closure?: Record; +} + +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 = {}; + // 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( + callback: (...args: Args) => ReturnValue | Promise, + args: Args, +): Promise { + 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 void>)[level] ?? console.log; + log(text); + } + if (!result.ok) throw new Error(result.error ?? "The worklet failed"); + return result.value as ReturnValue; +} diff --git a/packages/react-native/windows/.gitignore b/packages/react-native/windows/.gitignore new file mode 100644 index 000000000..445a03059 --- /dev/null +++ b/packages/react-native/windows/.gitignore @@ -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 diff --git a/packages/react-native/windows/ExperimentalFeatures.props b/packages/react-native/windows/ExperimentalFeatures.props new file mode 100644 index 000000000..db10c59f9 --- /dev/null +++ b/packages/react-native/windows/ExperimentalFeatures.props @@ -0,0 +1,33 @@ + + + + + + true + + + true + + true + + + diff --git a/packages/react-native/windows/NativeScriptNativeApi.sln b/packages/react-native/windows/NativeScriptNativeApi.sln new file mode 100644 index 000000000..f43143271 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi.sln @@ -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 diff --git a/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.cpp b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.cpp new file mode 100644 index 000000000..3099e2b1e --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.cpp @@ -0,0 +1,97 @@ +#include "pch.h" + +#include "NativeScriptNativeApi.h" + +#include "UIRuntime.h" + +#include +#include +#include + +namespace winrt::NativeScriptNativeApi { + +namespace { + +std::mutex g_lock; +std::shared_ptr g_runtime; +bool g_installed = false; + +// This module: it embeds the windows-napi addon its UI-thread runtime loads. +HMODULE ThisModule() { + HMODULE module{}; + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&ThisModule), + &module); + return module; +} + +} // namespace + +void NativeScriptNativeApi::Initialize(React::ReactContext const &reactContext) noexcept { + m_context = reactContext; + std::lock_guard lock(g_lock); + // One runtime per app: a reload gets a new React instance and module, not a new UI thread. + if (!g_runtime) { + auto dispatcher = reactContext.UIDispatcher(); + g_runtime = NativeScriptRN::UIRuntime::Create( + [dispatcher](std::function work) { dispatcher.Post([work = std::move(work)]() { work(); }); }, + ThisModule()); + } +} + +bool NativeScriptNativeApi::install(std::string /*metadataPath*/) noexcept { + std::lock_guard lock(g_lock); + if (!g_runtime) return false; + // Created on the UI thread, before any worklet: the dispatcher runs work in order. + auto runtime = g_runtime; + m_context.UIDispatcher().Post([runtime]() { runtime->Start(); }); + g_installed = true; + return true; +} + +bool NativeScriptNativeApi::installWorkletRuntime(React::JSValue /*runtimeHolder*/, std::string /*metadataPath*/) noexcept { + return false; +} + +bool NativeScriptNativeApi::isInstalled() noexcept { + std::lock_guard lock(g_lock); + return g_installed; +} + +std::string NativeScriptNativeApi::defaultMetadataPath() noexcept { + return ""; +} + +std::string NativeScriptNativeApi::getRuntimeBackend() noexcept { + return "hermes-node-api-ui"; +} + +bool NativeScriptNativeApi::__writeTestMarker(std::string content) noexcept { + wchar_t temp[MAX_PATH + 1]; + if (!GetTempPathW(MAX_PATH + 1, temp)) return false; + std::ofstream file(std::filesystem::path(temp) / L"nativescript-react-native-marker.txt", std::ios::binary); + file << content; + return static_cast(file); +} + +void NativeScriptNativeApi::runWorklet( + std::string code, + std::string payload, + React::ReactPromise &&result) noexcept { + std::shared_ptr runtime; + { + std::lock_guard lock(g_lock); + runtime = g_runtime; + } + if (!runtime) { + result.Reject("NativeScriptNativeApi isn't initialized"); + return; + } + m_context.UIDispatcher().Post([runtime, code = std::move(code), payload = std::move(payload), result]() { + // Failures inside the worklet resolve with { ok: false }; the JS side throws them. + runtime->Run(code, payload, [result](std::string json) { result.Resolve(json); }); + }); +} + +} // namespace winrt::NativeScriptNativeApi diff --git a/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.def b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.def new file mode 100644 index 000000000..24e7c1235 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.h b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.h new file mode 100644 index 000000000..c43d88ab4 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.h @@ -0,0 +1,58 @@ +#pragma once + +#include "pch.h" +#include "resource.h" + +#include "NativeModules.h" + +namespace winrt::NativeScriptNativeApi { + +// NativeScript for React Native on Windows (react-native-windows, New Architecture). +// +// On iOS, macOS and Android, NativeScript installs into a jsi::Runtime React Native owns: the +// main one, or the Worklets UI runtime. On Windows neither is reachable from a module: +// - A module DLL sees the main runtime only through the JsiRuntime ABI (JsiAbiRuntime), not the +// engine's own runtime object, and NativeScript's Windows runtime binds through Node-API, not +// JSI. +// - react-native-worklets has no Windows implementation. +// So the module runs its own Hermes runtime on the UI thread, made through the jsr_* API of the +// hermes.dll react-native-windows already loaded, with @nativescript/windows-napi attached to its +// napi_env: the whole Windows Runtime under `Windows`, plus `XamlWindow` and `NSWinRT`. Worklets +// run there (runWorklet), from the code the Worklets Babel plugin embeds in each one, with their +// captured values and arguments as JSON. See UIRuntime.h. +REACT_MODULE(NativeScriptNativeApi) +struct NativeScriptNativeApi { + REACT_INIT(Initialize) + void Initialize(React::ReactContext const &reactContext) noexcept; + + // Starts the UI-thread runtime. Windows has no metadata file: the runtime reads WinRT + // metadata (.winmd) from the system, so metadataPath is ignored. + REACT_SYNC_METHOD(install) + bool install(std::string metadataPath) noexcept; + + // There is no Worklets UI runtime on Windows; runWorklet replaces it. + REACT_SYNC_METHOD(installWorkletRuntime) + bool installWorkletRuntime(React::JSValue runtimeHolder, std::string metadataPath) noexcept; + + REACT_SYNC_METHOD(isInstalled) + bool isInstalled() noexcept; + + REACT_SYNC_METHOD(defaultMetadataPath) + std::string defaultMetadataPath() noexcept; + + REACT_SYNC_METHOD(getRuntimeBackend) + std::string getRuntimeBackend() noexcept; + + REACT_SYNC_METHOD(__writeTestMarker) + bool __writeTestMarker(std::string content) noexcept; + + // Windows only: runs a worklet's code on the UI thread. `payload` is JSON + // { args, closure, globals } and the result is JSON { ok, value | error, logs }. + REACT_METHOD(runWorklet) + void runWorklet(std::string code, std::string payload, React::ReactPromise &&result) noexcept; + + private: + React::ReactContext m_context; +}; + +} // namespace winrt::NativeScriptNativeApi diff --git a/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.rc b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.rc new file mode 100644 index 000000000..9ee6bf1fc Binary files /dev/null and b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.rc differ diff --git a/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.vcxproj b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.vcxproj new file mode 100644 index 000000000..235df937f --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.vcxproj @@ -0,0 +1,181 @@ + + + + + + true + true + {E029AA39-91AA-4750-92D2-438D750ADF6F} + NativeScriptNativeApi + Win32Proj + NativeScriptNativeApi + 10.0 + en-US + 17.0 + false + + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ + false + + false + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + DynamicLibrary + Unicode + + $(DefaultPlatformToolset) + false + + + true + + + false + true + + + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + true + %(AdditionalOptions) /bigobj + 4453;28204 + _WINRT_DLL;%(PreprocessorDefinitions) + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + + $(MSBuildThisFileDirectory); + %(AdditionalIncludeDirectories) + + + + shell32.lib;user32.lib;windowsapp.lib;%(AdditionalDependencies) + Console + true + NativeScriptNativeApi.def + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + + + + + + + ReactPackageProvider.idl + + + + + + + + + + + + NotUsing + + + Create + + + ReactPackageProvider.idl + + + + + + + + + + + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\@nativescript\windows-napi\package.json'))\node_modules\@nativescript\windows-napi\ + arm64 + x64 + $(NativeScriptWindowsNapiDir)windows.win32-$(NativeScriptWindowsNapiArch)-msvc.node + $(NativeScriptWindowsNapiDir)nswinrt.js + $(IntDir)nativescript\ + + + + $(NativeScriptEmbedDir);%(AdditionalIncludeDirectories) + + + + + + + + + + + + + + + + + + + + This project references targets in your node_modules\react-native-windows folder. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.vcxproj.filters b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.vcxproj.filters new file mode 100644 index 000000000..1f902803b --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NativeScriptNativeApi.vcxproj.filters @@ -0,0 +1,56 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/packages/react-native/windows/NativeScriptNativeApi/NodeApiHost.cpp b/packages/react-native/windows/NativeScriptNativeApi/NodeApiHost.cpp new file mode 100644 index 000000000..3a73abd65 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NodeApiHost.cpp @@ -0,0 +1,372 @@ +// Node's half of Node-API over an engine that provides only js_native_api.h. See NodeApiHost.h. +// Built without the project's precompiled header: it needs nothing beyond Win32. +#include + +#include "NodeApiHost.h" + +#include +#include +#include +#include +#include + +namespace { + +typedef struct napi_env__ *napi_env; +typedef struct napi_value__ *napi_value; +typedef struct napi_ref__ *napi_ref; +typedef struct napi_handle_scope__ *napi_handle_scope; +typedef int napi_status; +typedef void (*napi_finalize)(napi_env env, void *data, void *hint); +typedef void (*napi_threadsafe_function_call_js)(napi_env env, napi_value js_callback, void *context, void *data); +typedef void (*napi_async_execute_callback)(napi_env env, void *data); +typedef void (*napi_async_complete_callback)(napi_env env, napi_status status, void *data); + +constexpr napi_status napi_ok = 0; +constexpr napi_status napi_invalid_arg = 1; +constexpr napi_status napi_generic_failure = 9; +constexpr int napi_uint8_array = 1; +constexpr int napi_tsfn_abort = 1; + +struct Engine { + napi_status(__cdecl *create_reference)(napi_env, napi_value, uint32_t, napi_ref *); + napi_status(__cdecl *delete_reference)(napi_env, napi_ref); + napi_status(__cdecl *get_reference_value)(napi_env, napi_ref, napi_value *); + napi_status(__cdecl *get_undefined)(napi_env, napi_value *); + napi_status(__cdecl *get_global)(napi_env, napi_value *); + napi_status(__cdecl *call_function)(napi_env, napi_value, napi_value, size_t, const napi_value *, napi_value *); + napi_status(__cdecl *open_handle_scope)(napi_env, napi_handle_scope *); + napi_status(__cdecl *close_handle_scope)(napi_env, napi_handle_scope); + napi_status(__cdecl *create_arraybuffer)(napi_env, size_t, void **, napi_value *); + napi_status(__cdecl *create_external_arraybuffer)(napi_env, void *, size_t, napi_finalize, void *, napi_value *); + napi_status(__cdecl *create_typedarray)(napi_env, int, size_t, napi_value, size_t, napi_value *); + napi_status(__cdecl *get_typedarray_info)(napi_env, napi_value, int *, size_t *, void **, napi_value *, size_t *); + napi_status(__cdecl *is_typedarray)(napi_env, napi_value, bool *); +}; + +Engine g_engine{}; +std::function)> g_post; + +// Runs `work` on the runtime's thread inside a handle scope, as Node runs a threadsafe function's +// call or an async work's completion. +void OnRuntimeThread(napi_env env, std::function work) { + g_post([env, work = std::move(work)]() { + napi_handle_scope scope{}; + g_engine.open_handle_scope(env, &scope); + work(); + g_engine.close_handle_scope(env, scope); + }); +} + +struct ThreadsafeFunction { + napi_env env; + napi_ref callback; + void *context; + napi_threadsafe_function_call_js callJs; + napi_finalize finalize; + void *finalizeData; + std::atomic threads; + std::atomic closing{false}; +}; + +void Finalize(ThreadsafeFunction *fn) { + OnRuntimeThread(fn->env, [fn]() { + if (fn->finalize) fn->finalize(fn->env, fn->finalizeData, fn->context); + if (fn->callback) g_engine.delete_reference(fn->env, fn->callback); + delete fn; + }); +} + +struct AsyncWork { + napi_env env; + napi_async_execute_callback execute; + napi_async_complete_callback complete; + void *data; +}; + +struct NodeVersion { + uint32_t major, minor, patch; + const char *release; +}; +const NodeVersion kVersion{22, 0, 0, "nativescript-react-native-windows"}; + +} // namespace + +namespace NodeApiHost { + +bool Init(HMODULE hermes, std::function)> post) { + g_post = std::move(post); + bool ok = true; + auto get = [&](auto &fn, const char *name) { + fn = reinterpret_cast>(GetProcAddress(hermes, name)); + ok = ok && fn != nullptr; + }; + get(g_engine.create_reference, "napi_create_reference"); + get(g_engine.delete_reference, "napi_delete_reference"); + get(g_engine.get_reference_value, "napi_get_reference_value"); + get(g_engine.get_undefined, "napi_get_undefined"); + get(g_engine.get_global, "napi_get_global"); + get(g_engine.call_function, "napi_call_function"); + get(g_engine.open_handle_scope, "napi_open_handle_scope"); + get(g_engine.close_handle_scope, "napi_close_handle_scope"); + get(g_engine.create_arraybuffer, "napi_create_arraybuffer"); + get(g_engine.create_external_arraybuffer, "napi_create_external_arraybuffer"); + get(g_engine.create_typedarray, "napi_create_typedarray"); + get(g_engine.get_typedarray_info, "napi_get_typedarray_info"); + get(g_engine.is_typedarray, "napi_is_typedarray"); + return ok; +} + +std::wstring HostModules(HMODULE hermes) { + auto path = [](HMODULE module) { + std::wstring out(MAX_PATH, L'\0'); + for (;;) { + auto n = GetModuleFileNameW(module, out.data(), static_cast(out.size())); + if (n < out.size()) { + out.resize(n); + return out; + } + out.resize(out.size() * 2); + } + }; + HMODULE self{}; + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&HostModules), + &self); + return path(self) + L";" + path(hermes); +} + +} // namespace NodeApiHost + +#define NODE_API extern "C" __declspec(dllexport) napi_status __cdecl + +// ── threadsafe functions ────────────────────────────────────────────────────── + +NODE_API napi_create_threadsafe_function( + napi_env env, + napi_value func, + napi_value /*async_resource*/, + napi_value /*async_resource_name*/, + size_t /*max_queue_size*/, + size_t initial_thread_count, + void *thread_finalize_data, + napi_finalize thread_finalize_cb, + void *context, + napi_threadsafe_function_call_js call_js_cb, + void **result) { + if (!result || (!func && !call_js_cb)) return napi_invalid_arg; + auto *fn = new ThreadsafeFunction{env, nullptr, context, call_js_cb, thread_finalize_cb, thread_finalize_data}; + fn->threads = static_cast(initial_thread_count); + if (func && g_engine.create_reference(env, func, 1, &fn->callback) != napi_ok) { + delete fn; + return napi_generic_failure; + } + *result = fn; + return napi_ok; +} + +NODE_API napi_call_threadsafe_function(void *func, void *data, int /*is_blocking*/) { + auto *fn = static_cast(func); + if (!fn || fn->closing) return napi_generic_failure; + OnRuntimeThread(fn->env, [fn, data]() { + napi_value callback{}; + if (fn->callback) g_engine.get_reference_value(fn->env, fn->callback, &callback); + if (fn->callJs) { + fn->callJs(fn->env, callback, fn->context, data); + } else if (callback) { + napi_value receiver, ignored; + g_engine.get_undefined(fn->env, &receiver); + g_engine.call_function(fn->env, receiver, callback, 0, nullptr, &ignored); + } + }); + return napi_ok; +} + +NODE_API napi_acquire_threadsafe_function(void *func) { + auto *fn = static_cast(func); + if (!fn || fn->closing) return napi_generic_failure; + fn->threads++; + return napi_ok; +} + +NODE_API napi_release_threadsafe_function(void *func, int mode) { + auto *fn = static_cast(func); + if (!fn) return napi_invalid_arg; + if (mode == napi_tsfn_abort || --fn->threads <= 0) { + if (!fn->closing.exchange(true)) Finalize(fn); + } + return napi_ok; +} + +NODE_API napi_get_threadsafe_function_context(void *func, void **result) { + if (!func || !result) return napi_invalid_arg; + *result = static_cast(func)->context; + return napi_ok; +} + +// The runtime's thread is React Native's UI thread, which outlives every function: nothing to +// keep alive or let go. +NODE_API napi_ref_threadsafe_function(napi_env, void *) { + return napi_ok; +} + +NODE_API napi_unref_threadsafe_function(napi_env, void *) { + return napi_ok; +} + +// ── env cleanup hooks: the runtime lives as long as the app, so they never run ───── + +NODE_API napi_add_env_cleanup_hook(napi_env, void (*)(void *), void *) { + return napi_ok; +} + +NODE_API napi_remove_env_cleanup_hook(napi_env, void (*)(void *), void *) { + return napi_ok; +} + +NODE_API napi_add_async_cleanup_hook(napi_env, void (*)(void *, void *), void *, void **remove_handle) { + if (remove_handle) *remove_handle = nullptr; + return napi_ok; +} + +NODE_API napi_remove_async_cleanup_hook(void *) { + return napi_ok; +} + +// ── buffers, as Uint8Arrays ──────────────────────────────────────────────────── + +NODE_API napi_create_buffer(napi_env env, size_t length, void **data, napi_value *result) { + napi_value buffer; + if (auto status = g_engine.create_arraybuffer(env, length, data, &buffer); status != napi_ok) return status; + return g_engine.create_typedarray(env, napi_uint8_array, length, buffer, 0, result); +} + +NODE_API napi_create_buffer_copy(napi_env env, size_t length, const void *source, void **data, napi_value *result) { + void *bytes{}; + if (auto status = napi_create_buffer(env, length, &bytes, result); status != napi_ok) return status; + if (length) memcpy(bytes, source, length); + if (data) *data = bytes; + return napi_ok; +} + +NODE_API napi_create_external_buffer( + napi_env env, + size_t length, + void *data, + napi_finalize finalize_cb, + void *finalize_hint, + napi_value *result) { + napi_value buffer; + if (auto status = g_engine.create_external_arraybuffer(env, data, length, finalize_cb, finalize_hint, &buffer); + status != napi_ok) { + return status; + } + return g_engine.create_typedarray(env, napi_uint8_array, length, buffer, 0, result); +} + +NODE_API napi_get_buffer_info(napi_env env, napi_value value, void **data, size_t *length) { + int type; + size_t count, offset; + napi_value buffer; + return g_engine.get_typedarray_info(env, value, &type, length ? length : &count, data, &buffer, &offset); +} + +NODE_API napi_is_buffer(napi_env env, napi_value value, bool *result) { + return g_engine.is_typedarray(env, value, result); +} + +// ── async work: executes on a thread of its own, completes on the runtime's ────── + +NODE_API napi_create_async_work( + napi_env env, + napi_value, + napi_value, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data, + void **result) { + if (!execute || !result) return napi_invalid_arg; + *result = new AsyncWork{env, execute, complete, data}; + return napi_ok; +} + +NODE_API napi_queue_async_work(napi_env, void *work) { + auto *w = static_cast(work); + if (!w) return napi_invalid_arg; + std::thread([w]() { + w->execute(w->env, w->data); + OnRuntimeThread(w->env, [w]() { + if (w->complete) w->complete(w->env, napi_ok, w->data); + }); + }).detach(); + return napi_ok; +} + +NODE_API napi_delete_async_work(napi_env, void *work) { + // Deleted by its owner from the completion callback, as Node allows. + delete static_cast(work); + return napi_ok; +} + +NODE_API napi_cancel_async_work(napi_env, void *) { + return napi_generic_failure; +} + +// ── callback scopes and async context: microtasks drain after each task anyway ── + +NODE_API napi_async_init(napi_env, napi_value, napi_value, void **result) { + if (result) *result = nullptr; + return napi_ok; +} + +NODE_API napi_async_destroy(napi_env, void *) { + return napi_ok; +} + +NODE_API napi_open_callback_scope(napi_env, napi_value, void *, void **result) { + if (result) *result = nullptr; + return napi_ok; +} + +NODE_API napi_close_callback_scope(napi_env, void *) { + return napi_ok; +} + +NODE_API napi_make_callback( + napi_env env, + void *, + napi_value recv, + napi_value func, + size_t argc, + const napi_value *argv, + napi_value *result) { + return g_engine.call_function(env, recv, func, argc, argv, result); +} + +// ── the rest of node_api.h ───────────────────────────────────────────────────── + +NODE_API napi_get_node_version(napi_env, const NodeVersion **result) { + if (!result) return napi_invalid_arg; + *result = &kVersion; + return napi_ok; +} + +NODE_API napi_get_uv_event_loop(napi_env, void **) { + return napi_generic_failure; +} + +extern "C" __declspec(dllexport) void __cdecl napi_module_register(void *) {} + +extern "C" __declspec(dllexport) void __cdecl napi_fatal_error( + const char *location, + size_t location_len, + const char *message, + size_t message_len) { + std::string text = "Node-API fatal error"; + if (location) text += " at " + std::string(location, location_len == SIZE_MAX ? strlen(location) : location_len); + if (message) text += ": " + std::string(message, message_len == SIZE_MAX ? strlen(message) : message_len); + OutputDebugStringA(text.c_str()); + fprintf(stderr, "%s\n", text.c_str()); + std::abort(); +} diff --git a/packages/react-native/windows/NativeScriptNativeApi/NodeApiHost.h b/packages/react-native/windows/NativeScriptNativeApi/NodeApiHost.h new file mode 100644 index 000000000..76dbc6fc0 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/NodeApiHost.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include +#include + +namespace NodeApiHost { + +// Node's half of Node-API (node_api.h) for a runtime whose engine provides only the engine half +// (js_native_api.h), as Hermes' does: threadsafe functions, env cleanup hooks, buffers, async work +// and callback scopes, exported from this module so a Node addon finds them. +// +// The addon's napi-sys takes each napi_* from the first module in NAPI_HOST_MODULES that exports +// it, so the embedder sets that to ";" before loading the addon. +// +// `post` runs work on the runtime's thread, where every napi_env call must happen; it may be +// called from any thread. `hermes` provides the engine half these functions are built on. +bool Init(HMODULE hermes, std::function)> post); + +// The value for NAPI_HOST_MODULES: this module, then hermes.dll. +std::wstring HostModules(HMODULE hermes); + +} // namespace NodeApiHost diff --git a/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.cpp b/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.cpp new file mode 100644 index 000000000..12facbc2b --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.cpp @@ -0,0 +1,20 @@ +#include "pch.h" + +#include "ReactPackageProvider.h" +#if __has_include("ReactPackageProvider.g.cpp") +#include "ReactPackageProvider.g.cpp" +#endif + +#include "NativeScriptNativeApi.h" + +using namespace winrt::Microsoft::ReactNative; + +namespace winrt::NativeScriptNativeApi::implementation +{ + +void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuilder) noexcept +{ + AddAttributedModules(packageBuilder, true); +} + +} // namespace winrt::NativeScriptNativeApi::implementation diff --git a/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.h b/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.h new file mode 100644 index 000000000..2e79b42dd --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.h @@ -0,0 +1,24 @@ +#pragma once + +#include "ReactPackageProvider.g.h" + +using namespace winrt::Microsoft::ReactNative; + +namespace winrt::NativeScriptNativeApi::implementation +{ + +struct ReactPackageProvider : ReactPackageProviderT +{ + ReactPackageProvider() = default; + + void CreatePackage(IReactPackageBuilder const &packageBuilder) noexcept; +}; + +} // namespace winrt::NativeScriptNativeApi::implementation + +namespace winrt::NativeScriptNativeApi::factory_implementation +{ + +struct ReactPackageProvider : ReactPackageProviderT {}; + +} // namespace winrt::NativeScriptNativeApi::factory_implementation diff --git a/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.idl b/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.idl new file mode 100644 index 000000000..ae841c3b4 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/ReactPackageProvider.idl @@ -0,0 +1,9 @@ +namespace NativeScriptNativeApi +{ + [webhosthidden] + [default_interface] + runtimeclass ReactPackageProvider : Microsoft.ReactNative.IReactPackageProvider + { + ReactPackageProvider(); + }; +} diff --git a/packages/react-native/windows/NativeScriptNativeApi/UIRuntime.cpp b/packages/react-native/windows/NativeScriptNativeApi/UIRuntime.cpp new file mode 100644 index 000000000..4d6cd766e --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/UIRuntime.cpp @@ -0,0 +1,431 @@ +#include "pch.h" + +#include "UIRuntime.h" + +#include "NodeApiHost.h" +#include "resource.h" + +#include + +#include +#include +#include + +#include + +namespace NativeScriptRN { + +namespace { + +// The slice of Node-API and Hermes' JS runtime API (js_runtime_api.h) this file uses, resolved +// from hermes.dll at run time. +typedef struct napi_env__ *napi_env; +typedef struct napi_value__ *napi_value; +typedef int napi_status; +typedef struct jsr_runtime_s *jsr_runtime; +typedef struct jsr_config_s *jsr_config; +typedef struct jsr_napi_env_scope_s *jsr_napi_env_scope; +constexpr napi_status napi_ok = 0; + +struct Api { + napi_status(__cdecl *jsr_create_config)(jsr_config *); + napi_status(__cdecl *jsr_delete_config)(jsr_config); + napi_status(__cdecl *jsr_config_set_explicit_microtasks)(jsr_config, bool); + napi_status(__cdecl *jsr_create_runtime)(jsr_config, jsr_runtime *); + napi_status(__cdecl *jsr_runtime_get_node_api_env)(jsr_runtime, napi_env *); + napi_status(__cdecl *jsr_open_napi_env_scope)(napi_env, jsr_napi_env_scope *); + napi_status(__cdecl *jsr_run_script)(napi_env, napi_value, const char *, napi_value *); + napi_status(__cdecl *jsr_drain_microtasks)(napi_env, int32_t, bool *); + napi_status(__cdecl *napi_create_string_utf8)(napi_env, const char *, size_t, napi_value *); + napi_status(__cdecl *napi_create_object)(napi_env, napi_value *); + napi_status(__cdecl *napi_get_global)(napi_env, napi_value *); + napi_status(__cdecl *napi_get_named_property)(napi_env, napi_value, const char *, napi_value *); + napi_status(__cdecl *napi_set_named_property)(napi_env, napi_value, const char *, napi_value); + napi_status(__cdecl *napi_call_function)(napi_env, napi_value, napi_value, size_t, const napi_value *, napi_value *); + napi_status(__cdecl *napi_get_value_string_utf8)(napi_env, napi_value, char *, size_t, size_t *); + napi_status(__cdecl *napi_coerce_to_string)(napi_env, napi_value, napi_value *); + napi_status(__cdecl *napi_is_exception_pending)(napi_env, bool *); + napi_status(__cdecl *napi_get_and_clear_last_exception)(napi_env, napi_value *); + + HMODULE hermes{}; + + // hermes.dll: the one react-native-windows loaded, else one beside the app or this module. + bool Load(std::wstring const &moduleDir, std::string &error) { + hermes = GetModuleHandleW(L"hermes.dll"); + if (!hermes) hermes = LoadLibraryW(L"hermes.dll"); + if (!hermes) hermes = LoadLibraryW((moduleDir + L"\\hermes.dll").c_str()); + if (!hermes) { + error = "hermes.dll isn't loaded. NativeScript on Windows needs an app that runs Hermes (the react-native-windows default)."; + return false; + } + bool ok = true; + auto get = [&](auto &fn, const char *name) { + fn = reinterpret_cast>(GetProcAddress(hermes, name)); + if (!fn) { + ok = false; + error = std::string("hermes.dll has no ") + name + ": it is older than NativeScript needs."; + } + }; + get(jsr_create_config, "jsr_create_config"); + get(jsr_delete_config, "jsr_delete_config"); + get(jsr_config_set_explicit_microtasks, "jsr_config_set_explicit_microtasks"); + get(jsr_create_runtime, "jsr_create_runtime"); + get(jsr_runtime_get_node_api_env, "jsr_runtime_get_node_api_env"); + get(jsr_open_napi_env_scope, "jsr_open_napi_env_scope"); + get(jsr_run_script, "jsr_run_script"); + get(jsr_drain_microtasks, "jsr_drain_microtasks"); + get(napi_create_string_utf8, "napi_create_string_utf8"); + get(napi_create_object, "napi_create_object"); + get(napi_get_global, "napi_get_global"); + get(napi_get_named_property, "napi_get_named_property"); + get(napi_set_named_property, "napi_set_named_property"); + get(napi_call_function, "napi_call_function"); + get(napi_get_value_string_utf8, "napi_get_value_string_utf8"); + get(napi_coerce_to_string, "napi_coerce_to_string"); + get(napi_is_exception_pending, "napi_is_exception_pending"); + get(napi_get_and_clear_last_exception, "napi_get_and_clear_last_exception"); + return ok; + } +}; + +std::string Narrow(std::wstring const &text) { + return winrt::to_string(text); +} + +std::wstring ModulePath(HMODULE module) { + std::wstring path(MAX_PATH, L'\0'); + for (;;) { + auto length = GetModuleFileNameW(module, path.data(), static_cast(path.size())); + if (length < path.size()) { + path.resize(length); + return path; + } + path.resize(path.size() * 2); + } +} + +std::wstring Directory(std::wstring const &path) { + return path.substr(0, path.find_last_of(L"\\/")); +} + +// An embedded resource (NativeScriptNativeApi.rc) as bytes. +std::string_view Resource(HMODULE module, int id) { + HRSRC info = FindResourceW(module, MAKEINTRESOURCEW(id), RT_RCDATA); + if (!info) return {}; + HGLOBAL data = LoadResource(module, info); + if (!data) return {}; + return {static_cast(LockResource(data)), SizeofResource(module, info)}; +} + +// Windows loads a DLL only from a file, so the embedded addon is written out once per build of it: +// %LOCALAPPDATA%\nativescript-react-native\\nswinrt.node. A packaged app's +// writes there land in its own package data. +std::wstring ExtractAddon(std::string_view bytes, std::string &error) { + uint64_t hash = 14695981039346656037ull; + for (unsigned char c : bytes) hash = (hash ^ c) * 1099511628211ull; + wchar_t name[17]; + swprintf_s(name, L"%016llx", static_cast(hash)); + + wchar_t base[MAX_PATH]; + DWORD length = GetEnvironmentVariableW(L"LOCALAPPDATA", base, MAX_PATH); + std::wstring dir = (length && length < MAX_PATH ? std::wstring(base, length) : Directory(ModulePath(nullptr))) + + L"\\nativescript-react-native\\" + name; + std::wstring path = dir + L"\\nswinrt.node"; + WIN32_FILE_ATTRIBUTE_DATA existing{}; + if (GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &existing) && existing.nFileSizeHigh == 0 && + existing.nFileSizeLow == bytes.size()) { + return path; + } + SHCreateDirectoryExW(nullptr, dir.c_str(), nullptr); + // Through a temporary file, so another process never loads a half-written one. + std::wstring temp = path + L"." + std::to_wstring(GetCurrentProcessId()) + L".tmp"; + { + std::ofstream file(temp, std::ios::binary); + file.write(bytes.data(), bytes.size()); + if (!file) { + error = "Couldn't write " + Narrow(temp) + "."; + return {}; + } + } + if (!MoveFileExW(temp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING) && + GetFileAttributesW(path.c_str()) == INVALID_FILE_ATTRIBUTES) { + error = "Couldn't write " + Narrow(path) + " (error " + std::to_string(GetLastError()) + ")."; + DeleteFileW(temp.c_str()); + return {}; + } + DeleteFileW(temp.c_str()); + return path; +} + +// The JS the runtime runs once NativeScript is attached: globals as in the desktop hosts, a +// console that buffers for React Native's own, and the worklet runner. Results are JSON strings +// collected by id, so promises can settle later. +constexpr const char *kPrelude = R"JS( +(function () { + var ns = globalThis.__nsWinRT; + globalThis.Windows = ns.Windows; + globalThis.XamlWindow = ns.XamlWindow; + globalThis.NSWinRT = Object.assign(globalThis.NSWinRT || {}, { toPromise: ns.toPromise, interop: ns.interop }); + var logs = []; + var format = function (value) { + if (typeof value === 'string') return value; + try { var json = JSON.stringify(value); return json === undefined ? String(value) : json; } catch (e) { return String(value); } + }; + globalThis.console = {}; + ['log', 'info', 'warn', 'error', 'debug'].forEach(function (level) { + globalThis.console[level] = function () { + logs.push([level, Array.prototype.map.call(arguments, format).join(' ')]); + }; + }); + var results = {}; + var finish = function (id, ok, value) { + var out = { ok: ok, logs: logs.splice(0) }; + if (ok) out.value = value === undefined ? null : value; + else out.error = value; + try { results[id] = JSON.stringify(out); } + catch (e) { results[id] = JSON.stringify({ ok: false, error: 'The worklet returned a value that is not JSON: ' + e.message, logs: out.logs }); } + }; + var describe = function (e) { return e && e.stack ? String(e.stack) : e && e.message ? String(e.message) : String(e); }; + globalThis.__nsRunWorklet = function (id, code, payload) { + try { + var p = JSON.parse(payload); + var closure = p.closure || {}; + // Names a worklet captured that don't exist on React Native's side (Windows, XamlWindow, + // NSWinRT) are this runtime's globals. + (p.globals || []).forEach(function (name) { closure[name] = globalThis[name]; }); + var fn = (0, eval)('(' + code + ')'); + var result = fn.apply({ __closure: closure }, p.args || []); + if (result && typeof result.then === 'function') { + result.then(function (v) { finish(id, true, v); }, function (e) { finish(id, false, describe(e)); }); + } else { + finish(id, true, result); + } + } catch (e) { + finish(id, false, describe(e)); + } + }; + globalThis.__nsTakeResult = function (id) { + var r = results[id]; + if (r === undefined) return ''; + delete results[id]; + return r; + }; +})(); +)JS"; + +} // namespace + +struct UIRuntime::Impl : std::enable_shared_from_this { + Post post; + HMODULE resources{}; + Api api{}; + napi_env env{}; + bool started{false}; + std::string startError; + uint64_t nextId{1}; + + std::string Str(napi_value value) { + napi_value text; + if (api.napi_coerce_to_string(env, value, &text) != napi_ok) return ""; + size_t length = 0; + api.napi_get_value_string_utf8(env, text, nullptr, 0, &length); + std::string out(length, '\0'); + api.napi_get_value_string_utf8(env, text, out.data(), length + 1, &length); + return out; + } + + // The pending exception's stack (or message), cleared; "" if none. + std::string TakeException() { + bool pending = false; + api.napi_is_exception_pending(env, &pending); + if (!pending) return ""; + napi_value error, stack; + api.napi_get_and_clear_last_exception(env, &error); + if (api.napi_get_named_property(env, error, "stack", &stack) == napi_ok) { + auto text = Str(stack); + if (!text.empty() && text != "undefined") return text; + } + return Str(error); + } + + bool RunScript(std::string const &source, const char *name, std::string &error) { + napi_value script, result; + api.napi_create_string_utf8(env, source.c_str(), source.size(), &script); + if (api.jsr_run_script(env, script, name, &result) != napi_ok) { + error = std::string(name) + ": " + TakeException(); + return false; + } + Drain(); + return true; + } + + void Drain() { + bool more = true; + for (int i = 0; i < 16 && more; i++) { + if (api.jsr_drain_microtasks(env, -1, &more) != napi_ok) break; + } + } + + napi_value GlobalFunction(const char *name) { + napi_value global, fn; + api.napi_get_global(env, &global); + api.napi_get_named_property(env, global, name, &fn); + return fn; + } + + std::string Start() { + if (started) return ""; + if (!startError.empty()) return startError; + startError = StartOnce(); + started = startError.empty(); + return startError; + } + + std::string StartOnce() { + std::string error; + if (!api.Load(Directory(ModulePath(resources)), error)) return error; + + jsr_config config{}; + jsr_runtime runtime{}; + api.jsr_create_config(&config); + // Promise reactions run when this module drains them (Drain), after every script and task; + // without explicit microtasks this Hermes never runs them for an embedder. + api.jsr_config_set_explicit_microtasks(config, true); + if (api.jsr_create_runtime(config, &runtime) != napi_ok) return "Hermes couldn't create a runtime."; + api.jsr_delete_config(config); + api.jsr_runtime_get_node_api_env(runtime, &env); + // The scope stays open for the runtime's life: it lives on this thread only. + jsr_napi_env_scope scope{}; + api.jsr_open_napi_env_scope(env, &scope); + + // Node's half of Node-API (threadsafe functions, cleanup hooks, buffers) comes from this module, + // the engine's from hermes.dll: the addon's napi-sys reads NAPI_HOST_MODULES to find them. + std::weak_ptr weak = shared_from_this(); + auto onThread = [weak](std::function work) { + if (auto self = weak.lock()) { + self->post([weak, work = std::move(work)]() { + work(); + if (auto again = weak.lock()) again->Drain(); + }); + } + }; + if (!NodeApiHost::Init(api.hermes, onThread)) return "hermes.dll lacks Node-API functions NativeScript needs."; + SetEnvironmentVariableW(L"NAPI_HOST_MODULES", NodeApiHost::HostModules(api.hermes).c_str()); + + auto addonBytes = Resource(resources, IDR_NSWINRT_NODE); + auto helpers = std::string(Resource(resources, IDR_NSWINRT_JS)); + if (addonBytes.empty() || helpers.empty()) { + return "NativeScriptNativeApi.dll has no embedded @nativescript/windows-napi: rebuild it with the addon installed."; + } + auto addonPath = ExtractAddon(addonBytes, error); + if (addonPath.empty()) return error; + HMODULE addon = LoadLibraryW(addonPath.c_str()); + if (!addon) { + return "Couldn't load " + Narrow(addonPath) + " (error " + std::to_string(GetLastError()) + + "), @nativescript/windows-napi's addon."; + } + using Register = napi_value(__cdecl *)(napi_env, napi_value); + auto registerModule = reinterpret_cast(GetProcAddress(addon, "napi_register_module_v1")); + if (!registerModule) return Narrow(addonPath) + " is not a Node-API addon."; + + napi_value exports, global; + api.napi_create_object(env, &exports); + napi_value native = registerModule(env, exports); + auto pending = TakeException(); + if (!pending.empty()) return "@nativescript/windows-napi failed to start: " + pending; + api.napi_get_global(env, &global); + api.napi_set_named_property(env, global, "__nsNative", native ? native : exports); + + // nswinrt.js is CommonJS whose only require is './index.js', the native exports. + auto wrapped = "globalThis.__nsWinRT = (function () { var module = { exports: {} };" + " (function (module, exports, require) {\n" + + helpers + + "\n})(module, module.exports, function () { return globalThis.__nsNative; });" + " return module.exports; })();"; + if (!RunScript(wrapped, "nswinrt.js", error)) return error; + if (!RunScript(kPrelude, "nativescript-react-native-windows.js", error)) return error; + return ""; + } + + void Run(std::string const &code, std::string const &payload, Done done) { + auto error = Start(); + if (!error.empty()) return done(Failure(error)); + auto id = std::to_string(nextId++); + napi_value args[3], receiver{}, ignored; + api.napi_create_string_utf8(env, id.c_str(), id.size(), &args[0]); + api.napi_create_string_utf8(env, code.c_str(), code.size(), &args[1]); + api.napi_create_string_utf8(env, payload.c_str(), payload.size(), &args[2]); + api.napi_get_global(env, &receiver); + if (api.napi_call_function(env, receiver, GlobalFunction("__nsRunWorklet"), 3, args, &ignored) != napi_ok) { + return done(Failure(TakeException())); + } + Collect(id, std::move(done), std::chrono::steady_clock::now()); + } + + // A worklet that returned a promise settles later: from microtasks, or from WinRT completions + // that arrive as messages on this thread. Poll for its result until it's there. + void Collect(std::string id, Done done, std::chrono::steady_clock::time_point started) { + Drain(); + napi_value idValue, result, global; + api.napi_create_string_utf8(env, id.c_str(), id.size(), &idValue); + api.napi_get_global(env, &global); + if (api.napi_call_function(env, global, GlobalFunction("__nsTakeResult"), 1, &idValue, &result) != napi_ok) { + return done(Failure(TakeException())); + } + auto json = Str(result); + if (!json.empty()) return done(json); + if (std::chrono::steady_clock::now() - started > std::chrono::minutes(2)) { + return done(Failure("The worklet's promise didn't settle within 2 minutes.")); + } + std::weak_ptr weak = shared_from_this(); + auto retry = [weak, id, done, started]() { + if (auto self = weak.lock()) self->Collect(id, done, started); + }; + winrt::Windows::System::Threading::ThreadPoolTimer::CreateTimer( + [post = post, retry](auto const &) { post(retry); }, std::chrono::milliseconds(8)); + } + + static std::string Failure(std::string const &message) { + std::string escaped; + for (char c : message) { + switch (c) { + case '"': escaped += "\\\""; break; + case '\\': escaped += "\\\\"; break; + case '\n': escaped += "\\n"; break; + case '\r': escaped += "\\r"; break; + case '\t': escaped += "\\t"; break; + default: + if (static_cast(c) < 0x20) { + char buffer[8]; + snprintf(buffer, sizeof buffer, "\\u%04x", c); + escaped += buffer; + } else { + escaped += c; + } + } + } + return "{\"ok\":false,\"error\":\"" + escaped + "\",\"logs\":[]}"; + } +}; + +std::shared_ptr UIRuntime::Create(Post post, HMODULE resources) { + auto runtime = std::make_shared(); + runtime->impl_ = std::make_shared(); + runtime->impl_->post = std::move(post); + runtime->impl_->resources = resources; + return runtime; +} + +std::string UIRuntime::Start() { + return impl_->Start(); +} + +bool UIRuntime::Started() const { + return impl_->started; +} + +void UIRuntime::Run(std::string const &code, std::string const &payload, Done done) { + impl_->Run(code, payload, std::move(done)); +} + +} // namespace NativeScriptRN diff --git a/packages/react-native/windows/NativeScriptNativeApi/UIRuntime.h b/packages/react-native/windows/NativeScriptNativeApi/UIRuntime.h new file mode 100644 index 000000000..1c253566b --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/UIRuntime.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include +#include + +namespace NativeScriptRN { + +// A Hermes runtime on React Native's UI thread with NativeScript's Windows runtime +// (@nativescript/windows-napi) attached. Everything here runs on the UI thread: post to it. +// +// - Engine: the hermes.dll react-native-windows loaded (or one beside the app), reached through +// its jsr_* and napi_* exports, so the module links no engine of its own. +// - NativeScript: the windows-napi addon (a DLL exporting napi_register_module_v1) and its JS +// helper layer nswinrt.js, embedded in this module as resources and extracted on first start. +// The addon takes the engine half of Node-API from hermes.dll and Node's half from this module +// (NodeApiHost.h). +class UIRuntime { + public: + using Done = std::function; + using Post = std::function)>; + + // `post` queues work on the UI thread; `resources` is the module holding the embedded addon. + static std::shared_ptr Create(Post post, HMODULE resources); + + // Starts the runtime if it isn't running yet; returns why it can't start, or "". + std::string Start(); + bool Started() const; + + // Runs a worklet's code (a function expression) with payload JSON { args, closure, globals }; + // calls `done` with JSON { ok: true, value, logs } or { ok: false, error, logs }, now or once a + // returned promise settles. + void Run(std::string const &code, std::string const &payload, Done done); + + struct Impl; + + private: + std::shared_ptr impl_; +}; + +} // namespace NativeScriptRN diff --git a/packages/react-native/windows/NativeScriptNativeApi/pch.cpp b/packages/react-native/windows/NativeScriptNativeApi/pch.cpp new file mode 100644 index 000000000..1d9f38c57 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/packages/react-native/windows/NativeScriptNativeApi/pch.h b/packages/react-native/windows/NativeScriptNativeApi/pch.h new file mode 100644 index 000000000..61d356df6 --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/pch.h @@ -0,0 +1,30 @@ +// pch.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +#define NOMINMAX 1 +#define WIN32_LEAN_AND_MEAN 1 +#define WINRT_LEAN_AND_MEAN 1 + +// Windows Header Files +#include +#undef GetCurrentTime +#include + +// WinRT Header Files +#include +#include +#include + +// C RunTime Header Files +#include +#include +#include +#include + +// Reference additional headers your project requires here diff --git a/packages/react-native/windows/NativeScriptNativeApi/resource.h b/packages/react-native/windows/NativeScriptNativeApi/resource.h new file mode 100644 index 000000000..86f1c043d --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/resource.h @@ -0,0 +1,9 @@ +// +// Microsoft Visual C++ generated include file. +// Used by NativeScriptNativeApi.rc + +#pragma once + +// @nativescript/windows-napi, embedded (NativeScriptNativeApi.rc) +#define IDR_NSWINRT_NODE 201 +#define IDR_NSWINRT_JS 202 diff --git a/packages/react-native/windows/NativeScriptNativeApi/targetver.h b/packages/react-native/windows/NativeScriptNativeApi/targetver.h new file mode 100644 index 000000000..87c0086de --- /dev/null +++ b/packages/react-native/windows/NativeScriptNativeApi/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/scripts/build_metadata_generator.sh b/scripts/build_metadata_generator.sh index 1d4b3c0e2..053afe473 100755 --- a/scripts/build_metadata_generator.sh +++ b/scripts/build_metadata_generator.sh @@ -6,6 +6,15 @@ function metadata_generator_source_hash { find src include CMakeLists.txt -type f -print | LC_ALL=C sort | xargs shasum | shasum | awk '{print $1}' } +# Mirrors the libclang lookup in metadata-generator/CMakeLists.txt. +function libclang_has_arch { + local clang_path libclang + clang_path=$(xcrun --find clang 2>/dev/null || true) + libclang="$(dirname "$(dirname "$clang_path")")/lib/libclang.dylib" + [ -f "$libclang" ] || libclang="/Library/Developer/CommandLineTools/usr/lib/libclang.dylib" + lipo -archs "$libclang" 2>/dev/null | tr ' ' '\n' | grep -qx "$1" +} + function build { rm -rf build mkdir build @@ -19,12 +28,17 @@ function build { pushd "metadata-generator" rm -rf dist mkdir dist -checkpoint "Building metadata generator for x86_64 ..." -build "x86_64" -# make sure the binary is linked against the system libc++ instead of an @rpath one (which happens when compiling on arm64) -# todo: perhaps there is a better way to do this with cmake? -#install_name_tool -change @rpath/libc++.1.dylib /usr/lib/libc++.1.dylib dist/x86_64/bin/objc-metadata-generator -otool -L dist/x86_64/bin/objc-metadata-generator +# Xcode 27+ ships an arm64-only libclang, so an x86_64 generator cannot be linked there. +if libclang_has_arch x86_64; then + checkpoint "Building metadata generator for x86_64 ..." + build "x86_64" + # make sure the binary is linked against the system libc++ instead of an @rpath one (which happens when compiling on arm64) + # todo: perhaps there is a better way to do this with cmake? + #install_name_tool -change @rpath/libc++.1.dylib /usr/lib/libc++.1.dylib dist/x86_64/bin/objc-metadata-generator + otool -L dist/x86_64/bin/objc-metadata-generator +else + echo "warning: skipping x86_64 metadata generator: the active toolchain's libclang has no x86_64 slice" >&2 +fi checkpoint "Building metadata generator for arm64 ..." build "arm64" diff --git a/scripts/build_nativescript.sh b/scripts/build_nativescript.sh index f3c70d67c..ffb6e4c3f 100755 --- a/scripts/build_nativescript.sh +++ b/scripts/build_nativescript.sh @@ -193,7 +193,6 @@ function ensure_metadata_generator () { expected_hash=$(metadata_generator_source_hash) local hash_file="./metadata-generator/dist/.source_hash" if [ ! -x "./metadata-generator/dist/arm64/bin/objc-metadata-generator" ] || \ - [ ! -x "./metadata-generator/dist/x86_64/bin/objc-metadata-generator" ] || \ [ ! -f "$hash_file" ] || \ [ "$(cat "$hash_file")" != "$expected_hash" ]; then "$SCRIPT_DIR/build_metadata_generator.sh" diff --git a/scripts/build_npm_ios.sh b/scripts/build_npm_ios.sh index f7bd1bf6b..40089b805 100755 --- a/scripts/build_npm_ios.sh +++ b/scripts/build_npm_ios.sh @@ -38,8 +38,12 @@ cp -R "./platforms/apple/templates/ios/." "$STAGING_DIR/framework" cp -R "dist/NativeScript.xcframework" "$STAGING_DIR/framework/internal" cp -R "dist/TKLiveSync.xcframework" "$STAGING_DIR/framework/internal" -mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-x86_64" -cp -R "metadata-generator/dist/x86_64/." "$STAGING_DIR/framework/internal/metadata-generator-x86_64" +if [ -d "metadata-generator/dist/x86_64" ]; then + mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-x86_64" + cp -R "metadata-generator/dist/x86_64/." "$STAGING_DIR/framework/internal/metadata-generator-x86_64" +else + echo "warning: skipping x86_64 metadata generator in package: metadata-generator/dist/x86_64 was not built" >&2 +fi mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-arm64" cp -R "metadata-generator/dist/arm64/." "$STAGING_DIR/framework/internal/metadata-generator-arm64" diff --git a/scripts/build_npm_macos.sh b/scripts/build_npm_macos.sh index e15b28567..24821619c 100755 --- a/scripts/build_npm_macos.sh +++ b/scripts/build_npm_macos.sh @@ -18,8 +18,12 @@ cp -R "./platforms/apple/templates/macos/." "$STAGING_DIR/framework" cp -R "dist/NativeScript.xcframework" "$STAGING_DIR/framework/internal" cp -R "dist/TKLiveSync.xcframework" "$STAGING_DIR/framework/internal" -mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-x86_64" -cp -R "metadata-generator/dist/x86_64/." "$STAGING_DIR/framework/internal/metadata-generator-x86_64" +if [ -d "metadata-generator/dist/x86_64" ]; then + mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-x86_64" + cp -R "metadata-generator/dist/x86_64/." "$STAGING_DIR/framework/internal/metadata-generator-x86_64" +else + echo "warning: skipping x86_64 metadata generator in package: metadata-generator/dist/x86_64 was not built" >&2 +fi mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-arm64" cp -R "metadata-generator/dist/arm64/." "$STAGING_DIR/framework/internal/metadata-generator-arm64" diff --git a/scripts/build_npm_vision.sh b/scripts/build_npm_vision.sh index b540e6750..98f0a3999 100755 --- a/scripts/build_npm_vision.sh +++ b/scripts/build_npm_vision.sh @@ -18,8 +18,12 @@ cp -R "./platforms/apple/templates/visionos/." "$STAGING_DIR/framework" cp -R "dist/NativeScript.xcframework" "$STAGING_DIR/framework/internal" cp -R "dist/TKLiveSync.xcframework" "$STAGING_DIR/framework/internal" -mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-x86_64" -cp -R "metadata-generator/dist/x86_64/." "$STAGING_DIR/framework/internal/metadata-generator-x86_64" +if [ -d "metadata-generator/dist/x86_64" ]; then + mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-x86_64" + cp -R "metadata-generator/dist/x86_64/." "$STAGING_DIR/framework/internal/metadata-generator-x86_64" +else + echo "warning: skipping x86_64 metadata generator in package: metadata-generator/dist/x86_64 was not built" >&2 +fi mkdir -p "$STAGING_DIR/framework/internal/metadata-generator-arm64" cp -R "metadata-generator/dist/arm64/." "$STAGING_DIR/framework/internal/metadata-generator-arm64" diff --git a/scripts/build_react_native_turbomodule.sh b/scripts/build_react_native_turbomodule.sh index c57de7497..d53a500be 100755 --- a/scripts/build_react_native_turbomodule.sh +++ b/scripts/build_react_native_turbomodule.sh @@ -22,7 +22,6 @@ function ensure_metadata_generator { expected_hash=$(metadata_generator_source_hash) local hash_file="$REPO_ROOT/metadata-generator/dist/.source_hash" if [ ! -x "$REPO_ROOT/metadata-generator/dist/arm64/bin/objc-metadata-generator" ] || \ - [ ! -x "$REPO_ROOT/metadata-generator/dist/x86_64/bin/objc-metadata-generator" ] || \ [ ! -f "$hash_file" ] || \ [ "$(cat "$hash_file")" != "$expected_hash" ]; then "$SCRIPT_DIR/build_metadata_generator.sh"