├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ └── xcschemes │ ├── NodeAPI.xcscheme │ ├── NodeAPIMacrosTests.xcscheme │ ├── NodeJSC.xcscheme │ ├── NodeJSCTests.xcscheme │ ├── NodeModuleSupport.xcscheme │ └── node-swift-Package.xcscheme ├── LICENSE.md ├── NodeAPIMacros.xctestplan ├── NodeJSC.xctestplan ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── CNodeAPI │ ├── module.modulemap │ └── vendored │ │ ├── js_native_api.h │ │ ├── js_native_api_types.h │ │ ├── node_api.h │ │ └── node_api_types.h ├── CNodeAPISupport │ ├── include │ │ └── node_context.h │ └── node_context.c ├── CNodeJSC │ ├── include │ │ └── embedder.h │ └── js_native_api_javascriptcore.cc ├── NodeAPI │ ├── Locks.swift │ ├── NodeAPIError.swift │ ├── NodeActor.swift │ ├── NodeArray.swift │ ├── NodeArrayBuffer.swift │ ├── NodeAsyncQueue.swift │ ├── NodeBigInt.swift │ ├── NodeBool.swift │ ├── NodeBuffer.swift │ ├── NodeClass.swift │ ├── NodeContext.swift │ ├── NodeDataView.swift │ ├── NodeDate.swift │ ├── NodeEnvironment.swift │ ├── NodeError.swift │ ├── NodeExternal.swift │ ├── NodeFunction.swift │ ├── NodeInstanceData.swift │ ├── NodeModule.swift │ ├── NodeNull.swift │ ├── NodeNumber.swift │ ├── NodeObject.swift │ ├── NodePromise.swift │ ├── NodeProperty.swift │ ├── NodeString.swift │ ├── NodeSymbol.swift │ ├── NodeThrowable.swift │ ├── NodeTypedArray.swift │ ├── NodeUndefined.swift │ ├── NodeValue.swift │ ├── Sugar.swift │ └── Utilities.swift ├── NodeAPIMacros │ ├── Diagnostics.swift │ ├── NodeClassMacro.swift │ ├── NodeConstructorMacro.swift │ ├── NodeMethodMacro.swift │ ├── NodeModuleMacro.swift │ ├── NodePropertyMacro.swift │ ├── Plugin.swift │ └── SyntaxUtils.swift ├── NodeJSC │ └── NodeJSC.swift └── NodeModuleSupport │ ├── include │ └── .keep │ ├── node_gyp_LICENSE │ ├── register.c │ └── win_delay_load_hook.cc ├── Tests ├── NodeAPIMacrosTests │ ├── NodeClassMacroTests.swift │ ├── NodeConstructorMacroTests.swift │ ├── NodeMacroTest.swift │ ├── NodeMethodMacroTests.swift │ ├── NodeModuleMacroTests.swift │ └── NodePropertyMacroTests.swift └── NodeJSCTests │ ├── JSContext+GC.swift │ └── NodeJSCTests.swift ├── example ├── .gitignore ├── Package.swift ├── Sources │ └── MyExample │ │ └── MyExample.swift ├── index.js └── package.json ├── package-lock.json ├── package.json ├── src ├── builder.ts ├── cli.ts └── utils.ts ├── test ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata ├── Package.swift ├── index.js └── suites │ ├── Integration │ ├── Integration.swift │ └── index.js │ └── Test │ ├── Test.swift │ └── index.js ├── tsconfig.json └── vendored └── node └── lib └── node-win32-x64.lib /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | test 4 | example 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "airbnb-typescript/base" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "project": "./tsconfig.json" 11 | }, 12 | "plugins": ["import"], 13 | "rules": { 14 | "@typescript-eslint/indent": [ 15 | "error", 16 | 4 17 | ], 18 | "@typescript-eslint/lines-between-class-members": "off", 19 | "@typescript-eslint/no-unused-vars": "warn", 20 | "@typescript-eslint/quotes": ["error", "double"], 21 | "quote-props": ["error", "consistent"], 22 | "no-console": "off", 23 | "max-classes-per-file": "off", 24 | "prefer-destructuring": "off", 25 | "@typescript-eslint/comma-dangle": [ 26 | "error", 27 | { 28 | "arrays": "always-multiline", 29 | "objects": "always-multiline", 30 | "imports": "never", 31 | "exports": "never", 32 | "functions": "never" 33 | } 34 | ], 35 | "no-restricted-syntax": "off", 36 | "no-multi-str": "off" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | vendored/** linguist-vendored 2 | **/generated/** linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .build 4 | build 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | /lib 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | build/ 3 | xcuserdata/ 4 | DerivedData/ 5 | .swiftpm/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/NodeAPI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/NodeAPIMacrosTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 15 | 16 | 19 | 20 | 21 | 22 | 24 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 51 | 52 | 54 | 55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/NodeJSC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 47 | 48 | 54 | 55 | 61 | 62 | 63 | 64 | 66 | 67 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/NodeJSCTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 15 | 16 | 19 | 20 | 21 | 22 | 24 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 51 | 52 | 54 | 55 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/NodeModuleSupport.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/node-swift-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 63 | 69 | 70 | 71 | 73 | 79 | 80 | 81 | 82 | 83 | 93 | 94 | 100 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kabir Oberai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NodeAPIMacros.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "C616BF7E-11A9-4348-B376-F7574A149179", 5 | "name" : "Configuration", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:", 18 | "identifier" : "NodeAPIMacrosTests", 19 | "name" : "NodeAPIMacrosTests" 20 | } 21 | } 22 | ], 23 | "version" : 1 24 | } 25 | -------------------------------------------------------------------------------- /NodeJSC.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "CE0028CE-5DE1-4324-B4D9-6C17345A1156", 5 | "name" : "Configuration", 6 | "options" : { 7 | "addressSanitizer" : { 8 | "enabled" : true 9 | } 10 | } 11 | } 12 | ], 13 | "defaultOptions" : { 14 | 15 | }, 16 | "testTargets" : [ 17 | { 18 | "target" : { 19 | "containerPath" : "container:", 20 | "identifier" : "NodeJSCTests", 21 | "name" : "NodeJSCTests" 22 | } 23 | } 24 | ], 25 | "version" : 1 26 | } 27 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "2ae6feecadadc9a8b7b75efa9c5f99f1a88cd40d2e36e95e14f8472a8e5b1bad", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-custom-dump", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 8 | "state" : { 9 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 10 | "version" : "1.3.3" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-macro-testing", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 17 | "state" : { 18 | "revision" : "cfe474c7e97d429ea31eefed2e9ab8c7c74260f9", 19 | "version" : "0.6.2" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-snapshot-testing", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 26 | "state" : { 27 | "revision" : "1be8144023c367c5de701a6313ed29a3a10bf59b", 28 | "version" : "1.18.3" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-syntax", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/swiftlang/swift-syntax.git", 35 | "state" : { 36 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", 37 | "version" : "601.0.1" 38 | } 39 | }, 40 | { 41 | "identity" : "xctest-dynamic-overlay", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 44 | "state" : { 45 | "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", 46 | "version" : "1.5.2" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | import Foundation 6 | 7 | let buildDynamic = ProcessInfo.processInfo.environment["NODE_SWIFT_BUILD_DYNAMIC"] == "1" 8 | 9 | let package = Package( 10 | name: "node-swift", 11 | platforms: [ 12 | .macOS(.v10_15), .iOS(.v13), 13 | ], 14 | products: [ 15 | .library( 16 | name: "NodeAPI", 17 | type: buildDynamic ? .dynamic : nil, 18 | targets: ["NodeAPI"] 19 | ), 20 | .library( 21 | name: "NodeJSC", 22 | targets: ["NodeJSC"] 23 | ), 24 | .library( 25 | name: "NodeModuleSupport", 26 | targets: ["NodeModuleSupport"] 27 | ), 28 | ], 29 | dependencies: [ 30 | .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"602.0.0"), 31 | .package(url: "https://github.com/pointfreeco/swift-macro-testing.git", .upToNextMinor(from: "0.6.2")), 32 | ], 33 | targets: [ 34 | .systemLibrary(name: "CNodeAPI"), 35 | .target( 36 | name: "CNodeJSC", 37 | linkerSettings: [ 38 | .linkedFramework("JavaScriptCore"), 39 | ] 40 | ), 41 | .target( 42 | name: "NodeJSC", 43 | dependencies: [ 44 | "CNodeJSC", 45 | "NodeAPI", 46 | ] 47 | ), 48 | .target(name: "CNodeAPISupport"), 49 | .macro( 50 | name: "NodeAPIMacros", 51 | dependencies: [ 52 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 53 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 54 | ] 55 | ), 56 | .target( 57 | name: "NodeAPI", 58 | dependencies: ["CNodeAPI", "CNodeAPISupport", "NodeAPIMacros"] 59 | ), 60 | .target( 61 | name: "NodeModuleSupport", 62 | dependencies: ["CNodeAPI"] 63 | ), 64 | .testTarget( 65 | name: "NodeJSCTests", 66 | dependencies: ["NodeJSC", "NodeAPI"] 67 | ), 68 | .testTarget( 69 | name: "NodeAPIMacrosTests", 70 | dependencies: [ 71 | .target(name: "NodeAPIMacros"), 72 | .product(name: "MacroTesting", package: "swift-macro-testing"), 73 | ] 74 | ), 75 | ], 76 | swiftLanguageModes: [.v5, .v6], 77 | cxxLanguageStandard: .cxx17 78 | ) 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeSwift 2 | 3 | Bridge Node.js and Swift code. 4 | 5 | ## What is it? 6 | 7 | NodeSwift allows you to write Swift code that talks to Node.js libraries, and vice versa. This enables possibilities such as 8 | 9 | - Using native macOS APIs and SwiftPM in an Electron app. 10 | - Interacting with the vast array of NPM APIs from a Swift program (e.g. a macOS app, iOS app, or a Vapor server). 11 | - Speeding up your JS code by writing performance critical bits in Swift. 12 | 13 | ## Example 14 | 15 | **MyModule.swift** 16 | ```swift 17 | import NodeAPI 18 | 19 | #NodeModule(exports: [ 20 | "nums": [Double.pi.rounded(.down), Double.pi.rounded(.up)], 21 | "str": String(repeating: "NodeSwift! ", count: 3), 22 | "add": try NodeFunction { (a: Double, b: Double) in 23 | "\(a) + \(b) = \(a + b)" 24 | }, 25 | ]) 26 | ``` 27 | 28 | **index.js** 29 | ```js 30 | const { nums, str, add } = require("./build/MyModule.node"); 31 | console.log(nums); // [ 3, 4 ] 32 | console.log(str); // NodeSwift! NodeSwift! NodeSwift! 33 | console.log(add(5, 10)); // 5.0 + 10.0 = 15.0 34 | ``` 35 | 36 | ## Features 37 | 38 | - **Safe**: NodeSwift makes use of Swift's memory safety and automatic reference counting. This means that, unlike with the C-based Node-API, you never have to think about memory management while writing NodeSwift modules. 39 | - **Simple**: With progressive disclosure, you can decide whether you want to use simpler or more advanced NodeSwift APIs to suit whatever your needs might be. 40 | - **Idiomatic**: NodeSwift's APIs feel right at home in idiomatic Swift code. For example, to make a Swift class usable from Node.js you literally declare a `class` in Swift that conforms to `NodeClass`. We also use several Swift features like Dynamic Member Lookup that are designed precisely to make this sort of interop easy. 41 | - **Versatile**: You have access to the full set of Node.js APIs in Swift, from JavaScript object manipulation to event loop scheduling. 42 | - **Cross-platform**: NodeSwift works not only on macOS, but also on Linux, Windows, and even iOS! 43 | 44 | ## How? 45 | 46 | A NodeSwift module consists of an [SwiftPM](https://swift.org/package-manager/) package and [NPM](https://www.npmjs.com) package in the same folder, both of which express NodeSwift as a dependency. 47 | 48 | The Swift package is exposed to JavaScript as a native Node.js module, which can be `require`'d by the JS code. The two sides communicate via [Node-API](https://nodejs.org/api/n-api.html), which is wrapped by the `NodeAPI` module on the Swift side. 49 | 50 | ## Get started 51 | 52 | For details, see the example in [/example](/example). 53 | 54 | 60 | 61 | 62 | ## Alternatives 63 | 64 | **WebAssembly** 65 | 66 | While WebAssembly is great for performance, it still runs in a virtual machine, which means it can't access native Darwin/Win32/GNU+Linux APIs. NodeSwift runs your Swift code on the bare metal, which should be even faster than WASM, in addition to unlocking access to the operating system's native APIs. 67 | 68 | On the other hand, if you want to run Swift code in the browser, WebAssembly might be the right choice since NodeSwift requires a Node.js runtime. 69 | 70 | **Other NAPI wrappers** 71 | 72 | NAPI, NAN, Neon etc. are all other options for building native Node.js modules, each with its own strengths. For example, NAPI is written in C and thus affords great portability at the cost of memory unsafety. NodeSwift is a great option if you want to enhance your JS tool on Apple platforms, if you want to bring Node.js code into your existing Swift program, or if you simply prefer Swift to C/C++/Rust/etc. 73 | -------------------------------------------------------------------------------- /Sources/CNodeAPI/module.modulemap: -------------------------------------------------------------------------------- 1 | module CNodeAPI { 2 | export Darwin.POSIX 3 | header "vendored/node_api.h" 4 | } 5 | -------------------------------------------------------------------------------- /Sources/CNodeAPI/vendored/js_native_api_types.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_JS_NATIVE_API_TYPES_H_ 2 | #define SRC_JS_NATIVE_API_TYPES_H_ 3 | 4 | // This file needs to be compatible with C compilers. 5 | // This is a public include file, and these includes have essentially 6 | // became part of it's API. 7 | #include // NOLINT(modernize-deprecated-headers) 8 | #include // NOLINT(modernize-deprecated-headers) 9 | 10 | #if !defined __cplusplus || (defined(_MSC_VER) && _MSC_VER < 1900) 11 | typedef uint16_t char16_t; 12 | #endif 13 | 14 | #ifndef NAPI_CDECL 15 | #ifdef _WIN32 16 | #define NAPI_CDECL __cdecl 17 | #else 18 | #define NAPI_CDECL 19 | #endif 20 | #endif 21 | 22 | // JSVM API types are all opaque pointers for ABI stability 23 | // typedef undefined structs instead of void* for compile time type safety 24 | typedef struct napi_env__* napi_env; 25 | typedef struct napi_value__* napi_value; 26 | typedef struct napi_ref__* napi_ref; 27 | typedef struct napi_handle_scope__* napi_handle_scope; 28 | typedef struct napi_escapable_handle_scope__* napi_escapable_handle_scope; 29 | typedef struct napi_callback_info__* napi_callback_info; 30 | typedef struct napi_deferred__* napi_deferred; 31 | 32 | typedef enum { 33 | napi_default = 0, 34 | napi_writable = 1 << 0, 35 | napi_enumerable = 1 << 1, 36 | napi_configurable = 1 << 2, 37 | 38 | // Used with napi_define_class to distinguish static properties 39 | // from instance properties. Ignored by napi_define_properties. 40 | napi_static = 1 << 10, 41 | 42 | #if NAPI_VERSION >= 8 43 | // Default for class methods. 44 | napi_default_method = napi_writable | napi_configurable, 45 | 46 | // Default for object properties, like in JS obj[prop]. 47 | napi_default_jsproperty = napi_writable | napi_enumerable | napi_configurable, 48 | #endif // NAPI_VERSION >= 8 49 | } napi_property_attributes; 50 | 51 | typedef enum { 52 | // ES6 types (corresponds to typeof) 53 | napi_undefined, 54 | napi_null, 55 | napi_boolean, 56 | napi_number, 57 | napi_string, 58 | napi_symbol, 59 | napi_object, 60 | napi_function, 61 | napi_external, 62 | napi_bigint, 63 | } napi_valuetype; 64 | 65 | typedef enum { 66 | napi_int8_array, 67 | napi_uint8_array, 68 | napi_uint8_clamped_array, 69 | napi_int16_array, 70 | napi_uint16_array, 71 | napi_int32_array, 72 | napi_uint32_array, 73 | napi_float32_array, 74 | napi_float64_array, 75 | napi_bigint64_array, 76 | napi_biguint64_array, 77 | } napi_typedarray_type; 78 | 79 | typedef enum { 80 | napi_ok, 81 | napi_invalid_arg, 82 | napi_object_expected, 83 | napi_string_expected, 84 | napi_name_expected, 85 | napi_function_expected, 86 | napi_number_expected, 87 | napi_boolean_expected, 88 | napi_array_expected, 89 | napi_generic_failure, 90 | napi_pending_exception, 91 | napi_cancelled, 92 | napi_escape_called_twice, 93 | napi_handle_scope_mismatch, 94 | napi_callback_scope_mismatch, 95 | napi_queue_full, 96 | napi_closing, 97 | napi_bigint_expected, 98 | napi_date_expected, 99 | napi_arraybuffer_expected, 100 | napi_detachable_arraybuffer_expected, 101 | napi_would_deadlock, // unused 102 | napi_no_external_buffers_allowed, 103 | napi_cannot_run_js, 104 | } napi_status; 105 | // Note: when adding a new enum value to `napi_status`, please also update 106 | // * `const int last_status` in the definition of `napi_get_last_error_info()' 107 | // in file js_native_api_v8.cc. 108 | // * `const char* error_messages[]` in file js_native_api_v8.cc with a brief 109 | // message explaining the error. 110 | // * the definition of `napi_status` in doc/api/n-api.md to reflect the newly 111 | // added value(s). 112 | 113 | typedef napi_value(NAPI_CDECL* napi_callback)(napi_env env, 114 | napi_callback_info info); 115 | typedef void(NAPI_CDECL* napi_finalize)(napi_env env, 116 | void* finalize_data, 117 | void* finalize_hint); 118 | 119 | typedef struct { 120 | // One of utf8name or name should be NULL. 121 | const char* utf8name; 122 | napi_value name; 123 | 124 | napi_callback method; 125 | napi_callback getter; 126 | napi_callback setter; 127 | napi_value value; 128 | 129 | napi_property_attributes attributes; 130 | void* data; 131 | } napi_property_descriptor; 132 | 133 | typedef struct { 134 | const char* error_message; 135 | void* engine_reserved; 136 | uint32_t engine_error_code; 137 | napi_status error_code; 138 | } napi_extended_error_info; 139 | 140 | #if NAPI_VERSION >= 6 141 | typedef enum { 142 | napi_key_include_prototypes, 143 | napi_key_own_only 144 | } napi_key_collection_mode; 145 | 146 | typedef enum { 147 | napi_key_all_properties = 0, 148 | napi_key_writable = 1, 149 | napi_key_enumerable = 1 << 1, 150 | napi_key_configurable = 1 << 2, 151 | napi_key_skip_strings = 1 << 3, 152 | napi_key_skip_symbols = 1 << 4 153 | } napi_key_filter; 154 | 155 | typedef enum { 156 | napi_key_keep_numbers, 157 | napi_key_numbers_to_strings 158 | } napi_key_conversion; 159 | #endif // NAPI_VERSION >= 6 160 | 161 | #if NAPI_VERSION >= 8 162 | typedef struct { 163 | uint64_t lower; 164 | uint64_t upper; 165 | } napi_type_tag; 166 | #endif // NAPI_VERSION >= 8 167 | 168 | #endif // SRC_JS_NATIVE_API_TYPES_H_ 169 | -------------------------------------------------------------------------------- /Sources/CNodeAPI/vendored/node_api.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_NODE_API_H_ 2 | #define SRC_NODE_API_H_ 3 | 4 | #ifdef BUILDING_NODE_EXTENSION 5 | #ifdef _WIN32 6 | // Building native addon against node 7 | #define NAPI_EXTERN __declspec(dllimport) 8 | #elif defined(__wasm32__) 9 | #define NAPI_EXTERN __attribute__((__import_module__("napi"))) 10 | #endif 11 | #endif 12 | #include "js_native_api.h" 13 | #include "node_api_types.h" 14 | 15 | struct uv_loop_s; // Forward declaration. 16 | 17 | #ifdef _WIN32 18 | #define NAPI_MODULE_EXPORT __declspec(dllexport) 19 | #else 20 | #define NAPI_MODULE_EXPORT __attribute__((visibility("default"))) 21 | #endif 22 | 23 | #if defined(__GNUC__) 24 | #define NAPI_NO_RETURN __attribute__((noreturn)) 25 | #elif defined(_WIN32) 26 | #define NAPI_NO_RETURN __declspec(noreturn) 27 | #else 28 | #define NAPI_NO_RETURN 29 | #endif 30 | 31 | typedef napi_value(NAPI_CDECL* napi_addon_register_func)(napi_env env, 32 | napi_value exports); 33 | typedef int32_t(NAPI_CDECL* node_api_addon_get_api_version_func)(); 34 | 35 | // Used by deprecated registration method napi_module_register. 36 | typedef struct napi_module { 37 | int nm_version; 38 | unsigned int nm_flags; 39 | const char* nm_filename; 40 | napi_addon_register_func nm_register_func; 41 | const char* nm_modname; 42 | void* nm_priv; 43 | void* reserved[4]; 44 | } napi_module; 45 | 46 | #define NAPI_MODULE_VERSION 1 47 | 48 | #define NAPI_MODULE_INITIALIZER_X(base, version) \ 49 | NAPI_MODULE_INITIALIZER_X_HELPER(base, version) 50 | #define NAPI_MODULE_INITIALIZER_X_HELPER(base, version) base##version 51 | 52 | #ifdef __wasm32__ 53 | #define NAPI_MODULE_INITIALIZER_BASE napi_register_wasm_v 54 | #else 55 | #define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v 56 | #endif 57 | 58 | #define NODE_API_MODULE_GET_API_VERSION_BASE node_api_module_get_api_version_v 59 | 60 | #define NAPI_MODULE_INITIALIZER \ 61 | NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION) 62 | 63 | #define NODE_API_MODULE_GET_API_VERSION \ 64 | NAPI_MODULE_INITIALIZER_X(NODE_API_MODULE_GET_API_VERSION_BASE, \ 65 | NAPI_MODULE_VERSION) 66 | 67 | #define NAPI_MODULE_INIT() \ 68 | EXTERN_C_START \ 69 | NAPI_MODULE_EXPORT int32_t NODE_API_MODULE_GET_API_VERSION() { \ 70 | return NAPI_VERSION; \ 71 | } \ 72 | NAPI_MODULE_EXPORT napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ 73 | napi_value exports); \ 74 | EXTERN_C_END \ 75 | napi_value NAPI_MODULE_INITIALIZER(napi_env env, napi_value exports) 76 | 77 | #define NAPI_MODULE(modname, regfunc) \ 78 | NAPI_MODULE_INIT() { return regfunc(env, exports); } 79 | 80 | // Deprecated. Use NAPI_MODULE. 81 | #define NAPI_MODULE_X(modname, regfunc, priv, flags) \ 82 | NAPI_MODULE(modname, regfunc) 83 | 84 | EXTERN_C_START 85 | 86 | // Deprecated. Replaced by symbol-based registration defined by NAPI_MODULE 87 | // and NAPI_MODULE_INIT macros. 88 | #if defined(__cplusplus) && __cplusplus >= 201402L 89 | [[deprecated]] 90 | #endif 91 | NAPI_EXTERN void NAPI_CDECL 92 | napi_module_register(napi_module* mod); 93 | 94 | NAPI_EXTERN NAPI_NO_RETURN void NAPI_CDECL 95 | napi_fatal_error(const char* location, 96 | size_t location_len, 97 | const char* message, 98 | size_t message_len); 99 | 100 | // Methods for custom handling of async operations 101 | NAPI_EXTERN napi_status NAPI_CDECL 102 | napi_async_init(napi_env env, 103 | napi_value async_resource, 104 | napi_value async_resource_name, 105 | napi_async_context* result); 106 | 107 | NAPI_EXTERN napi_status NAPI_CDECL 108 | napi_async_destroy(napi_env env, napi_async_context async_context); 109 | 110 | NAPI_EXTERN napi_status NAPI_CDECL 111 | napi_make_callback(napi_env env, 112 | napi_async_context async_context, 113 | napi_value recv, 114 | napi_value func, 115 | size_t argc, 116 | const napi_value* argv, 117 | napi_value* result); 118 | 119 | // Methods to provide node::Buffer functionality with napi types 120 | NAPI_EXTERN napi_status NAPI_CDECL napi_create_buffer(napi_env env, 121 | size_t length, 122 | void** data, 123 | napi_value* result); 124 | #ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED 125 | NAPI_EXTERN napi_status NAPI_CDECL 126 | napi_create_external_buffer(napi_env env, 127 | size_t length, 128 | void* data, 129 | napi_finalize finalize_cb, 130 | void* finalize_hint, 131 | napi_value* result); 132 | #endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED 133 | NAPI_EXTERN napi_status NAPI_CDECL napi_create_buffer_copy(napi_env env, 134 | size_t length, 135 | const void* data, 136 | void** result_data, 137 | napi_value* result); 138 | NAPI_EXTERN napi_status NAPI_CDECL napi_is_buffer(napi_env env, 139 | napi_value value, 140 | bool* result); 141 | NAPI_EXTERN napi_status NAPI_CDECL napi_get_buffer_info(napi_env env, 142 | napi_value value, 143 | void** data, 144 | size_t* length); 145 | 146 | #ifndef __wasm32__ 147 | // Methods to manage simple async operations 148 | NAPI_EXTERN napi_status NAPI_CDECL 149 | napi_create_async_work(napi_env env, 150 | napi_value async_resource, 151 | napi_value async_resource_name, 152 | napi_async_execute_callback execute, 153 | napi_async_complete_callback complete, 154 | void* data, 155 | napi_async_work* result); 156 | NAPI_EXTERN napi_status NAPI_CDECL napi_delete_async_work(napi_env env, 157 | napi_async_work work); 158 | NAPI_EXTERN napi_status NAPI_CDECL napi_queue_async_work(napi_env env, 159 | napi_async_work work); 160 | NAPI_EXTERN napi_status NAPI_CDECL napi_cancel_async_work(napi_env env, 161 | napi_async_work work); 162 | #endif // __wasm32__ 163 | 164 | // version management 165 | NAPI_EXTERN napi_status NAPI_CDECL 166 | napi_get_node_version(napi_env env, const napi_node_version** version); 167 | 168 | #if NAPI_VERSION >= 2 169 | 170 | // Return the current libuv event loop for a given environment 171 | NAPI_EXTERN napi_status NAPI_CDECL 172 | napi_get_uv_event_loop(napi_env env, struct uv_loop_s** loop); 173 | 174 | #endif // NAPI_VERSION >= 2 175 | 176 | #if NAPI_VERSION >= 3 177 | 178 | NAPI_EXTERN napi_status NAPI_CDECL napi_fatal_exception(napi_env env, 179 | napi_value err); 180 | 181 | NAPI_EXTERN napi_status NAPI_CDECL 182 | napi_add_env_cleanup_hook(napi_env env, napi_cleanup_hook fun, void* arg); 183 | 184 | NAPI_EXTERN napi_status NAPI_CDECL 185 | napi_remove_env_cleanup_hook(napi_env env, napi_cleanup_hook fun, void* arg); 186 | 187 | NAPI_EXTERN napi_status NAPI_CDECL 188 | napi_open_callback_scope(napi_env env, 189 | napi_value resource_object, 190 | napi_async_context context, 191 | napi_callback_scope* result); 192 | 193 | NAPI_EXTERN napi_status NAPI_CDECL 194 | napi_close_callback_scope(napi_env env, napi_callback_scope scope); 195 | 196 | #endif // NAPI_VERSION >= 3 197 | 198 | #if NAPI_VERSION >= 4 199 | 200 | #ifndef __wasm32__ 201 | // Calling into JS from other threads 202 | NAPI_EXTERN napi_status NAPI_CDECL 203 | napi_create_threadsafe_function(napi_env env, 204 | napi_value func, 205 | napi_value async_resource, 206 | napi_value async_resource_name, 207 | size_t max_queue_size, 208 | size_t initial_thread_count, 209 | void* thread_finalize_data, 210 | napi_finalize thread_finalize_cb, 211 | void* context, 212 | napi_threadsafe_function_call_js call_js_cb, 213 | napi_threadsafe_function* result); 214 | 215 | NAPI_EXTERN napi_status NAPI_CDECL napi_get_threadsafe_function_context( 216 | napi_threadsafe_function func, void** result); 217 | 218 | NAPI_EXTERN napi_status NAPI_CDECL 219 | napi_call_threadsafe_function(napi_threadsafe_function func, 220 | void* data, 221 | napi_threadsafe_function_call_mode is_blocking); 222 | 223 | NAPI_EXTERN napi_status NAPI_CDECL 224 | napi_acquire_threadsafe_function(napi_threadsafe_function func); 225 | 226 | NAPI_EXTERN napi_status NAPI_CDECL napi_release_threadsafe_function( 227 | napi_threadsafe_function func, napi_threadsafe_function_release_mode mode); 228 | 229 | NAPI_EXTERN napi_status NAPI_CDECL 230 | napi_unref_threadsafe_function(napi_env env, napi_threadsafe_function func); 231 | 232 | NAPI_EXTERN napi_status NAPI_CDECL 233 | napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func); 234 | #endif // __wasm32__ 235 | 236 | #endif // NAPI_VERSION >= 4 237 | 238 | #if NAPI_VERSION >= 8 239 | 240 | NAPI_EXTERN napi_status NAPI_CDECL 241 | napi_add_async_cleanup_hook(napi_env env, 242 | napi_async_cleanup_hook hook, 243 | void* arg, 244 | napi_async_cleanup_hook_handle* remove_handle); 245 | 246 | NAPI_EXTERN napi_status NAPI_CDECL 247 | napi_remove_async_cleanup_hook(napi_async_cleanup_hook_handle remove_handle); 248 | 249 | #endif // NAPI_VERSION >= 8 250 | 251 | #if NAPI_VERSION >= 9 252 | 253 | NAPI_EXTERN napi_status NAPI_CDECL 254 | node_api_get_module_file_name(napi_env env, const char** result); 255 | 256 | #endif // NAPI_VERSION >= 9 257 | 258 | EXTERN_C_END 259 | 260 | #endif // SRC_NODE_API_H_ 261 | -------------------------------------------------------------------------------- /Sources/CNodeAPI/vendored/node_api_types.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_NODE_API_TYPES_H_ 2 | #define SRC_NODE_API_TYPES_H_ 3 | 4 | #include "js_native_api_types.h" 5 | 6 | typedef struct napi_callback_scope__* napi_callback_scope; 7 | typedef struct napi_async_context__* napi_async_context; 8 | typedef struct napi_async_work__* napi_async_work; 9 | 10 | #if NAPI_VERSION >= 3 11 | typedef void(NAPI_CDECL* napi_cleanup_hook)(void* arg); 12 | #endif // NAPI_VERSION >= 3 13 | 14 | #if NAPI_VERSION >= 4 15 | typedef struct napi_threadsafe_function__* napi_threadsafe_function; 16 | #endif // NAPI_VERSION >= 4 17 | 18 | #if NAPI_VERSION >= 4 19 | typedef enum { 20 | napi_tsfn_release, 21 | napi_tsfn_abort 22 | } napi_threadsafe_function_release_mode; 23 | 24 | typedef enum { 25 | napi_tsfn_nonblocking, 26 | napi_tsfn_blocking 27 | } napi_threadsafe_function_call_mode; 28 | #endif // NAPI_VERSION >= 4 29 | 30 | typedef void(NAPI_CDECL* napi_async_execute_callback)(napi_env env, void* data); 31 | typedef void(NAPI_CDECL* napi_async_complete_callback)(napi_env env, 32 | napi_status status, 33 | void* data); 34 | #if NAPI_VERSION >= 4 35 | typedef void(NAPI_CDECL* napi_threadsafe_function_call_js)( 36 | napi_env env, napi_value js_callback, void* context, void* data); 37 | #endif // NAPI_VERSION >= 4 38 | 39 | typedef struct { 40 | uint32_t major; 41 | uint32_t minor; 42 | uint32_t patch; 43 | const char* release; 44 | } napi_node_version; 45 | 46 | #if NAPI_VERSION >= 8 47 | typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle; 48 | typedef void(NAPI_CDECL* napi_async_cleanup_hook)( 49 | napi_async_cleanup_hook_handle handle, void* data); 50 | #endif // NAPI_VERSION >= 8 51 | 52 | #endif // SRC_NODE_API_TYPES_H_ 53 | -------------------------------------------------------------------------------- /Sources/CNodeAPISupport/include/node_context.h: -------------------------------------------------------------------------------- 1 | #ifndef node_context_h 2 | #define node_context_h 3 | 4 | _Pragma("clang assume_nonnull begin") 5 | 6 | // all thread-specific 7 | const void * _Nullable node_swift_context_peek(void); 8 | const void * _Nullable node_swift_context_pop(void); 9 | void node_swift_context_push(const void *value); 10 | 11 | _Pragma("clang assume_nonnull end") 12 | 13 | #endif /* node_context_h */ 14 | -------------------------------------------------------------------------------- /Sources/CNodeAPISupport/node_context.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | struct context_list { 5 | const void *value; 6 | struct context_list *next; 7 | }; 8 | 9 | static __thread struct context_list *list_head = NULL; 10 | 11 | const void *node_swift_context_peek(void) { 12 | if (!list_head) return NULL; 13 | return list_head->value; 14 | } 15 | 16 | const void *node_swift_context_pop(void) { 17 | if (!list_head) return NULL; 18 | struct context_list *old_head = list_head; 19 | list_head = old_head->next; 20 | const void *value = old_head->value; 21 | free(old_head); 22 | return value; 23 | } 24 | 25 | void node_swift_context_push(const void *value) { 26 | struct context_list *ctx = malloc(sizeof(*ctx)); 27 | ctx->value = value; 28 | ctx->next = list_head; 29 | list_head = ctx; 30 | } 31 | -------------------------------------------------------------------------------- /Sources/CNodeJSC/include/embedder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct napi_env__* napi_env; 6 | 7 | typedef void (*napi_executor_free)(void *); 8 | typedef void (*napi_executor_assert_current)(void *); 9 | typedef void (*napi_executor_dispatch_async)(void *, void(*)(void *), void *); 10 | 11 | typedef struct napi_executor { 12 | uint64_t version; // should be 1 13 | void *context; 14 | napi_executor_free free; 15 | napi_executor_assert_current assert_current; 16 | napi_executor_dispatch_async dispatch_async; 17 | } napi_executor; 18 | 19 | #ifdef __cplusplus 20 | #define NAPI_JSC_EXTERN_C extern "C" 21 | #else 22 | #define NAPI_JSC_EXTERN_C 23 | #endif 24 | 25 | NAPI_JSC_EXTERN_C napi_env napi_env_jsc_create(JSGlobalContextRef context, napi_executor executor); 26 | NAPI_JSC_EXTERN_C void napi_env_jsc_delete(napi_env env); 27 | -------------------------------------------------------------------------------- /Sources/NodeAPI/Locks.swift: -------------------------------------------------------------------------------- 1 | // Taken directly from 2 | // https://github.com/apple/swift-log/blob/3577a992d373d0e7f75d1fdc6e6e4570b60a1e1f/Sources/Logging/Locks.swift 3 | 4 | //===----------------------------------------------------------------------===// 5 | // 6 | // This source file is part of the Swift Logging API open source project 7 | // 8 | // Copyright (c) 2018-2019 Apple Inc. and the Swift Logging API project authors 9 | // Licensed under Apache License v2.0 10 | // 11 | // See LICENSE.txt for license information 12 | // See CONTRIBUTORS.txt for the list of Swift Logging API project authors 13 | // 14 | // SPDX-License-Identifier: Apache-2.0 15 | // 16 | //===----------------------------------------------------------------------===// 17 | 18 | //===----------------------------------------------------------------------===// 19 | // 20 | // This source file is part of the SwiftNIO open source project 21 | // 22 | // Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors 23 | // Licensed under Apache License v2.0 24 | // 25 | // See LICENSE.txt for license information 26 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 27 | // 28 | // SPDX-License-Identifier: Apache-2.0 29 | // 30 | //===----------------------------------------------------------------------===// 31 | 32 | #if canImport(WASILibc) 33 | // No locking on WASILibc 34 | #else 35 | 36 | #if canImport(Darwin) 37 | import Darwin 38 | #elseif os(Windows) 39 | import WinSDK 40 | #elseif canImport(Glibc) 41 | import Glibc 42 | #else 43 | #error("Unsupported runtime") 44 | #endif 45 | 46 | /// A threading lock based on `libpthread` instead of `libdispatch`. 47 | /// 48 | /// This object provides a lock on top of a single `pthread_mutex_t`. This kind 49 | /// of lock is safe to use with `libpthread`-based threading models, such as the 50 | /// one used by NIO. On Windows, the lock is based on the substantially similar 51 | /// `SRWLOCK` type. 52 | internal final class Lock: @unchecked Sendable { 53 | #if os(Windows) 54 | fileprivate let mutex: UnsafeMutablePointer = 55 | UnsafeMutablePointer.allocate(capacity: 1) 56 | #else 57 | fileprivate let mutex: UnsafeMutablePointer = 58 | UnsafeMutablePointer.allocate(capacity: 1) 59 | #endif 60 | 61 | /// Create a new lock. 62 | public init() { 63 | #if os(Windows) 64 | InitializeSRWLock(self.mutex) 65 | #else 66 | var attr = pthread_mutexattr_t() 67 | pthread_mutexattr_init(&attr) 68 | pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) 69 | 70 | let err = pthread_mutex_init(self.mutex, &attr) 71 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 72 | #endif 73 | } 74 | 75 | deinit { 76 | #if os(Windows) 77 | // SRWLOCK does not need to be free'd 78 | #else 79 | let err = pthread_mutex_destroy(self.mutex) 80 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 81 | #endif 82 | self.mutex.deallocate() 83 | } 84 | 85 | /// Acquire the lock. 86 | /// 87 | /// Whenever possible, consider using `withLock` instead of this method and 88 | /// `unlock`, to simplify lock handling. 89 | public func lock() { 90 | #if os(Windows) 91 | AcquireSRWLockExclusive(self.mutex) 92 | #else 93 | let err = pthread_mutex_lock(self.mutex) 94 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 95 | #endif 96 | } 97 | 98 | /// Release the lock. 99 | /// 100 | /// Whenever possible, consider using `withLock` instead of this method and 101 | /// `lock`, to simplify lock handling. 102 | public func unlock() { 103 | #if os(Windows) 104 | ReleaseSRWLockExclusive(self.mutex) 105 | #else 106 | let err = pthread_mutex_unlock(self.mutex) 107 | precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") 108 | #endif 109 | } 110 | } 111 | 112 | extension Lock { 113 | /// Acquire the lock for the duration of the given block. 114 | /// 115 | /// This convenience method should be preferred to `lock` and `unlock` in 116 | /// most situations, as it ensures that the lock will be released regardless 117 | /// of how `body` exits. 118 | /// 119 | /// - Parameter body: The block to execute while holding the lock. 120 | /// - Returns: The value returned by the block. 121 | @inlinable 122 | internal func withLock(_ body: () throws -> T) rethrows -> T { 123 | self.lock() 124 | defer { 125 | self.unlock() 126 | } 127 | return try body() 128 | } 129 | 130 | // specialise Void return (for performance) 131 | @inlinable 132 | internal func withLockVoid(_ body: () throws -> Void) rethrows { 133 | try self.withLock(body) 134 | } 135 | } 136 | 137 | /// A reader/writer threading lock based on `libpthread` instead of `libdispatch`. 138 | /// 139 | /// This object provides a lock on top of a single `pthread_rwlock_t`. This kind 140 | /// of lock is safe to use with `libpthread`-based threading models, such as the 141 | /// one used by NIO. On Windows, the lock is based on the substantially similar 142 | /// `SRWLOCK` type. 143 | internal final class ReadWriteLock: @unchecked Sendable { 144 | #if os(Windows) 145 | fileprivate let rwlock: UnsafeMutablePointer = 146 | UnsafeMutablePointer.allocate(capacity: 1) 147 | fileprivate var shared: Bool = true 148 | #else 149 | fileprivate let rwlock: UnsafeMutablePointer = 150 | UnsafeMutablePointer.allocate(capacity: 1) 151 | #endif 152 | 153 | /// Create a new lock. 154 | public init() { 155 | #if os(Windows) 156 | InitializeSRWLock(self.rwlock) 157 | #else 158 | let err = pthread_rwlock_init(self.rwlock, nil) 159 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 160 | #endif 161 | } 162 | 163 | deinit { 164 | #if os(Windows) 165 | // SRWLOCK does not need to be free'd 166 | #else 167 | let err = pthread_rwlock_destroy(self.rwlock) 168 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 169 | #endif 170 | self.rwlock.deallocate() 171 | } 172 | 173 | /// Acquire a reader lock. 174 | /// 175 | /// Whenever possible, consider using `withReaderLock` instead of this 176 | /// method and `unlock`, to simplify lock handling. 177 | public func lockRead() { 178 | #if os(Windows) 179 | AcquireSRWLockShared(self.rwlock) 180 | self.shared = true 181 | #else 182 | let err = pthread_rwlock_rdlock(self.rwlock) 183 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 184 | #endif 185 | } 186 | 187 | /// Acquire a writer lock. 188 | /// 189 | /// Whenever possible, consider using `withWriterLock` instead of this 190 | /// method and `unlock`, to simplify lock handling. 191 | public func lockWrite() { 192 | #if os(Windows) 193 | AcquireSRWLockExclusive(self.rwlock) 194 | self.shared = false 195 | #else 196 | let err = pthread_rwlock_wrlock(self.rwlock) 197 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 198 | #endif 199 | } 200 | 201 | /// Release the lock. 202 | /// 203 | /// Whenever possible, consider using `withReaderLock` and `withWriterLock` 204 | /// instead of this method and `lockRead` and `lockWrite`, to simplify lock 205 | /// handling. 206 | public func unlock() { 207 | #if os(Windows) 208 | if self.shared { 209 | ReleaseSRWLockShared(self.rwlock) 210 | } else { 211 | ReleaseSRWLockExclusive(self.rwlock) 212 | } 213 | #else 214 | let err = pthread_rwlock_unlock(self.rwlock) 215 | precondition(err == 0, "\(#function) failed in pthread_rwlock with error \(err)") 216 | #endif 217 | } 218 | } 219 | 220 | extension ReadWriteLock { 221 | /// Acquire the reader lock for the duration of the given block. 222 | /// 223 | /// This convenience method should be preferred to `lockRead` and `unlock` 224 | /// in most situations, as it ensures that the lock will be released 225 | /// regardless of how `body` exits. 226 | /// 227 | /// - Parameter body: The block to execute while holding the reader lock. 228 | /// - Returns: The value returned by the block. 229 | @inlinable 230 | internal func withReaderLock(_ body: () throws -> T) rethrows -> T { 231 | self.lockRead() 232 | defer { 233 | self.unlock() 234 | } 235 | return try body() 236 | } 237 | 238 | /// Acquire the writer lock for the duration of the given block. 239 | /// 240 | /// This convenience method should be preferred to `lockWrite` and `unlock` 241 | /// in most situations, as it ensures that the lock will be released 242 | /// regardless of how `body` exits. 243 | /// 244 | /// - Parameter body: The block to execute while holding the writer lock. 245 | /// - Returns: The value returned by the block. 246 | @inlinable 247 | internal func withWriterLock(_ body: () throws -> T) rethrows -> T { 248 | self.lockWrite() 249 | defer { 250 | self.unlock() 251 | } 252 | return try body() 253 | } 254 | 255 | // specialise Void return (for performance) 256 | @inlinable 257 | internal func withReaderLockVoid(_ body: () throws -> Void) rethrows { 258 | try self.withReaderLock(body) 259 | } 260 | 261 | // specialise Void return (for performance) 262 | @inlinable 263 | internal func withWriterLockVoid(_ body: () throws -> Void) rethrows { 264 | try self.withWriterLock(body) 265 | } 266 | } 267 | #endif 268 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeAPIError.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public struct NodeAPIError: Error { 4 | public enum Code: Sendable { 5 | case invalidArg 6 | case objectExpected 7 | case stringExpected 8 | case nameExpected 9 | case functionExpected 10 | case numberExpected 11 | case booleanExpected 12 | case arrayExpected 13 | case genericFailure 14 | case pendingException 15 | case cancelled 16 | case escapeCalledTwice 17 | case handleScopeMismatch 18 | case callbackScopeMismatch 19 | case queueFull 20 | case closing 21 | case bigintExpected 22 | case dateExpected 23 | case arraybufferExpected 24 | case detachableArraybufferExpected 25 | case wouldDeadlock 26 | 27 | init?(status: napi_status) { 28 | switch status { 29 | case napi_ok: 30 | return nil 31 | case napi_invalid_arg: 32 | self = .invalidArg 33 | case napi_object_expected: 34 | self = .objectExpected 35 | case napi_string_expected: 36 | self = .stringExpected 37 | case napi_name_expected: 38 | self = .nameExpected 39 | case napi_function_expected: 40 | self = .functionExpected 41 | case napi_number_expected: 42 | self = .numberExpected 43 | case napi_boolean_expected: 44 | self = .booleanExpected 45 | case napi_array_expected: 46 | self = .arrayExpected 47 | case napi_generic_failure: 48 | self = .genericFailure 49 | case napi_pending_exception: 50 | self = .pendingException 51 | case napi_cancelled: 52 | self = .cancelled 53 | case napi_escape_called_twice: 54 | self = .escapeCalledTwice 55 | case napi_handle_scope_mismatch: 56 | self = .handleScopeMismatch 57 | case napi_callback_scope_mismatch: 58 | self = .callbackScopeMismatch 59 | case napi_queue_full: 60 | self = .queueFull 61 | case napi_closing: 62 | self = .closing 63 | case napi_bigint_expected: 64 | self = .bigintExpected 65 | case napi_date_expected: 66 | self = .dateExpected 67 | case napi_arraybuffer_expected: 68 | self = .arraybufferExpected 69 | case napi_detachable_arraybuffer_expected: 70 | self = .detachableArraybufferExpected 71 | case napi_would_deadlock: 72 | self = .wouldDeadlock 73 | default: 74 | self = .genericFailure 75 | } 76 | } 77 | } 78 | 79 | public struct Details: Sendable { 80 | public let message: String? 81 | public let engineErrorCode: UInt32 82 | 83 | init(message: String?, engineErrorCode: UInt32) { 84 | self.message = message 85 | self.engineErrorCode = engineErrorCode 86 | } 87 | 88 | init(raw: napi_extended_error_info) { 89 | self.message = raw.error_message.map(String.init(cString:)) 90 | self.engineErrorCode = raw.engine_error_code 91 | } 92 | } 93 | 94 | let code: Code 95 | var details: Details? 96 | 97 | init(_ code: Code, details: Details? = nil) { 98 | self.code = code 99 | self.details = details 100 | } 101 | 102 | init(_ code: Code, message: String) { 103 | self.code = code 104 | self.details = .init(message: message, engineErrorCode: 0) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeActor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | internal import CNodeAPI 3 | 4 | extension NodeContext { 5 | // if we're on a node thread, run `action` on it 6 | static func runOnActor(_ action: @NodeActor @Sendable () throws -> T) rethrows -> T? { 7 | guard NodeContext.hasCurrent else { return nil } 8 | return try NodeActor.unsafeAssumeIsolated(action) 9 | } 10 | } 11 | 12 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 13 | private final class NodeExecutor: SerialExecutor { 14 | private let schedulerQueue = DispatchQueue(label: "NodeExecutorScheduler") 15 | 16 | fileprivate init() { 17 | // Swift often thinks that we're on the wrong executor, so we end up 18 | // with a lot of false alarms. This is what `checkIsolation` ostensibly 19 | // mitigates, but on Darwin that method doesn't seem to be called in many 20 | // circumstances (pre macOS 15, but also on macOS 15 if the host node binary 21 | // is built with an older SDK.) Best we can do is disable the checks for now. 22 | #if canImport(Darwin) 23 | setenv("SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL", "0", 1) 24 | #endif 25 | } 26 | 27 | func enqueue(_ job: UnownedJob) { 28 | // We want to enqueue the job on the "current" NodeActor, which is 29 | // stored in task-local storage. In the Swift 6 compiler, 30 | // NodeExecutor.enqueue is invoked with the same isolation as the caller, 31 | // which means we can simply read out the TaskLocal value to obtain 32 | // this. 33 | let target = NodeActor.target 34 | 35 | guard let q = target?.queue else { 36 | nodeFatalError("There is no target NodeAsyncQueue associated with this Task") 37 | } 38 | 39 | let ref = self.asUnownedSerialExecutor() 40 | 41 | do { 42 | try q.run { job.runSynchronously(on: ref) } 43 | } catch { 44 | nodeFatalError("Could not execute job on NodeActor: \(error)") 45 | } 46 | } 47 | 48 | func asUnownedSerialExecutor() -> UnownedSerialExecutor { 49 | .init(ordinary: self) 50 | } 51 | 52 | func checkIsolated() { 53 | // TODO: crash if we're not on a Node thread 54 | } 55 | } 56 | 57 | // This isn't *actually* a single global actor. Rather, its associated 58 | // serial executor runs jobs on the task-local "target" NodeAsyncQueue. 59 | // As a result, if you switch Node instances (eg from the main thread 60 | // onto a worker) you should still be wary of using values from one 61 | // instance in another. Similarly, trying to run on NodeActor from a 62 | // Task.detatched closure will crash. 63 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 64 | @globalActor public actor NodeActor { 65 | private init() {} 66 | public static let shared = NodeActor() 67 | 68 | @TaskLocal static var target: NodeAsyncQueue.Handle? 69 | 70 | private nonisolated let executor = NodeExecutor() 71 | public nonisolated var unownedExecutor: UnownedSerialExecutor { 72 | executor.asUnownedSerialExecutor() 73 | } 74 | 75 | public static func run(resultType: T.Type = T.self, body: @NodeActor @Sendable () throws -> T) async rethrows -> T { 76 | try await body() 77 | } 78 | } 79 | 80 | extension NodeActor { 81 | public static func unsafeAssumeIsolated(_ action: @NodeActor @Sendable () throws -> T) rethrows -> T { 82 | try withoutActuallyEscaping(action) { 83 | try unsafeBitCast($0, to: (() throws -> T).self)() 84 | } 85 | } 86 | 87 | public static func assumeIsolated( 88 | _ action: @NodeActor @Sendable () throws -> T, 89 | file: StaticString = #fileID, 90 | line: UInt = #line 91 | ) rethrows -> T { 92 | guard NodeContext.hasCurrent else { 93 | nodeFatalError("NodeActor.assumeIsolated failed", file: file, line: line) 94 | } 95 | return try unsafeAssumeIsolated(action) 96 | } 97 | } 98 | 99 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 100 | extension Task where Failure == Never { 101 | // if it's absolutely necessary to create a detached task, use this 102 | // instead of Task.detached since the latter doesn't inherit any 103 | // task-locals, which means the current Node instance won't be 104 | // preserved; this explicitly restores the NodeAsyncQueue local. 105 | @discardableResult 106 | public static func nodeDetached(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success) -> Task { 107 | Task.detached(priority: priority) { [t = NodeActor.target] in 108 | await NodeActor.$target.withValue(t, operation: operation) 109 | } 110 | } 111 | } 112 | 113 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 114 | extension Task where Failure == Error { 115 | @discardableResult 116 | public static func nodeDetached(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) -> Task { 117 | Task.detached(priority: priority) { [t = NodeActor.target] in 118 | try await NodeActor.$target.withValue(t, operation: operation) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeArray.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeArray: NodeObject { 4 | 5 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 6 | super.init(base) 7 | } 8 | 9 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 10 | let env = value.environment 11 | var result = false 12 | try env.check(napi_is_array(env.raw, value.rawValue(), &result)) 13 | return result 14 | } 15 | 16 | // capacity is the initial capacity; the array can still grow 17 | public init(capacity: Int? = nil) throws { 18 | let ctx = NodeContext.current 19 | let env = ctx.environment 20 | var result: napi_value! 21 | if let length = capacity { 22 | try env.check(napi_create_array_with_length(env.raw, length, &result)) 23 | } else { 24 | try env.check(napi_create_array(env.raw, &result)) 25 | } 26 | super.init(NodeValueBase(raw: result, in: ctx)) 27 | } 28 | 29 | public func count() throws -> Int { 30 | let env = base.environment 31 | var length: UInt32 = 0 32 | try env.check(napi_get_array_length(env.raw, base.rawValue(), &length)) 33 | return Int(length) 34 | } 35 | 36 | } 37 | 38 | extension Array: NodeValueConvertible, NodeObjectConvertible, NodePropertyConvertible 39 | where Element == NodeValueConvertible { 40 | public func nodeValue() throws -> NodeValue { 41 | let arr = try NodeArray(capacity: count) 42 | for (idx, element) in enumerated() { 43 | try arr[idx].set(to: element) 44 | } 45 | return arr 46 | } 47 | } 48 | 49 | extension Array: NodeValueCreatable, AnyNodeValueCreatable where Element == NodeValue { 50 | public static func from(_ value: NodeArray) throws -> [Element] { 51 | try (0.. Void 6 | 7 | public init(action: @escaping @Sendable @NodeActor (UnsafeMutableRawBufferPointer) -> Void) { 8 | self.action = action 9 | } 10 | 11 | public static let deallocate = Self { $0.deallocate() } 12 | 13 | // retains the object until the deallocator is called, after which 14 | // it's released 15 | public static func capture(_ object: some Sendable) -> Self { 16 | .init { _ in _ = object } 17 | } 18 | } 19 | 20 | typealias Hint = Box<(NodeDataDeallocator, UnsafeMutableRawBufferPointer)> 21 | 22 | public final class NodeArrayBuffer: NodeObject { 23 | 24 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 25 | let env = value.environment 26 | var result = false 27 | try env.check(napi_is_arraybuffer(env.raw, value.rawValue(), &result)) 28 | return result 29 | } 30 | 31 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 32 | super.init(base) 33 | } 34 | 35 | public init(capacity: Int) throws { 36 | let ctx = NodeContext.current 37 | let env = ctx.environment 38 | var data: UnsafeMutableRawPointer? 39 | var result: napi_value! 40 | try env.check(napi_create_arraybuffer(env.raw, capacity, &data, &result)) 41 | super.init(NodeValueBase(raw: result, in: ctx)) 42 | } 43 | 44 | // bytes must remain valid while the object is alive (i.e. until 45 | // deallocator is called) 46 | public init(bytes: UnsafeMutableRawBufferPointer, deallocator: NodeDataDeallocator) throws { 47 | let ctx = NodeContext.current 48 | let env = ctx.environment 49 | var result: napi_value! 50 | let hint = Unmanaged.passRetained(Hint((deallocator, bytes))).toOpaque() 51 | try env.check(napi_create_external_arraybuffer(env.raw, bytes.baseAddress, bytes.count, { rawEnv, _, hint in 52 | NodeContext.withUnsafeEntrypoint(rawEnv!) { _ in 53 | let (deallocator, bytes) = Unmanaged.fromOpaque(hint!).takeRetainedValue().value 54 | deallocator.action(bytes) 55 | } 56 | }, hint, &result)) 57 | super.init(NodeValueBase(raw: result, in: ctx)) 58 | } 59 | 60 | public convenience init(data: NSMutableData) throws { 61 | try self.init( 62 | bytes: UnsafeMutableRawBufferPointer(start: data.mutableBytes, count: data.length), 63 | deallocator: .capture(UncheckedSendable(data)) 64 | ) 65 | } 66 | 67 | public func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> T) throws -> T { 68 | let env = base.environment 69 | var data: UnsafeMutableRawPointer? 70 | var count = 0 71 | try env.check(napi_get_arraybuffer_info(env.raw, base.rawValue(), &data, &count)) 72 | return try body(UnsafeMutableRawBufferPointer(start: data, count: count)) 73 | } 74 | 75 | #if !NAPI_VERSIONED || NAPI_GE_7 76 | 77 | public func detach() throws { 78 | try base.environment.check( 79 | napi_detach_arraybuffer(base.environment.raw, base.rawValue()) 80 | ) 81 | } 82 | 83 | public func isDetached() throws -> Bool { 84 | var result = false 85 | try base.environment.check( 86 | napi_is_detached_arraybuffer(base.environment.raw, base.rawValue(), &result) 87 | ) 88 | return result 89 | } 90 | 91 | #endif 92 | 93 | } 94 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeAsyncQueue.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | import Foundation 3 | 4 | extension NodeEnvironment { 5 | @NodeInstanceData private static var id: UUID? 6 | func instanceID() throws -> UUID { 7 | if let id = Self.id { return id } 8 | let id = UUID() 9 | Self.id = id 10 | return id 11 | } 12 | } 13 | 14 | private class Token {} 15 | private typealias CallbackBox = Box<(NodeEnvironment) -> Void> 16 | 17 | private func cCallback( 18 | env: napi_env?, cb: napi_value?, 19 | context: UnsafeMutableRawPointer!, data: UnsafeMutableRawPointer! 20 | ) { 21 | let callback = Unmanaged.fromOpaque(data).takeRetainedValue() 22 | 23 | guard let env = env else { return } 24 | 25 | // we DON'T create a new NodeContext here. See handle.deinit for rationale. 26 | callback.value(NodeEnvironment(env)) 27 | } 28 | 29 | private let cCallbackC: napi_threadsafe_function_call_js = { 30 | cCallback(env: $0, cb: $1, context: $2, data: $3) 31 | } 32 | 33 | // this is the only async API we implement because it's more or less isomorphic 34 | // to napi_async_init+napi_[open|close]_callback_scope (which are in turn 35 | // supersets of the other async APIs) and unlike the callback APIs, where you 36 | // need to figure out how to get back onto the main event loop yourself 37 | // (probably using libuv), this API does that for you. 38 | 39 | // a queue that allows dispatching closures to the JS thread it was initialized on. 40 | public final class NodeAsyncQueue: @unchecked Sendable { 41 | // tsfnToken is effectively an atomic indicator of whether 42 | // the tsfn finalizer has been called. The only thing 43 | // holding a strong ref to this is the threadsafe 44 | // function itself, and that ref is released when cFinalizer 45 | // is invoked, therefore making this handle nil 46 | private weak var _tsfnToken: AnyObject? 47 | private var isValid: Bool { _tsfnToken != nil } 48 | 49 | let label: String 50 | let instanceID: UUID 51 | private let environment: NodeEnvironment 52 | private let raw: napi_threadsafe_function 53 | private weak var currentHandle: Handle? 54 | 55 | @NodeActor public init( 56 | label: String, 57 | asyncResource: NodeObjectConvertible? = nil, 58 | maxQueueSize: Int? = nil 59 | ) throws { 60 | self.label = label 61 | environment = .current 62 | self.instanceID = try environment.instanceID() 63 | let tsfnToken = Token() 64 | self._tsfnToken = tsfnToken 65 | let box = Unmanaged.passRetained(tsfnToken) 66 | var result: napi_threadsafe_function! 67 | do { 68 | try environment.check(napi_create_threadsafe_function( 69 | environment.raw, nil, 70 | asyncResource?.rawValue(), label.rawValue(), 71 | maxQueueSize ?? 0, 1, 72 | box.toOpaque(), { rawEnv, data, hint in 73 | Unmanaged.fromOpaque(data!).release() 74 | }, 75 | nil, cCallbackC, 76 | &result 77 | )) 78 | } catch { 79 | box.release() // we stan strong exception safety 80 | throw error 81 | } 82 | self.raw = result 83 | try environment.check(napi_unref_threadsafe_function(environment.raw, raw)) 84 | } 85 | 86 | private static func check(_ status: napi_status) throws { 87 | if let errCode = NodeAPIError.Code(status: status) { 88 | throw NodeAPIError(errCode) 89 | } 90 | } 91 | 92 | // if isValid is false (i.e. callbackHandle has been released), 93 | // the tsfn finalizer has been called and so it's now invalid for 94 | // use. This can happen during napi_env teardown, in which case 95 | // the tsfn will have been invalidated without our explicitly asking 96 | // for it 97 | private func ensureValid() throws { 98 | guard isValid else { 99 | throw NodeAPIError(.closing, message: "NodeAsyncQueue '\(label)' has been released") 100 | } 101 | } 102 | 103 | // makes any future calls to the threadsafe function return NodeAPIError(.closing) 104 | public func close() throws { 105 | try ensureValid() 106 | try Self.check(napi_acquire_threadsafe_function(raw)) 107 | try Self.check(napi_release_threadsafe_function(raw, napi_tsfn_abort)) 108 | } 109 | 110 | public class Handle: @unchecked Sendable { 111 | public let queue: NodeAsyncQueue 112 | 113 | @NodeActor fileprivate init(_ queue: NodeAsyncQueue) throws { 114 | let env = queue.environment 115 | try env.check(napi_ref_threadsafe_function(env.raw, queue.raw)) 116 | self.queue = queue 117 | } 118 | 119 | deinit { 120 | let raw = UncheckedSendable(queue.raw) 121 | // capture raw right here since `queue` might be deinitialized 122 | // by the time we enter the closure. Also, we use the variant 123 | // of `run` that doesn't do NodeContext.withContext since that 124 | // would result in a new handle being created, meaning that we'd 125 | // effectively never end up with a nil currentHandle. 126 | try? queue.run { [weak queue] env in 127 | // unref isn't ref-counted (e.g. ref, ref, unref is equivalent 128 | // to ref, unref) so we only want to call it when we're really 129 | // sure we're done; that is, if we're the last handle to be 130 | // deinitialized 131 | guard queue?.currentHandle == nil else { return } 132 | // we aren't really isolated but this is necessary to suppress 133 | // warnings about accessing `env.raw` off of NodeActor 134 | NodeActor.unsafeAssumeIsolated { 135 | _ = napi_unref_threadsafe_function(env.raw, raw.value) 136 | } 137 | } 138 | } 139 | } 140 | 141 | // returns a handle to the queue that keeps the node thread alive 142 | // while the handle is alive, even if the queue's own 143 | // keepsNodeThreadAlive is false 144 | @NodeActor public func handle() throws -> Handle { 145 | if let currentHandle = currentHandle { 146 | return currentHandle 147 | } else { 148 | let handle = try Handle(self) 149 | currentHandle = handle 150 | return handle 151 | } 152 | } 153 | 154 | deinit { 155 | if isValid { 156 | napi_release_threadsafe_function(raw, napi_tsfn_release) 157 | } 158 | } 159 | 160 | // will throw NodeAPIError(.closing) if another thread called abort() 161 | private func run( 162 | blocking: Bool = false, 163 | _ action: @escaping @Sendable (NodeEnvironment) -> Void 164 | ) throws { 165 | try ensureValid() 166 | let payload = CallbackBox(action) 167 | let unmanagedPayload = Unmanaged.passRetained(payload) 168 | let rawPayload = unmanagedPayload.toOpaque() 169 | do { 170 | try Self.check( 171 | napi_call_threadsafe_function( 172 | raw, rawPayload, 173 | blocking ? napi_tsfn_blocking : napi_tsfn_nonblocking 174 | ) 175 | ) 176 | } catch { 177 | unmanagedPayload.release() 178 | } 179 | } 180 | 181 | public func run( 182 | blocking: Bool = false, 183 | @_implicitSelfCapture _ action: @escaping @Sendable @NodeActor () throws -> Void 184 | ) throws { 185 | try run(blocking: blocking) { env in 186 | NodeContext.withUnsafeEntrypoint(env) { _ in try action() } 187 | } 188 | } 189 | 190 | private enum RunState { 191 | case pending 192 | case running(Task) 193 | case cancelled 194 | } 195 | 196 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 197 | public func run( 198 | blocking: Bool = false, 199 | resultType: T.Type = T.self, 200 | @_implicitSelfCapture body: @escaping @Sendable @NodeActor () async throws -> T 201 | ) async throws -> T { 202 | // TODO: Create a 'LockIsolated' helper type or use atomics here 203 | let lock = Lock() 204 | let state = UncheckedSendable(Box(.pending)) 205 | return try await withTaskCancellationHandler { 206 | try await withCheckedThrowingContinuation { cont in 207 | do { 208 | try run(blocking: blocking) { 209 | lock.withLock { 210 | switch state.value.value { 211 | case .cancelled: 212 | cont.resume(throwing: CancellationError()) 213 | case .pending: 214 | state.value.value = .running(Task { 215 | do { 216 | cont.resume(returning: try await body()) 217 | } catch { 218 | cont.resume(throwing: error) 219 | } 220 | }) 221 | case .running: 222 | break // wat 223 | } 224 | } 225 | } 226 | } catch { 227 | cont.resume(throwing: error) 228 | } 229 | } 230 | } onCancel: { [state] in 231 | lock.withLock { 232 | switch state.value.value { 233 | case .pending: 234 | state.value.value = .cancelled 235 | case .running(let task): 236 | task.cancel() 237 | state.value.value = .cancelled 238 | case .cancelled: 239 | break // wat 240 | } 241 | } 242 | } 243 | } 244 | 245 | } 246 | 247 | extension NodeAsyncQueue: CustomStringConvertible { 248 | public var description: String { 249 | "" 250 | } 251 | } 252 | 253 | extension NodeAsyncQueue.Handle: CustomStringConvertible { 254 | public var description: String { 255 | "" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeBigInt.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeBigInt: NodePrimitive { 4 | 5 | @frozen public enum Sign { 6 | case positive 7 | case negative 8 | 9 | var bit: Int32 { 10 | switch self { 11 | case .positive: 12 | return 0 13 | case .negative: 14 | return 1 15 | } 16 | } 17 | 18 | init(bit: Int32) { 19 | if bit % 2 == 0 { 20 | self = .positive 21 | } else { 22 | self = .negative 23 | } 24 | } 25 | } 26 | 27 | @_spi(NodeAPI) public let base: NodeValueBase 28 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 29 | self.base = base 30 | } 31 | 32 | public init(signed: Int64) throws { 33 | let ctx = NodeContext.current 34 | var value: napi_value! 35 | try ctx.environment.check(napi_create_bigint_int64(ctx.environment.raw, signed, &value)) 36 | self.base = NodeValueBase(raw: value, in: ctx) 37 | } 38 | 39 | public init(unsigned: UInt64) throws { 40 | let ctx = NodeContext.current 41 | var value: napi_value! 42 | try ctx.environment.check(napi_create_bigint_uint64(ctx.environment.raw, unsigned, &value)) 43 | self.base = NodeValueBase(raw: value, in: ctx) 44 | } 45 | 46 | // words should be from least to most significant (little endian) 47 | public init(sign: Sign, words: [UInt64]) throws { 48 | let ctx = NodeContext.current 49 | var value: napi_value! 50 | try ctx.environment.check(napi_create_bigint_words(ctx.environment.raw, sign.bit, words.count, words, &value)) 51 | self.base = NodeValueBase(raw: value, in: ctx) 52 | } 53 | 54 | public func signed() throws -> (value: Int64, lossless: Bool) { 55 | var value: Int64 = 0 56 | var isLossless: Bool = false 57 | try base.environment.check( 58 | napi_get_value_bigint_int64(base.environment.raw, base.rawValue(), &value, &isLossless) 59 | ) 60 | return (value, isLossless) 61 | } 62 | 63 | public func unsigned() throws -> (value: UInt64, lossless: Bool) { 64 | var value: UInt64 = 0 65 | var isLossless: Bool = false 66 | try base.environment.check( 67 | napi_get_value_bigint_uint64(base.environment.raw, base.rawValue(), &value, &isLossless) 68 | ) 69 | return (value, isLossless) 70 | } 71 | 72 | // little endian 73 | public func words() throws -> (sign: Sign, words: [UInt64]) { 74 | let env = base.environment 75 | var count: Int = 0 76 | let raw = try base.rawValue() 77 | try env.check(napi_get_value_bigint_words(env.raw, raw, nil, &count, nil)) 78 | var signBit: Int32 = 0 79 | let words = try [UInt64](unsafeUninitializedCapacity: count) { buf, outCount in 80 | outCount = 0 81 | try env.check(napi_get_value_bigint_words(env.raw, raw, &signBit, &count, buf.baseAddress)) 82 | outCount = count 83 | } 84 | return (Sign(bit: signBit), words) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeBool.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeBool: NodePrimitive, NodeValueCoercible { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init(coercing value: NodeValueConvertible) throws { 11 | let val = try value.nodeValue() 12 | if let val = val as? NodeBool { 13 | self.base = val.base 14 | return 15 | } 16 | let ctx = NodeContext.current 17 | let env = ctx.environment 18 | var coerced: napi_value! 19 | try env.check(napi_coerce_to_bool(env.raw, val.rawValue(), &coerced)) 20 | self.base = NodeValueBase(raw: coerced, in: ctx) 21 | } 22 | 23 | public init(_ bool: Bool) throws { 24 | let ctx = NodeContext.current 25 | let env = ctx.environment 26 | var val: napi_value! 27 | try env.check(napi_get_boolean(env.raw, bool, &val)) 28 | base = NodeValueBase(raw: val, in: ctx) 29 | } 30 | 31 | public func bool() throws -> Bool { 32 | let env = base.environment 33 | var value = false 34 | try env.check(napi_get_value_bool(env.raw, base.rawValue(), &value)) 35 | return value 36 | } 37 | 38 | } 39 | 40 | extension Bool: NodePrimitiveConvertible, NodeValueCreatable { 41 | public func nodeValue() throws -> NodeValue { 42 | try NodeBool(self) 43 | } 44 | 45 | public static func from(_ value: NodeBool) throws -> Bool { 46 | try value.bool() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeBuffer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | internal import CNodeAPI 3 | 4 | public final class NodeBuffer: NodeTypedArray { 5 | 6 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 7 | let env = value.environment 8 | var result = false 9 | try env.check(napi_is_buffer(env.raw, value.rawValue(), &result)) 10 | return result 11 | } 12 | 13 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 14 | super.init(base) 15 | } 16 | 17 | public init(capacity: Int) throws { 18 | let ctx = NodeContext.current 19 | let env = ctx.environment 20 | var data: UnsafeMutableRawPointer? 21 | var result: napi_value! 22 | try env.check(napi_create_buffer(env.raw, capacity, &data, &result)) 23 | super.init(NodeValueBase(raw: result, in: ctx)) 24 | } 25 | 26 | // bytes must remain valid while the object is alive (i.e. until deallocator is called) 27 | public init(bytes: UnsafeMutableRawBufferPointer, deallocator: NodeDataDeallocator) throws { 28 | let ctx = NodeContext.current 29 | let env = ctx.environment 30 | var result: napi_value! 31 | let hint = Unmanaged.passRetained(Hint((deallocator, bytes))).toOpaque() 32 | try env.check( 33 | napi_create_external_buffer(env.raw, bytes.count, bytes.baseAddress, { rawEnv, _, hint in 34 | NodeContext.withUnsafeEntrypoint(rawEnv!) { _ in 35 | let (deallocator, bytes) = Unmanaged.fromOpaque(hint!).takeRetainedValue().value 36 | deallocator.action(bytes) 37 | } 38 | }, hint, &result) 39 | ) 40 | super.init(NodeValueBase(raw: result, in: ctx)) 41 | } 42 | 43 | public convenience init(data: NSMutableData) throws { 44 | try self.init( 45 | bytes: UnsafeMutableRawBufferPointer(start: data.mutableBytes, count: data.length), 46 | deallocator: .capture(UncheckedSendable(data)) 47 | ) 48 | } 49 | 50 | public init(copying data: Data) throws { 51 | let ctx = NodeContext.current 52 | let env = ctx.environment 53 | var resultData: UnsafeMutableRawPointer? 54 | var result: napi_value! 55 | try data.withUnsafeBytes { buf in 56 | try env.check(napi_create_buffer_copy(env.raw, buf.count, buf.baseAddress, &resultData, &result)) 57 | } 58 | super.init(NodeValueBase(raw: result, in: ctx)) 59 | } 60 | 61 | } 62 | 63 | extension Data: NodeValueConvertible, NodeValueCreatable { 64 | public func nodeValue() throws -> NodeValue { 65 | try NodeBuffer(copying: self) 66 | } 67 | 68 | public static func from(_ value: NodeTypedArray) throws -> Data { 69 | try value.data() 70 | } 71 | } 72 | 73 | extension NodeTypedArray where Element == UInt8 { 74 | public func data() throws -> Data { 75 | try withUnsafeMutableBytes(Data.init(_:)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeContext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CNodeAPISupport 3 | internal import CNodeAPI 4 | 5 | extension NodeEnvironment { 6 | @NodeInstanceData private static var defaultQueue: NodeAsyncQueue? 7 | func getDefaultQueue() throws -> NodeAsyncQueue { 8 | if let q = Self.defaultQueue { return q } 9 | let q = try NodeAsyncQueue(label: "NAPI_SWIFT_EXECUTOR") 10 | Self.defaultQueue = q 11 | return q 12 | } 13 | } 14 | 15 | // An object context that manages allocations in native code. 16 | // You **must not** allow NodeContext instances to escape 17 | // the scope in which they were called. 18 | final class NodeContext { 19 | let environment: NodeEnvironment 20 | let isManaged: Bool 21 | 22 | private init(environment: NodeEnvironment, isManaged: Bool) { 23 | self.environment = environment 24 | self.isManaged = isManaged 25 | } 26 | 27 | // a list of values created with this context 28 | private var values: [Weak] = [] 29 | func registerValue(_ value: NodeValueBase) { 30 | // if we're in debug mode, register the value even in 31 | // unmanaged mode, to allow us to do sanity checks 32 | #if !DEBUG 33 | guard isManaged else { return } 34 | #endif 35 | values.append(Weak(value)) 36 | } 37 | 38 | @NodeActor private static func _withContext( 39 | _ ctx: NodeContext, 40 | environment env: NodeEnvironment, 41 | isTopLevel: Bool, 42 | do action: @NodeActor (NodeContext) throws -> T 43 | ) throws -> T { 44 | let ret: T 45 | do { 46 | ret = try action(ctx) 47 | if isTopLevel { 48 | // get the release queue one time and pass it in 49 | // to all persist calls for perf 50 | let q = try env.getDefaultQueue() 51 | for val in ctx.values { 52 | try val.value?.persist(releaseQueue: q) 53 | } 54 | ctx.values.removeAll() 55 | } else { 56 | #if DEBUG 57 | let escapedBase: NodeValueBase? 58 | #endif 59 | // this allows for the escaping of a single NodeValue 60 | // from a non-toplevel context 61 | if let escapedRet = ret as? NodeValue { 62 | let base = escapedRet.base 63 | try base.persist() 64 | #if DEBUG 65 | escapedBase = base 66 | #endif 67 | } else { 68 | #if DEBUG 69 | escapedBase = nil 70 | #endif 71 | } 72 | #if DEBUG 73 | // if anything besides the return value of `action` escaped, it's 74 | // an error on the user's end 75 | if let escaped = ctx.values.lazy.compactMap({ $0.value }).filter({ $0 !== escapedBase }).first { 76 | nodeFatalError("\(escaped) escaped unmanaged NodeContext") 77 | } 78 | #endif 79 | } 80 | } catch let error where isTopLevel { 81 | try? ctx.environment.throw(error) 82 | // we have to bail before the return statement somehow. 83 | // isTopLevel:true is accompanied by try? so what we 84 | // throw here doesn't really matter 85 | throw error 86 | } 87 | return ret 88 | } 89 | 90 | @NodeActor private static func withContext( 91 | environment env: NodeEnvironment, 92 | isTopLevel: Bool, 93 | do action: @NodeActor (NodeContext) throws -> T 94 | ) throws -> T { 95 | #if DEBUG 96 | weak var weakCtx: NodeContext? 97 | defer { 98 | if let weakCtx = weakCtx { 99 | nodeFatalError("\(weakCtx) escaped its expected scope") 100 | } 101 | } 102 | #endif 103 | do { 104 | let ctx = NodeContext(environment: env, isManaged: isTopLevel) 105 | node_swift_context_push(Unmanaged.passUnretained(ctx).toOpaque()) 106 | defer { node_swift_context_pop() } 107 | #if DEBUG 108 | defer { weakCtx = ctx } 109 | #endif 110 | if isTopLevel, #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { 111 | let q = try env.getDefaultQueue() 112 | return try NodeActor.$target.withValue(q.handle()) { 113 | return try _withContext(ctx, environment: env, isTopLevel: isTopLevel, do: action) 114 | } 115 | } else { 116 | return try _withContext(ctx, environment: env, isTopLevel: isTopLevel, do: action) 117 | } 118 | } 119 | } 120 | 121 | static func withUnsafeEntrypoint(_ raw: napi_env, action: @NodeActor @Sendable (NodeContext) throws -> T) -> T? { 122 | withUnsafeEntrypoint(NodeEnvironment(raw), action: action) 123 | } 124 | 125 | static func withUnsafeEntrypoint(_ environment: NodeEnvironment, action: @NodeActor @Sendable (NodeContext) throws -> T) -> T? { 126 | NodeActor.unsafeAssumeIsolated { 127 | try? withContext(environment: environment, isTopLevel: true, do: action) 128 | } 129 | } 130 | 131 | // This should be called at any entrypoint into our code from JS, with the 132 | // passed in environment. 133 | // 134 | // Upon completion of `action`, we can persist any node values that were 135 | // escaped, and perform other necessary cleanup. 136 | @NodeActor static func withContext(environment: NodeEnvironment, do action: @NodeActor (NodeContext) throws -> T) -> T? { 137 | try? withContext(environment: environment, isTopLevel: true, do: action) 138 | } 139 | 140 | // Calls `action` with a NodeContext which does not manage NodeValueConvertible 141 | // instances created using it. That is, the new context will assume that all 142 | // NodeValueConvertible instances created with it (except for possibly the return value) 143 | // do not escape its own lifetime, which in turn is exactly the lifetime of the closure. 144 | // This trades away safety for performance. 145 | // 146 | // Note that this is a dangerous API to use. The cause of UB could be as innocuous as 147 | // calling NodeClass.constructor(), which memoizes (and thereby escapes) the returned 148 | // NodeFunction. Even something as trivial as calling NodeValueConvertible.rawValue() 149 | // could thus yield UB, eg if the receiver is `NodeDeferredValue { MyClass.constructor() }` 150 | @NodeActor static func withUnmanagedContext(environment: NodeEnvironment, do action: @NodeActor (NodeContext) throws -> T) throws -> T { 151 | try withContext(environment: environment, isTopLevel: false, do: action) 152 | } 153 | 154 | static var hasCurrent: Bool { 155 | node_swift_context_peek() != nil 156 | } 157 | 158 | static var current: NodeContext { 159 | guard let last = node_swift_context_peek() else { 160 | // Node doesn't give us a backtrace (because we're not on a JS thread?) so 161 | // print one ourselves 162 | print(Thread.callStackSymbols.joined(separator: "\n")) 163 | nodeFatalError("Attempted to call a NodeAPI function on a non-JS thread") 164 | } 165 | return Unmanaged.fromOpaque(last).takeUnretainedValue() 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeDataView.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | // TODO: Can we implement NodeArrayBuffer as a collection and make 4 | // DataView its slice? How would that interact with typed arrays? 5 | public final class NodeDataView: NodeObject { 6 | 7 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 8 | let env = value.environment 9 | var result = false 10 | try env.check(napi_is_dataview(env.raw, value.rawValue(), &result)) 11 | return result 12 | } 13 | 14 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 15 | super.init(base) 16 | } 17 | 18 | public init( 19 | for buf: NodeArrayBuffer, 20 | range: E 21 | ) throws where E.Bound == Int { 22 | let range = try buf.withUnsafeMutableBytes { range.relative(to: $0) } 23 | let env = buf.base.environment 24 | var result: napi_value! 25 | try env.check(napi_create_dataview(env.raw, range.count, buf.base.rawValue(), range.lowerBound, &result)) 26 | super.init(NodeValueBase(raw: result, in: .current)) 27 | } 28 | 29 | public convenience init( 30 | for buf: NodeArrayBuffer, 31 | range: UnboundedRange 32 | ) throws { 33 | try self.init(for: buf, range: 0...) 34 | } 35 | 36 | public func arrayBuffer() throws -> NodeArrayBuffer { 37 | let env = base.environment 38 | var buf: napi_value! 39 | try env.check(napi_get_dataview_info(env.raw, base.rawValue(), nil, nil, &buf, nil)) 40 | return NodeArrayBuffer(NodeValueBase(raw: buf, in: .current)) 41 | } 42 | 43 | // range of bytes in backing array buffer 44 | public func byteRange() throws -> Range { 45 | let env = base.environment 46 | var length = 0 47 | var offset = 0 48 | try env.check(napi_get_dataview_info(env.raw, base.rawValue(), &length, nil, nil, &offset)) 49 | return offset ..< (offset + length) 50 | } 51 | 52 | public func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> T) throws -> T { 53 | let env = base.environment 54 | var data: UnsafeMutableRawPointer? 55 | var count = 0 56 | try env.check(napi_get_dataview_info(env.raw, base.rawValue(), &count, &data, nil, nil)) 57 | return try body(UnsafeMutableRawBufferPointer(start: data, count: count)) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeDate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | internal import CNodeAPI 3 | 4 | public final class NodeDate: NodeObject { 5 | 6 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 7 | super.init(base) 8 | } 9 | 10 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 11 | let env = value.environment 12 | var result = false 13 | try env.check(napi_is_date(env.raw, value.rawValue(), &result)) 14 | return result 15 | } 16 | 17 | public init(_ date: Date) throws { 18 | let ctx = NodeContext.current 19 | var result: napi_value! 20 | try ctx.environment.check(napi_create_date(ctx.environment.raw, date.timeIntervalSince1970 * 1000, &result)) 21 | super.init(NodeValueBase(raw: result, in: ctx)) 22 | } 23 | 24 | public func date() throws -> Date { 25 | let env = base.environment 26 | var msec: Double = 0 27 | try env.check(napi_get_date_value(env.raw, base.rawValue(), &msec)) 28 | return Date(timeIntervalSince1970: msec / 1000) 29 | } 30 | 31 | } 32 | 33 | extension Date: NodeValueConvertible, NodeValueCreatable { 34 | public func nodeValue() throws -> NodeValue { 35 | try NodeDate(self) 36 | } 37 | 38 | public static func from(_ value: NodeDate) throws -> Date { 39 | try value.date() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeError.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeError: NodeObject { 4 | 5 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 6 | super.init(base) 7 | } 8 | 9 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 10 | let env = value.environment 11 | var result = false 12 | try env.check(napi_is_error(env.raw, value.rawValue(), &result)) 13 | return result 14 | } 15 | 16 | public func exceptionValue() throws -> NodeValue { self } 17 | 18 | public init(code: String?, message: String) throws { 19 | let ctx = NodeContext.current 20 | let env = ctx.environment 21 | var result: napi_value! 22 | try env.check( 23 | napi_create_error( 24 | env.raw, 25 | code?.rawValue(), 26 | message.rawValue(), 27 | &result 28 | ) 29 | ) 30 | super.init(NodeValueBase(raw: result, in: ctx)) 31 | } 32 | 33 | public init(typeErrorCode code: String, message: String) throws { 34 | let ctx = NodeContext.current 35 | let env = ctx.environment 36 | var result: napi_value! 37 | try env.check( 38 | napi_create_type_error( 39 | env.raw, 40 | code.rawValue(), 41 | message.rawValue(), 42 | &result 43 | ) 44 | ) 45 | super.init(NodeValueBase(raw: result, in: ctx)) 46 | } 47 | 48 | public init(rangeErrorCode code: String, message: String) throws { 49 | let ctx = NodeContext.current 50 | let env = ctx.environment 51 | var result: napi_value! 52 | try env.check( 53 | napi_create_range_error( 54 | env.raw, 55 | code.rawValue(), 56 | message.rawValue(), 57 | &result 58 | ) 59 | ) 60 | super.init(NodeValueBase(raw: result, in: ctx)) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeExternal.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeExternal: NodeValue { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init(value: Any) throws { 11 | let ctx = NodeContext.current 12 | let env = ctx.environment 13 | let unmanaged = Unmanaged.passRetained(value as AnyObject) 14 | let opaque = unmanaged.toOpaque() 15 | var result: napi_value! 16 | try env.check(napi_create_external(env.raw, opaque, { rawEnv, data, hint in 17 | Unmanaged.fromOpaque(data!).release() 18 | }, nil, &result)) 19 | self.base = NodeValueBase(raw: result, in: ctx) 20 | } 21 | 22 | public func value() throws -> Any { 23 | let env = base.environment 24 | var opaque: UnsafeMutableRawPointer! 25 | try env.check(napi_get_value_external(env.raw, base.rawValue(), &opaque)) 26 | return Unmanaged.fromOpaque(opaque).takeUnretainedValue() 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeFunction.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | private typealias CallbackWrapper = Box 4 | 5 | private func cCallback(rawEnv: napi_env!, info: napi_callback_info!) -> napi_value? { 6 | let info = UncheckedSendable(info) 7 | return NodeContext.withUnsafeEntrypoint(rawEnv) { ctx -> napi_value in 8 | let arguments = try NodeArguments(raw: info.value!, in: ctx) 9 | let data = arguments.data 10 | let callback = Unmanaged.fromOpaque(data).takeUnretainedValue() 11 | return try callback.value(arguments).rawValue() 12 | } 13 | } 14 | 15 | public struct NodeArguments: MutableCollection, RandomAccessCollection { 16 | private var value: [AnyNodeValue] 17 | public let this: NodeObject? 18 | public let newTarget: NodeFunction? // new.target 19 | let data: UnsafeMutableRawPointer 20 | 21 | @NodeActor init(raw: napi_callback_info, in ctx: NodeContext) throws { 22 | let env = ctx.environment 23 | 24 | var argc: Int = 0 25 | try env.check(napi_get_cb_info(env.raw, raw, &argc, nil, nil, nil)) 26 | var this: napi_value! 27 | var data: UnsafeMutableRawPointer! 28 | let args = try [napi_value?](unsafeUninitializedCapacity: argc) { buf, len in 29 | len = 0 30 | try env.check(napi_get_cb_info(env.raw, raw, &argc, buf.baseAddress, &this, &data)) 31 | len = argc 32 | }.map { AnyNodeValue(raw: $0!, in: ctx) } 33 | 34 | var newTarget: napi_value? 35 | try env.check(napi_get_new_target(env.raw, raw, &newTarget)) 36 | 37 | self.this = try NodeValueBase(raw: this, in: ctx).as(NodeObject.self) 38 | self.newTarget = try newTarget.flatMap { try NodeValueBase(raw: $0, in: ctx).as(NodeFunction.self) } 39 | self.data = data 40 | self.value = args 41 | } 42 | 43 | public var startIndex: Int { value.startIndex } 44 | public var endIndex: Int { value.endIndex } 45 | public func index(after i: Int) -> Int { 46 | value.index(after: i) 47 | } 48 | 49 | public subscript(index: Int) -> AnyNodeValue { 50 | get { value[index] } 51 | set { value[index] = newValue } 52 | } 53 | } 54 | 55 | public final class NodeFunction: NodeObject, NodeCallable { 56 | 57 | public typealias Callback = @NodeActor (_ arguments: NodeArguments) throws -> NodeValueConvertible 58 | public typealias VoidCallback = @NodeActor (_ arguments: NodeArguments) throws -> Void 59 | public typealias AsyncCallback = @NodeActor (_ arguments: NodeArguments) async throws -> NodeValueConvertible 60 | public typealias AsyncVoidCallback = @NodeActor (_ arguments: NodeArguments) async throws -> Void 61 | 62 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 63 | super.init(base) 64 | } 65 | 66 | // this may seem useless since .function is handled in NodeValueType, but 67 | // consider the following example ("new Function()") 68 | // try Node.Function.as(NodeFunction.self)!.new().as(NodeFunction.self) 69 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 70 | try value.nodeType() == .function 71 | } 72 | 73 | // TODO: Add a convenience overload for returning Void (convert to NodeUndefined) 74 | // adding such an overload currently confuses the compiler during overload resolution 75 | // so we need to figure out how to make it select the right one (@_disfavoredOverload 76 | // doesn't seem to help) 77 | public init(name: String = "", callback: @escaping Callback) throws { 78 | let ctx = NodeContext.current 79 | let env = ctx.environment 80 | let wrapper = CallbackWrapper(callback) 81 | let data = Unmanaged.passUnretained(wrapper) 82 | var name = name 83 | var value: napi_value! 84 | try name.withUTF8 { 85 | try $0.withMemoryRebound(to: CChar.self) { 86 | try env.check( 87 | napi_create_function( 88 | env.raw, 89 | $0.baseAddress, $0.count, 90 | { cCallback(rawEnv: $0, info: $1) }, 91 | data.toOpaque(), 92 | &value 93 | ) 94 | ) 95 | } 96 | } 97 | super.init(NodeValueBase(raw: value, in: ctx)) 98 | // we retain CallbackWrapper using the finalizer functionality instead of 99 | // using Unmanaged.passRetained for data, since napi_create_function doesn't 100 | // accept a finalizer 101 | try addFinalizer { _ = wrapper } 102 | } 103 | 104 | public convenience init(name: String = "", callback: @escaping VoidCallback) throws { 105 | try self.init(name: name) { args in 106 | try callback(args) 107 | return try NodeUndefined() 108 | } 109 | } 110 | 111 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 112 | public convenience init(name: String = "", callback: @escaping AsyncCallback) throws { 113 | try self.init(name: name) { args in 114 | try NodePromise { try await callback(args) } 115 | } 116 | } 117 | 118 | public convenience init(name: String = "", callback: @escaping AsyncVoidCallback) throws { 119 | try self.init(name: name) { args in 120 | try await callback(args) 121 | return try NodeUndefined() 122 | } 123 | } 124 | 125 | @discardableResult 126 | public func call( 127 | on receiver: NodeValueConvertible = undefined, 128 | _ arguments: [NodeValueConvertible] 129 | ) throws -> AnyNodeValue { 130 | let env = base.environment 131 | var ret: napi_value! 132 | let rawArgs = try arguments.map { arg -> napi_value? in 133 | try arg.rawValue() 134 | } 135 | try env.check( 136 | napi_call_function( 137 | env.raw, 138 | receiver.rawValue(), 139 | base.rawValue(), 140 | rawArgs.count, rawArgs, 141 | &ret 142 | ) 143 | ) 144 | return AnyNodeValue(raw: ret) 145 | } 146 | 147 | public func construct(withArguments arguments: [NodeValueConvertible]) throws -> NodeObject { 148 | let env = base.environment 149 | let argv: [napi_value?] = try arguments.map { try $0.rawValue() } 150 | var result: napi_value! 151 | try env.check( 152 | napi_new_instance(env.raw, base.rawValue(), arguments.count, argv, &result) 153 | ) 154 | return try NodeValueBase(raw: result, in: .current).as(NodeObject.self)! 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeInstanceData.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | typealias InstanceDataBox = Box<[ObjectIdentifier: Any]> 4 | 5 | private class NodeInstanceDataStorage: @unchecked Sendable { 6 | private let lock = ReadWriteLock() 7 | private var storage: [napi_env: InstanceDataBox] = [:] 8 | private init() {} 9 | 10 | static let current = NodeInstanceDataStorage() 11 | 12 | @NodeActor func instanceData(for env: NodeEnvironment) -> InstanceDataBox { 13 | let raw = env.raw 14 | return lock.withReaderLock { 15 | // fast path: in most cases, we should have storage 16 | // for the env already 17 | storage[raw] 18 | } ?? lock.withWriterLock { 19 | // slow path: we don't have storage yet. Upgrade 20 | // to a writer lock to block concurrent reads 21 | // while we modify storage. 22 | 23 | // check if another thread beat us to it 24 | // TODO: Is this even possible? 25 | // I don't think napi_env can be used concurrently 26 | if let dict = storage[raw] { 27 | return dict 28 | } 29 | 30 | // we're the first to need storage for this env 31 | let box = Box<[ObjectIdentifier: Any]>([:]) 32 | storage[raw] = box 33 | 34 | // remove our associated storage when napi destroys the env 35 | let sendableRaw = UncheckedSendable(raw) 36 | _ = try? env.addCleanupHook { 37 | self.lock.withWriterLockVoid { 38 | self.storage.removeValue(forKey: sendableRaw.value) 39 | } 40 | } 41 | 42 | return box 43 | } 44 | } 45 | } 46 | 47 | public class NodeInstanceDataKey {} 48 | 49 | extension NodeEnvironment { 50 | private func instanceDataDict() -> InstanceDataBox { 51 | NodeInstanceDataStorage.current.instanceData(for: self) 52 | } 53 | 54 | func instanceData(for id: ObjectIdentifier) -> Any? { 55 | instanceDataDict().value[id] 56 | } 57 | 58 | func setInstanceData(_ value: Any?, for id: ObjectIdentifier) { 59 | instanceDataDict().value[id] = value 60 | } 61 | 62 | public subscript(key: NodeInstanceDataKey) -> T? { 63 | get { instanceData(for: ObjectIdentifier(key)) as? T } 64 | set { setInstanceData(newValue, for: ObjectIdentifier(key)) } 65 | } 66 | } 67 | 68 | @NodeActor 69 | @propertyWrapper public final class NodeInstanceData { 70 | private let defaultValue: Value 71 | 72 | private var key: ObjectIdentifier { ObjectIdentifier(self) } 73 | 74 | public var wrappedValue: Value { 75 | get { Node.instanceData(for: key) as? Value ?? defaultValue } 76 | set { Node.setInstanceData(newValue, for: key) } 77 | } 78 | 79 | public var projectedValue: NodeInstanceData { self } 80 | 81 | public nonisolated init(wrappedValue defaultValue: Value) where Value: Sendable { 82 | self.defaultValue = defaultValue 83 | } 84 | 85 | @available(*, unavailable, message: "NodeInstanceData cannot be an instance member") 86 | public static subscript( 87 | _enclosingInstance object: Never, 88 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 89 | storage storageKeyPath: ReferenceWritableKeyPath> 90 | ) -> Value { 91 | get {} 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeModule.swift: -------------------------------------------------------------------------------- 1 | public struct NodeModuleRegistrar { 2 | private let env: OpaquePointer? 3 | public init(_ env: OpaquePointer?) { 4 | self.env = env 5 | } 6 | 7 | // NB: this API is used by NodeModuleMacro and is sensitive to changes. 8 | public func register( 9 | init create: @escaping @Sendable @NodeActor () throws -> NodeValueConvertible 10 | ) -> OpaquePointer? { 11 | NodeContext.withUnsafeEntrypoint(NodeEnvironment(env!)) { _ in 12 | try create().rawValue() 13 | } 14 | } 15 | 16 | @available(*, deprecated, message: "Use register(init:) instead.") 17 | public func register( 18 | exports create: @autoclosure @escaping @Sendable @NodeActor () throws -> NodeValueConvertible 19 | ) -> OpaquePointer? { 20 | register(init: create) 21 | } 22 | } 23 | 24 | @freestanding(declaration) 25 | public macro NodeModule(init: @escaping @NodeActor () throws -> NodeValueConvertible) 26 | = #externalMacro(module: "NodeAPIMacros", type: "NodeModuleMacro") 27 | 28 | @freestanding(declaration) 29 | public macro NodeModule(exports: @autoclosure @escaping @NodeActor () throws -> NodeValueConvertible) 30 | = #externalMacro(module: "NodeAPIMacros", type: "NodeModuleMacro") 31 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeNull.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeNull: NodePrimitive { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init() throws { 11 | let ctx = NodeContext.current 12 | let env = ctx.environment 13 | var result: napi_value! 14 | try env.check(napi_get_null(env.raw, &result)) 15 | self.base = NodeValueBase(raw: result, in: ctx) 16 | } 17 | 18 | } 19 | 20 | extension Optional: NodeValueConvertible, NodePropertyConvertible where Wrapped: NodeValueConvertible { 21 | public func nodeValue() throws -> NodeValue { 22 | try self?.nodeValue() ?? NodeNull() 23 | } 24 | } 25 | 26 | extension Optional: AnyNodeValueCreatable where Wrapped: AnyNodeValueCreatable { 27 | public static func from(_ value: NodeValue) throws -> Wrapped?? { 28 | // first try to create Wrapped; this means that if we try to create 29 | // a Optional we'll get .some(.some(undefined)) instead 30 | // of .some(.none) (which is admittedly nonsensical anyway but i'd say 31 | // it makes more sense than the alternative, semantically) 32 | if let val = try Wrapped.from(value) { 33 | return val 34 | } 35 | switch try value.nodeType() { 36 | case .null, .undefined: 37 | // conversion succeeded, and we got nil 38 | return Wrapped??.some(.none) 39 | default: 40 | // conversion failed 41 | return Wrapped??.none 42 | } 43 | } 44 | } 45 | 46 | extension Optional: NodeClassPropertyConvertible where Wrapped: NodePrimitiveConvertible {} 47 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeNumber.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeNumber: NodePrimitive, NodeValueCoercible { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init(coercing value: NodeValueConvertible) throws { 11 | let val = try value.nodeValue() 12 | if let val = val as? NodeNumber { 13 | self.base = val.base 14 | return 15 | } 16 | let ctx = NodeContext.current 17 | let env = ctx.environment 18 | var coerced: napi_value! 19 | try env.check(napi_coerce_to_number(env.raw, val.rawValue(), &coerced)) 20 | self.base = NodeValueBase(raw: coerced, in: ctx) 21 | } 22 | 23 | public init(_ double: Double) throws { 24 | let ctx = NodeContext.current 25 | let env = ctx.environment 26 | var result: napi_value! 27 | try env.check(napi_create_double(env.raw, double, &result)) 28 | self.base = NodeValueBase(raw: result, in: ctx) 29 | } 30 | 31 | public func double() throws -> Double { 32 | let env = base.environment 33 | var value: Double = 0 34 | try env.check(napi_get_value_double(env.raw, base.rawValue(), &value)) 35 | return value 36 | } 37 | 38 | } 39 | 40 | extension Double: NodePrimitiveConvertible, NodeValueCreatable { 41 | public func nodeValue() throws -> NodeValue { 42 | try NodeNumber(self) 43 | } 44 | 45 | public static func from(_ value: NodeNumber) throws -> Double { 46 | try value.double() 47 | } 48 | } 49 | 50 | extension Int: NodePrimitiveConvertible, AnyNodeValueCreatable { 51 | public func nodeValue() throws -> NodeValue { 52 | try Double(self).nodeValue() 53 | } 54 | 55 | public static func from(_ value: NodeValue) throws -> Int? { 56 | try value.as(Double.self).flatMap(Int.init(exactly:)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodePromise.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | // similar to Combine.Future 4 | public final class NodePromise: NodeObject { 5 | public enum Error: Swift.Error { 6 | case completedTwice 7 | } 8 | 9 | // similar to Combine.Promise 10 | @NodeActor public final class Deferred { 11 | public private(set) var hasCompleted = false 12 | public let promise: NodePromise 13 | var raw: napi_deferred 14 | 15 | public init() throws { 16 | let ctx = NodeContext.current 17 | var value: napi_value! 18 | var deferred: napi_deferred! 19 | try ctx.environment.check(napi_create_promise(ctx.environment.raw, &deferred, &value)) 20 | self.promise = NodePromise(NodeValueBase(raw: value, in: ctx)) 21 | self.raw = deferred 22 | } 23 | 24 | public func callAsFunction(_ result: Result) throws { 25 | // calling reject/resolve multiple times is considered UB 26 | // by Node 27 | guard !hasCompleted else { 28 | throw Error.completedTwice 29 | } 30 | let env = promise.base.environment 31 | switch result { 32 | case .success(let value): 33 | try env.check(napi_resolve_deferred(env.raw, raw, value.rawValue())) 34 | case .failure(let error): 35 | try env.check(napi_reject_deferred(env.raw, raw, AnyNodeValue(error: error).rawValue())) 36 | } 37 | hasCompleted = true 38 | } 39 | 40 | @_disfavoredOverload 41 | public func callAsFunction(_ result: Result) throws { 42 | switch result { 43 | case .success: 44 | try self(.success(undefined)) 45 | case .failure(let error): 46 | try self(.failure(error)) 47 | } 48 | } 49 | } 50 | 51 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 52 | super.init(base) 53 | } 54 | 55 | public init(body: (_ deferred: Deferred) -> Void) throws { 56 | let deferred = try Deferred() 57 | body(deferred) 58 | super.init(deferred.promise.base) 59 | } 60 | 61 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 62 | let env = value.environment 63 | var result = false 64 | try env.check(napi_is_promise(env.raw, value.rawValue(), &result)) 65 | return result 66 | } 67 | 68 | // sugar around then/catch 69 | public func get(completion: @escaping (Result) -> Void) { 70 | // since we're on NodeActor, access to hasResumed is serial 71 | var hasResumed = false 72 | do { 73 | try self.then(NodeFunction { (val: AnyNodeValue) in 74 | if !hasResumed { 75 | hasResumed = true 76 | completion(.success(val)) 77 | } 78 | return undefined 79 | }).catch(NodeFunction { (err: AnyNodeValue) in 80 | if !hasResumed { 81 | hasResumed = true 82 | completion(.failure(err)) 83 | } 84 | return undefined 85 | }) 86 | } catch { 87 | if !hasResumed { 88 | hasResumed = true 89 | completion(.failure(error)) 90 | } 91 | } 92 | } 93 | 94 | } 95 | 96 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 97 | extension NodePromise { 98 | 99 | public convenience init(body: @escaping @Sendable @NodeActor () async throws -> NodeValueConvertible) throws { 100 | try self.init { deferred in 101 | Task { 102 | let result: Result 103 | do { 104 | result = .success(try await body()) 105 | } catch { 106 | result = .failure(error) 107 | } 108 | try deferred(result) 109 | } 110 | } 111 | } 112 | 113 | public var value: AnyNodeValue { 114 | get async throws { 115 | try await withCheckedThrowingContinuation { continuation in 116 | self.get { continuation.resume(with: $0) } 117 | } 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeProperty.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | private func cCallback(rawEnv: napi_env!, info: napi_callback_info!, isGetter: Bool) -> napi_value? { 4 | let info = UncheckedSendable(info) 5 | return NodeContext.withUnsafeEntrypoint(rawEnv) { ctx -> napi_value in 6 | let arguments = try NodeArguments(raw: info.value!, in: ctx) 7 | let data = arguments.data 8 | let callbacks = Unmanaged.fromOpaque(data).takeUnretainedValue() 9 | return try (isGetter ? callbacks.value.0 : callbacks.value.1)!(arguments).rawValue() 10 | } 11 | } 12 | 13 | private func cGetterOrMethod(rawEnv: napi_env!, info: napi_callback_info!) -> napi_value? { 14 | cCallback(rawEnv: rawEnv, info: info, isGetter: true) 15 | } 16 | 17 | private func cSetter(rawEnv: napi_env!, info: napi_callback_info!) -> napi_value? { 18 | cCallback(rawEnv: rawEnv, info: info, isGetter: false) 19 | } 20 | 21 | public protocol NodePropertyConvertible { 22 | @NodeActor var nodeProperty: NodePropertyBase { get } 23 | } 24 | 25 | // marker protocol: some values can be represented as properties 26 | // on objects but not on classes (eg non-primitive NodeValues as 27 | // .data) 28 | public protocol NodeClassPropertyConvertible: NodePropertyConvertible {} 29 | 30 | public typealias NodePrimitive = NodeValue & NodeClassPropertyConvertible 31 | public typealias NodePrimitiveConvertible = NodeValueConvertible & NodeClassPropertyConvertible 32 | 33 | public struct NodePropertyList: ExpressibleByDictionaryLiteral { 34 | let elements: [(NodeName, Property)] 35 | public init(_ elements: [(NodeName, Property)]) { 36 | self.elements = elements 37 | } 38 | public init(dictionaryLiteral elements: (NodeName, Property)...) { 39 | self.elements = elements 40 | } 41 | } 42 | public typealias NodeObjectPropertyList = NodePropertyList 43 | public typealias NodeClassPropertyList = NodePropertyList 44 | 45 | @NodeActor public struct NodeMethod: NodeClassPropertyConvertible { 46 | public let nodeProperty: NodePropertyBase 47 | 48 | public init(attributes: NodePropertyAttributes = .defaultMethod, _ callback: @escaping NodeFunction.Callback) { 49 | nodeProperty = .init(attributes: attributes, value: .method(callback)) 50 | } 51 | 52 | public init(attributes: NodePropertyAttributes = .defaultMethod, _ callback: @escaping NodeFunction.VoidCallback) { 53 | self.init(attributes: attributes) { args in 54 | try callback(args) 55 | return try NodeUndefined() 56 | } 57 | } 58 | 59 | public init(attributes: NodePropertyAttributes = .defaultMethod, _ callback: @escaping NodeFunction.AsyncCallback) { 60 | self.init(attributes: attributes) { args in 61 | try NodePromise { try await callback(args) } 62 | } 63 | } 64 | 65 | public init(attributes: NodePropertyAttributes = .defaultMethod, _ callback: @escaping NodeFunction.AsyncVoidCallback) { 66 | self.init(attributes: attributes) { args in 67 | try await callback(args) 68 | return try NodeUndefined() 69 | } 70 | } 71 | } 72 | 73 | @NodeActor public struct NodeProperty: NodeClassPropertyConvertible { 74 | public let nodeProperty: NodePropertyBase 75 | public init( 76 | attributes: NodePropertyAttributes = .defaultProperty, 77 | get: @escaping NodeFunction.Callback, 78 | set: NodeFunction.VoidCallback? = nil 79 | ) { 80 | var attributes = attributes 81 | if set == nil { 82 | // TODO: Is this necessary? 83 | attributes.remove(.writable) 84 | } 85 | nodeProperty = .init( 86 | attributes: attributes, 87 | value: set.map { set in 88 | .computed(get: get) { 89 | try set($0) 90 | return undefined 91 | } 92 | } ?? .computedGet(get) 93 | ) 94 | } 95 | } 96 | 97 | public struct NodePropertyAttributes: RawRepresentable, OptionSet, Sendable { 98 | public let rawValue: CEnum 99 | public init(rawValue: CEnum) { 100 | self.rawValue = rawValue 101 | } 102 | 103 | init(_ raw: napi_property_attributes) { 104 | self.rawValue = raw.rawValue 105 | } 106 | var raw: napi_property_attributes { .init(rawValue) } 107 | 108 | public static let writable = NodePropertyAttributes(napi_writable) 109 | public static let enumerable = NodePropertyAttributes(napi_enumerable) 110 | public static let configurable = NodePropertyAttributes(napi_configurable) 111 | // ignored by NodeObject.define 112 | public static let `static` = NodePropertyAttributes(napi_static) 113 | 114 | public static let `default`: NodePropertyAttributes = [] 115 | public static let defaultMethod: NodePropertyAttributes = [.writable, .configurable] 116 | public static let defaultProperty: NodePropertyAttributes = [.writable, .enumerable, .configurable] 117 | } 118 | 119 | @NodeActor public struct NodePropertyBase: NodePropertyConvertible { 120 | typealias Callbacks = Box<(getterOrMethod: NodeFunction.Callback?, setter: NodeFunction.Callback?)> 121 | 122 | public enum Value { 123 | case data(NodeValueConvertible) 124 | // we need this because you can't use .data for functions 125 | // while declaring a class prototype 126 | case method(NodeFunction.Callback) 127 | case computed(get: NodeFunction.Callback, set: NodeFunction.Callback) 128 | case computedGet(NodeFunction.Callback) 129 | case computedSet(NodeFunction.Callback) 130 | } 131 | 132 | public var nodeProperty: NodePropertyBase { self } 133 | 134 | public let attributes: NodePropertyAttributes 135 | public let value: Value 136 | 137 | init(attributes: NodePropertyAttributes, value: Value) { 138 | self.attributes = attributes 139 | self.value = value 140 | } 141 | 142 | public init(attributes: NodePropertyAttributes = .defaultProperty, _ data: NodeValueConvertible) { 143 | self.attributes = attributes 144 | self.value = .data(data) 145 | } 146 | 147 | // when needed, returns a Callbacks object which must be retained 148 | // on the object 149 | func raw(name: NodeName) throws -> (napi_property_descriptor, Callbacks?) { 150 | let callbacks: Callbacks? 151 | var raw = napi_property_descriptor() 152 | raw.name = try name.rawValue() 153 | raw.attributes = attributes.raw 154 | switch value { 155 | case .data(let data): 156 | raw.value = try data.rawValue() 157 | callbacks = nil 158 | case .method(let method): 159 | raw.method = { cGetterOrMethod(rawEnv: $0, info: $1) } 160 | callbacks = Callbacks((method, nil)) 161 | case .computedGet(let getter): 162 | raw.getter = { cGetterOrMethod(rawEnv: $0, info: $1) } 163 | callbacks = Callbacks((getter, nil)) 164 | case .computedSet(let setter): 165 | raw.setter = { cSetter(rawEnv: $0, info: $1) } 166 | callbacks = Callbacks((nil, setter)) 167 | case let .computed(getter, setter): 168 | raw.getter = { cGetterOrMethod(rawEnv: $0, info: $1) } 169 | raw.setter = { cSetter(rawEnv: $0, info: $1) } 170 | callbacks = Callbacks((getter, setter)) 171 | } 172 | raw.data = callbacks.map { Unmanaged.passUnretained($0).toOpaque() } 173 | return (raw, callbacks) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeString.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeString: NodePrimitive, NodeName, NodeValueCoercible { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init(coercing value: NodeValueConvertible) throws { 11 | let val = try value.nodeValue() 12 | if let val = val as? NodeString { 13 | self.base = val.base 14 | return 15 | } 16 | let ctx = NodeContext.current 17 | let env = ctx.environment 18 | var coerced: napi_value! 19 | try env.check( 20 | napi_coerce_to_string(env.raw, val.rawValue(), &coerced) 21 | ) 22 | self.base = NodeValueBase(raw: coerced, in: ctx) 23 | } 24 | 25 | public init(_ string: String) throws { 26 | let ctx = NodeContext.current 27 | let env = ctx.environment 28 | var result: napi_value! 29 | var string = string 30 | try string.withUTF8 { buf in 31 | try buf.withMemoryRebound(to: Int8.self) { newBuf in 32 | try env.check( 33 | napi_create_string_utf8(env.raw, newBuf.baseAddress, newBuf.count, &result) 34 | ) 35 | } 36 | } 37 | self.base = NodeValueBase(raw: result, in: ctx) 38 | } 39 | 40 | public func string() throws -> String { 41 | let env = base.environment 42 | let nodeVal = try base.rawValue() 43 | var length: Int = 0 44 | try env.check(napi_get_value_string_utf8(env.raw, nodeVal, nil, 0, &length)) 45 | // napi nul-terminates strings 46 | let totLength = length + 1 47 | return try String(portableUnsafeUninitializedCapacity: totLength) { 48 | try $0.withMemoryRebound(to: CChar.self) { 49 | try env.check(napi_get_value_string_utf8(env.raw, nodeVal, $0.baseAddress!, totLength, &length)) 50 | return length 51 | } 52 | }! 53 | } 54 | 55 | } 56 | 57 | extension String: NodePrimitiveConvertible, NodeName, NodeValueCreatable { 58 | public func nodeValue() throws -> NodeValue { 59 | try NodeString(self) 60 | } 61 | 62 | public static func from(_ value: NodeString) throws -> String { 63 | try value.string() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeSymbol.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeSymbol: NodePrimitive, NodeName { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init(description: String? = nil) throws { 11 | let ctx = NodeContext.current 12 | let env = ctx.environment 13 | var result: napi_value! 14 | let descRaw = try description.map { try $0.rawValue() } 15 | try env.check(napi_create_symbol(env.raw, descRaw, &result)) 16 | self.base = NodeValueBase(raw: result, in: ctx) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeThrowable.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | extension AnyNodeValue { 4 | private static let exceptionKey = NodeWrappedDataKey() 5 | 6 | public init(error: Error) throws { 7 | switch error { 8 | case let error as NodeValue: 9 | // if it's already a NodeValue, assign the base directly 10 | self.init(error) 11 | // TODO: handle specific error types 12 | // case let error as NodeAPIError: 13 | // break 14 | // case let error where type(of: error) is NSError.Type: 15 | // let cocoaError = error as NSError 16 | // break 17 | // TODO: maybe create our own Error class which allows round-tripping the 18 | // actual error object, instead of merely passing along stringified vals 19 | case let error: 20 | let nodeError = try NodeError(code: "\(type(of: error))", message: "\(error)") 21 | try nodeError.setWrappedValue(error, forKey: Self.exceptionKey) 22 | self.init(nodeError) 23 | } 24 | } 25 | 26 | public var nativeError: Error? { 27 | try? self.as(NodeError.self)?.wrappedValue(forKey: Self.exceptionKey) 28 | } 29 | } 30 | 31 | public func nodeFatalError(_ message: String = "", file: StaticString = #file, line: UInt = #line) -> Never { 32 | var message = message 33 | message.withUTF8 { 34 | $0.withMemoryRebound(to: CChar.self) { messageBuf -> Never in 35 | var loc = "\(file):\(line)" 36 | loc.withUTF8 { 37 | $0.withMemoryRebound(to: CChar.self) { locBuf in 38 | napi_fatal_error(locBuf.baseAddress, locBuf.count, messageBuf.baseAddress, messageBuf.count) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeTypedArray.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public enum NodeTypedArrayKind { 4 | struct UnknownKindError: Error { 5 | let kind: napi_typedarray_type 6 | } 7 | 8 | case int8 9 | case uint8 10 | case uint8Clamped 11 | case int16 12 | case uint16 13 | case int32 14 | case uint32 15 | case float32 16 | case float64 17 | case int64 18 | case uint64 19 | 20 | init(raw: napi_typedarray_type) throws { 21 | switch raw { 22 | case napi_int8_array: 23 | self = .int8 24 | case napi_uint8_array: 25 | self = .uint8 26 | case napi_uint8_clamped_array: 27 | self = .uint8Clamped 28 | case napi_int16_array: 29 | self = .int16 30 | case napi_uint16_array: 31 | self = .uint16 32 | case napi_int32_array: 33 | self = .int32 34 | case napi_uint32_array: 35 | self = .uint32 36 | case napi_float32_array: 37 | self = .float32 38 | case napi_float64_array: 39 | self = .float64 40 | case napi_bigint64_array: 41 | self = .int64 42 | case napi_biguint64_array: 43 | self = .uint64 44 | default: 45 | throw UnknownKindError(kind: raw) 46 | } 47 | } 48 | 49 | var raw: napi_typedarray_type { 50 | switch self { 51 | case .int8: 52 | return napi_int8_array 53 | case .uint8: 54 | return napi_uint8_array 55 | case .uint8Clamped: 56 | return napi_uint8_clamped_array 57 | case .int16: 58 | return napi_int16_array 59 | case .uint16: 60 | return napi_uint16_array 61 | case .int32: 62 | return napi_int32_array 63 | case .uint32: 64 | return napi_uint32_array 65 | case .float32: 66 | return napi_float32_array 67 | case .float64: 68 | return napi_float64_array 69 | case .int64: 70 | return napi_bigint64_array 71 | case .uint64: 72 | return napi_biguint64_array 73 | } 74 | } 75 | } 76 | 77 | public protocol NodeTypedArrayElement { 78 | @_spi(NodeAPI) static var kind: NodeTypedArrayKind { get } 79 | } 80 | 81 | extension NodeTypedArrayElement { 82 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { 83 | fatalError("Custom implementations of NodeTypedArrayKind are unsupported") 84 | } 85 | } 86 | 87 | extension Int8: NodeTypedArrayElement { 88 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .int8 } 89 | } 90 | 91 | extension UInt8: NodeTypedArrayElement { 92 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .uint8 } 93 | } 94 | 95 | extension Int16: NodeTypedArrayElement { 96 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .int16 } 97 | } 98 | 99 | extension UInt16: NodeTypedArrayElement { 100 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .uint16 } 101 | } 102 | 103 | extension Int32: NodeTypedArrayElement { 104 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .int32 } 105 | } 106 | 107 | extension UInt32: NodeTypedArrayElement { 108 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .uint32 } 109 | } 110 | 111 | extension Float: NodeTypedArrayElement { 112 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .float32 } 113 | } 114 | 115 | extension Double: NodeTypedArrayElement { 116 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .float64 } 117 | } 118 | 119 | extension Int64: NodeTypedArrayElement { 120 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .int64 } 121 | } 122 | 123 | extension UInt64: NodeTypedArrayElement { 124 | @_spi(NodeAPI) public static var kind: NodeTypedArrayKind { .uint64 } 125 | } 126 | 127 | public class NodeAnyTypedArray: NodeObject { 128 | 129 | fileprivate static func isObjectType(for value: NodeValueBase, kind: NodeTypedArrayKind?) throws -> Bool { 130 | let env = value.environment 131 | var result = false 132 | try env.check(napi_is_typedarray(env.raw, value.rawValue(), &result)) 133 | guard result else { return false } 134 | if let kind = kind { 135 | var type = napi_int8_array 136 | try env.check(napi_get_typedarray_info(env.raw, value.rawValue(), &type, nil, nil, nil, nil)) 137 | guard type == kind.raw else { return false } 138 | } 139 | return true 140 | } 141 | 142 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 143 | try isObjectType(for: value, kind: nil) 144 | } 145 | 146 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 147 | super.init(base) 148 | } 149 | 150 | public init(for buf: NodeArrayBuffer, kind: NodeTypedArrayKind, offset: Int = 0, count: Int) throws { 151 | let ctx = NodeContext.current 152 | let env = ctx.environment 153 | var result: napi_value! 154 | try env.check(napi_create_typedarray(env.raw, kind.raw, count, buf.base.rawValue(), offset, &result)) 155 | super.init(NodeValueBase(raw: result, in: ctx)) 156 | } 157 | 158 | public final func kind() throws -> NodeTypedArrayKind { 159 | let env = base.environment 160 | var type = napi_int8_array 161 | try env.check(napi_get_typedarray_info(env.raw, base.rawValue(), &type, nil, nil, nil, nil)) 162 | return try NodeTypedArrayKind(raw: type) 163 | } 164 | 165 | // offset in backing array buffer 166 | public final func byteOffset() throws -> Int { 167 | let env = base.environment 168 | var offset = 0 169 | try env.check(napi_get_typedarray_info(env.raw, base.rawValue(), nil, nil, nil, nil, &offset)) 170 | return offset 171 | } 172 | 173 | public final func withUnsafeMutableRawBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> T) throws -> T { 174 | let env = base.environment 175 | var data: UnsafeMutableRawPointer? 176 | var count = 0 177 | try env.check(napi_get_typedarray_info(env.raw, base.rawValue(), nil, &count, &data, nil, nil)) 178 | // TODO: Do we need to advance `data` by `offset`/a multiple of offset? 179 | return try body(UnsafeMutableRawBufferPointer(start: data, count: count)) 180 | } 181 | 182 | } 183 | 184 | public class NodeTypedArray: NodeAnyTypedArray { 185 | 186 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 187 | try isObjectType(for: value, kind: Element.kind) 188 | } 189 | 190 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 191 | super.init(base) 192 | } 193 | 194 | public init(for buf: NodeArrayBuffer, offset: Int = 0, count: Int) throws { 195 | try super.init(for: buf, kind: Element.kind, offset: offset, count: count) 196 | } 197 | 198 | public func withUnsafeMutableBytes(_ body: (UnsafeMutableBufferPointer) throws -> T) throws -> T { 199 | try withUnsafeMutableRawBytes { try body($0.bindMemory(to: Element.self)) } 200 | } 201 | 202 | } 203 | 204 | public final class NodeUInt8ClampedArray: NodeAnyTypedArray { 205 | 206 | override class func isObjectType(for value: NodeValueBase) throws -> Bool { 207 | try isObjectType(for: value, kind: .uint8Clamped) 208 | } 209 | 210 | @_spi(NodeAPI) public required init(_ base: NodeValueBase) { 211 | super.init(base) 212 | } 213 | 214 | public init(for buf: NodeArrayBuffer, offset: Int = 0, count: Int) throws { 215 | try super.init(for: buf, kind: .uint8Clamped, offset: offset, count: count) 216 | } 217 | 218 | public func withUnsafeMutableBytes(_ body: (UnsafeMutableBufferPointer) throws -> T) throws -> T { 219 | try withUnsafeMutableRawBytes { try body($0.bindMemory(to: UInt8.self)) } 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /Sources/NodeAPI/NodeUndefined.swift: -------------------------------------------------------------------------------- 1 | internal import CNodeAPI 2 | 3 | public final class NodeUndefined: NodePrimitive { 4 | 5 | @_spi(NodeAPI) public let base: NodeValueBase 6 | @_spi(NodeAPI) public init(_ base: NodeValueBase) { 7 | self.base = base 8 | } 9 | 10 | public init() throws { 11 | let ctx = NodeContext.current 12 | let env = ctx.environment 13 | var result: napi_value! 14 | try env.check(napi_get_undefined(env.raw, &result)) 15 | self.base = NodeValueBase(raw: result, in: ctx) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/NodeAPI/Sugar.swift: -------------------------------------------------------------------------------- 1 | extension NodeFunction { 2 | 3 | public convenience init( 4 | name: String = "", 5 | callback: @escaping @NodeActor (repeat each A) throws -> NodeValueConvertible 6 | ) throws { 7 | try self.init(name: name) { args in 8 | var reader = ArgReader(args) 9 | return try callback(repeat reader.next() as each A) 10 | } 11 | } 12 | 13 | public convenience init( 14 | name: String = "", 15 | callback: @escaping @NodeActor (repeat each A) throws -> Void 16 | ) throws { 17 | try self.init(name: name) { args in 18 | var reader = ArgReader(args) 19 | return try callback(repeat reader.next() as each A) 20 | } 21 | } 22 | 23 | public convenience init( 24 | name: String = "", 25 | callback: @escaping @NodeActor (repeat each A) async throws -> NodeValueConvertible 26 | ) throws { 27 | try self.init(name: name) { args in 28 | var reader = ArgReader(args) 29 | return try await callback(repeat reader.next() as each A) 30 | } 31 | } 32 | 33 | public convenience init( 34 | name: String = "", 35 | callback: @escaping @NodeActor (repeat each A) async throws -> Void 36 | ) throws { 37 | try self.init(name: name) { args in 38 | var reader = ArgReader(args) 39 | return try await callback(repeat reader.next() as each A) 40 | } 41 | } 42 | 43 | } 44 | 45 | extension NodeMethod { 46 | 47 | // instance methods 48 | 49 | public init( 50 | attributes: NodePropertyAttributes = .defaultMethod, 51 | of _: T.Type = T.self, 52 | _ callback: @escaping (T) -> @NodeActor (repeat each A) throws -> NodeValueConvertible 53 | ) { 54 | self.init(attributes: attributes) { (target: T) in 55 | { (args: NodeArguments) in 56 | var reader = ArgReader(args) 57 | return try callback(target)(repeat reader.next() as each A) 58 | } 59 | } 60 | } 61 | 62 | public init( 63 | attributes: NodePropertyAttributes = .defaultMethod, 64 | of _: T.Type = T.self, 65 | _ callback: @escaping (T) -> @NodeActor (repeat each A) throws -> Void 66 | ) { 67 | self.init(attributes: attributes) { (target: T) in 68 | { (args: NodeArguments) in 69 | var reader = ArgReader(args) 70 | return try callback(target)(repeat reader.next() as each A) 71 | } 72 | } 73 | } 74 | 75 | public init( 76 | attributes: NodePropertyAttributes = .defaultMethod, 77 | of _: T.Type = T.self, 78 | _ callback: @escaping (T) -> @NodeActor (repeat each A) async throws -> NodeValueConvertible 79 | ) { 80 | self.init(attributes: attributes) { (target: T) in 81 | { (args: NodeArguments) in 82 | var reader = ArgReader(args) 83 | return try await callback(target)(repeat reader.next() as each A) 84 | } 85 | } 86 | } 87 | 88 | public init( 89 | attributes: NodePropertyAttributes = .defaultMethod, 90 | of _: T.Type = T.self, 91 | _ callback: @escaping (T) -> @NodeActor (repeat each A) async throws -> Void 92 | ) { 93 | self.init(attributes: attributes) { (target: T) in 94 | { (args: NodeArguments) in 95 | var reader = ArgReader(args) 96 | return try await callback(target)(repeat reader.next() as each A) 97 | } 98 | } 99 | } 100 | 101 | // static methods 102 | 103 | public init( 104 | attributes: NodePropertyAttributes = .defaultMethod, 105 | _ callback: @escaping @NodeActor (repeat each A) throws -> NodeValueConvertible 106 | ) { 107 | self.init(attributes: attributes.union(.static)) { (args: NodeArguments) in 108 | var reader = ArgReader(args) 109 | return try callback(repeat reader.next() as each A) 110 | } 111 | } 112 | 113 | public init( 114 | attributes: NodePropertyAttributes = .defaultMethod, 115 | _ callback: @escaping @NodeActor (repeat each A) throws -> Void 116 | ) { 117 | self.init(attributes: attributes.union(.static)) { (args: NodeArguments) in 118 | var reader = ArgReader(args) 119 | return try callback(repeat reader.next() as each A) 120 | } 121 | } 122 | 123 | public init( 124 | attributes: NodePropertyAttributes = .defaultMethod, 125 | _ callback: @escaping @NodeActor (repeat each A) async throws -> NodeValueConvertible 126 | ) { 127 | self.init(attributes: attributes.union(.static)) { (args: NodeArguments) in 128 | var reader = ArgReader(args) 129 | return try await callback(repeat reader.next() as each A) 130 | } 131 | } 132 | 133 | public init( 134 | attributes: NodePropertyAttributes = .defaultMethod, 135 | _ callback: @escaping @NodeActor (repeat each A) async throws -> Void 136 | ) { 137 | self.init(attributes: attributes.union(.static)) { (args: NodeArguments) in 138 | var reader = ArgReader(args) 139 | return try await callback(repeat reader.next() as each A) 140 | } 141 | } 142 | 143 | } 144 | 145 | extension NodeConstructor { 146 | public init( 147 | _ invoke: @escaping @NodeActor @Sendable (repeat each A) throws -> T 148 | ) { 149 | self.init { args in 150 | var reader = ArgReader(args) 151 | return try invoke(repeat reader.next() as (each A)) 152 | } 153 | } 154 | } 155 | 156 | @NodeActor private struct ArgReader { 157 | var index = 0 158 | let arguments: NodeArguments 159 | init(_ arguments: NodeArguments) { 160 | self.arguments = arguments 161 | } 162 | mutating func next() throws -> T { 163 | defer { index += 1 } 164 | if index < arguments.count { 165 | guard let converted = try arguments[index].as(T.self) else { 166 | throw try NodeError( 167 | code: nil, 168 | message: "Could not convert parameter \(index) to type \(T.self)" 169 | ) 170 | } 171 | return converted 172 | } else { 173 | // if we're asking for an arg that's out of bounds, 174 | // return the equivalent of `undefined` if possible, 175 | // else throw 176 | guard let converted = try undefined.as(T.self) else { 177 | throw try NodeError( 178 | code: nil, 179 | message: "At least \(index + 1) argument\(index == 0 ? "" : "s") required. Got \(arguments.count)." 180 | ) 181 | } 182 | return converted 183 | } 184 | } 185 | } 186 | 187 | extension NodeClass { 188 | // used to refer to Self as a non-covariant 189 | @_documentation(visibility: private) 190 | public typealias _NodeSelf = Self 191 | } 192 | 193 | @attached(extension, conformances: NodeClass, names: named(properties), named(construct)) 194 | public macro NodeClass() = #externalMacro(module: "NodeAPIMacros", type: "NodeClassMacro") 195 | 196 | @attached(peer, names: named(construct)) 197 | public macro NodeConstructor() = #externalMacro(module: "NodeAPIMacros", type: "NodeConstructorMacro") 198 | 199 | @attached(peer, names: prefixed(`$`)) 200 | public macro NodeMethod(_: NodePropertyAttributes = .defaultMethod) 201 | = #externalMacro(module: "NodeAPIMacros", type: "NodeMethodMacro") 202 | 203 | @attached(peer, names: prefixed(`$`)) 204 | public macro NodeProperty(_: NodePropertyAttributes = .defaultProperty) 205 | = #externalMacro(module: "NodeAPIMacros", type: "NodePropertyMacro") 206 | -------------------------------------------------------------------------------- /Sources/NodeAPI/Utilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct NilValueError: Error {} 4 | 5 | struct Weak { 6 | weak var value: T? 7 | init(_ value: T) { self.value = value } 8 | } 9 | 10 | final class Box { 11 | var value: T 12 | init(_ value: T) { self.value = value } 13 | } 14 | 15 | package struct UncheckedSendable: @unchecked Sendable { 16 | package var value: T 17 | package init(_ value: T) { self.value = value } 18 | } 19 | 20 | extension String { 21 | func copiedCString() -> UnsafeMutablePointer { 22 | let utf8 = utf8CString 23 | let buf = UnsafeMutableBufferPointer.allocate(capacity: utf8.count) 24 | _ = buf.initialize(from: utf8) 25 | return buf.baseAddress! 26 | } 27 | 28 | // this is preferable to bytesNoCopy because the latter calls free on Darwin/Linux 29 | // but deallocate on Windows (when freeWhenDone is true) 30 | init?( 31 | portableUnsafeUninitializedCapacity length: Int, 32 | initializingUTF8With initializer: (UnsafeMutableBufferPointer) throws -> Int 33 | ) rethrows { 34 | #if os(Windows) || os(Linux) 35 | try self.init(unsafeUninitializedCapacity: length, initializingUTF8With: initializer) 36 | #else 37 | if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { 38 | try self.init(unsafeUninitializedCapacity: length, initializingUTF8With: initializer) 39 | } else { 40 | // we're on Darwin so we know that freeWhenDone uses libc free 41 | let buf = malloc(length).bindMemory(to: UInt8.self, capacity: length) 42 | let actualLength = try initializer(UnsafeMutableBufferPointer(start: buf, count: length)) 43 | self.init(bytesNoCopy: buf, length: actualLength, encoding: .utf8, freeWhenDone: true) 44 | } 45 | #endif 46 | } 47 | } 48 | 49 | #if os(Windows) 50 | public typealias CEnum = Int32 51 | #else 52 | public typealias CEnum = UInt32 53 | #endif 54 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/Diagnostics.swift: -------------------------------------------------------------------------------- 1 | import SwiftDiagnostics 2 | 3 | struct NodeDiagnosticMessage: DiagnosticMessage { 4 | let message: String 5 | private let messageID: String 6 | 7 | fileprivate init(_ message: String, messageID: String = #function) { 8 | self.message = message 9 | self.messageID = messageID 10 | } 11 | 12 | var diagnosticID: MessageID { 13 | MessageID(domain: "NodeAPIMacros", id: "\(type(of: self)).\(messageID)") 14 | } 15 | 16 | var severity: DiagnosticSeverity { .error } 17 | } 18 | 19 | 20 | extension DiagnosticMessage where Self == NodeDiagnosticMessage { 21 | static var expectedClassDecl: Self { 22 | .init("@NodeClass can only be applied to a class") 23 | } 24 | 25 | static var expectedFinal: Self { 26 | .init("@NodeClass classes must be final") 27 | } 28 | 29 | static var expectedFunction: Self { 30 | .init("@NodeMethod can only be applied to a function") 31 | } 32 | 33 | static var expectedProperty: Self { 34 | .init("@NodeProperty can only be applied to a property") 35 | } 36 | 37 | static var expectedInit: Self { 38 | .init("@NodeConstructor can only be applied to an initializer") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/NodeClassMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | struct NodeClassMacro: ExtensionMacro { 5 | static func expansion( 6 | of node: AttributeSyntax, 7 | attachedTo declaration: some DeclGroupSyntax, 8 | providingExtensionsOf type: some TypeSyntaxProtocol, 9 | conformingTo protocols: [TypeSyntax], 10 | in context: some MacroExpansionContext 11 | ) throws -> [ExtensionDeclSyntax] { 12 | guard let classDecl = declaration.as(ClassDeclSyntax.self) else { 13 | context.diagnose(.init(node: Syntax(declaration), message: .expectedClassDecl)) 14 | return [] 15 | } 16 | 17 | guard classDecl.modifiers.hasKeyword(.final) else { 18 | context.diagnose(.init(node: Syntax(declaration), message: .expectedFinal)) 19 | return [] 20 | } 21 | 22 | let dict = DictionaryExprSyntax { 23 | for member in classDecl.memberBlock.members { 24 | let identifier = 25 | if let function = member.decl.as(FunctionDeclSyntax.self), 26 | function.attributes.hasAttribute(named: "NodeMethod") == true { 27 | function.name 28 | } else if let property = member.decl.as(VariableDeclSyntax.self), 29 | property.attributes.hasAttribute(named: "NodeProperty") == true { 30 | property.identifier 31 | } else { 32 | nil as TokenSyntax? 33 | } 34 | if let identifier = identifier?.trimmed.textWithoutBackticks { 35 | DictionaryElementSyntax( 36 | key: "\(literal: identifier)" as ExprSyntax, 37 | value: "$\(raw: identifier)" as ExprSyntax 38 | ) 39 | } 40 | } 41 | } 42 | 43 | let inheritanceClause = protocols.isEmpty ? nil : InheritanceClauseSyntax( 44 | inheritedTypes: .init(protocols.map { .init(type: $0) }) 45 | ) 46 | 47 | return [ExtensionDeclSyntax(extendedType: type, inheritanceClause: inheritanceClause) { 48 | DeclSyntax(""" 49 | @NodeActor public static let properties: NodeClassPropertyList = \(dict) 50 | """) 51 | }] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/NodeConstructorMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | struct NodeConstructorMacro: PeerMacro { 5 | static func expansion( 6 | of node: AttributeSyntax, 7 | providingPeersOf declaration: some DeclSyntaxProtocol, 8 | in context: some MacroExpansionContext 9 | ) throws -> [DeclSyntax] { 10 | guard let ctor = declaration.as(InitializerDeclSyntax.self) else { 11 | context.diagnose(.init(node: Syntax(declaration), message: .expectedInit)) 12 | return [] 13 | } 14 | 15 | var sig = ctor.signature 16 | sig.returnClause = .init(type: "_NodeSelf" as TypeSyntax) 17 | 18 | return [""" 19 | @NodeActor public static let construct 20 | = NodeConstructor(_NodeSelf.init\(sig.arguments) as @NodeActor \(sig.functionType)) 21 | """] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/NodeMethodMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | struct NodeMethodMacro: PeerMacro { 5 | static func expansion( 6 | of node: AttributeSyntax, 7 | providingPeersOf declaration: some DeclSyntaxProtocol, 8 | in context: some MacroExpansionContext 9 | ) throws -> [DeclSyntax] { 10 | guard let function = declaration.as(FunctionDeclSyntax.self) else { 11 | context.diagnose(.init(node: Syntax(declaration), message: .expectedFunction)) 12 | return [] 13 | } 14 | 15 | // we don't need to change the attribtues for static methods 16 | // because the NodeMethod.init overloads that accept non-instance 17 | // methods automatically union the attributes with `.static`. 18 | let attributes = node.nodeAttributes ?? ".defaultMethod" 19 | let sig = function.signature 20 | 21 | let val: ExprSyntax = if function.modifiers.hasKeyword(.static) { 22 | "_NodeSelf.\(function.name) as @NodeActor \(sig.functionType)" 23 | } else { 24 | "{ $0.\(function.name) } as (_NodeSelf) -> @NodeActor \(sig.functionType)" 25 | } 26 | 27 | return [""" 28 | @NodeActor static let $\(raw: function.name.textWithoutBackticks) 29 | = NodeMethod(attributes: \(attributes), \(val)) 30 | """] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/NodeModuleMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | struct NodeModuleMacro: DeclarationMacro { 5 | // we avoid formatting to preserve sourceLocation info 6 | static var formatMode: FormatMode { .disabled } 7 | 8 | static func expansion( 9 | of node: some FreestandingMacroExpansionSyntax, 10 | in context: some MacroExpansionContext 11 | ) throws -> [DeclSyntax] { 12 | let name = context.makeUniqueName("register") 13 | 14 | let call: CodeBlockItemSyntax 15 | if node.arguments.count == 1, 16 | let argument = node.arguments.first, 17 | argument.label?.text == "exports" { 18 | // wrap `exports:` argument in a closure to allow for NodeActor isolation. 19 | // https://github.com/kabiroberai/node-swift/issues/26 20 | call = "NodeAPI.NodeModuleRegistrar(env).register { \(argument.expression) }" 21 | } else { 22 | call = CodeBlockItemSyntax(item: .expr(ExprSyntax(FunctionCallExprSyntax( 23 | calledExpression: "NodeAPI.NodeModuleRegistrar(env).register" as ExprSyntax, 24 | leftParen: node.leftParen, 25 | arguments: node.arguments, 26 | rightParen: node.rightParen, 27 | trailingClosure: node.trailingClosure, 28 | additionalTrailingClosures: node.additionalTrailingClosures 29 | )))) 30 | } 31 | 32 | let start = context.location(of: node, at: .afterLeadingTrivia, filePathMode: .filePath)! 33 | var file = start.file 34 | #if os(Windows) 35 | // on Windows, the literal will use raw string syntax by default: 36 | // along the lines of #"C:\Users\..."#. But sourceLocation doesn't 37 | // like this, so extract the underlying string and re-encode it 38 | // as an escaped string instead: "C:\\Users\\..." 39 | if let literal = file.as(StringLiteralExprSyntax.self), 40 | literal.openingPounds != nil, 41 | let raw = literal.representedLiteralValue { 42 | file = "\"\(raw: raw.replacing("\\", with: "\\\\"))\"" 43 | } 44 | #endif 45 | 46 | return [""" 47 | @_cdecl("node_swift_register") 48 | public func \(name)(env: Swift.OpaquePointer) -> Swift.OpaquePointer? { 49 | #sourceLocation(file: \(file), line: \(start.line)) 50 | \(call) 51 | #sourceLocation() 52 | } 53 | """] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/NodePropertyMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | struct NodePropertyMacro: PeerMacro { 5 | static func expansion( 6 | of node: AttributeSyntax, 7 | providingPeersOf declaration: some DeclSyntaxProtocol, 8 | in context: some MacroExpansionContext 9 | ) throws -> [DeclSyntax] { 10 | guard let identifier = declaration.as(VariableDeclSyntax.self)?.identifier?.trimmed else { 11 | context.diagnose(.init(node: Syntax(declaration), message: .expectedProperty)) 12 | return [] 13 | } 14 | 15 | let attributes = node.nodeAttributes ?? ".defaultProperty" 16 | 17 | let raw = identifier.textWithoutBackticks 18 | return [""" 19 | @NodeActor static let $\(raw: raw) 20 | = NodeProperty(attributes: \(attributes), \\_NodeSelf.\(identifier)) 21 | """] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main struct Plugin: CompilerPlugin { 5 | let providingMacros: [Macro.Type] = [ 6 | NodeMethodMacro.self, 7 | NodePropertyMacro.self, 8 | NodeConstructorMacro.self, 9 | NodeClassMacro.self, 10 | NodeModuleMacro.self, 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/NodeAPIMacros/SyntaxUtils.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension AttributeListSyntax { 4 | func hasAttribute(named name: String) -> Bool { 5 | contains { 6 | if case let .attribute(value) = $0 { 7 | value.attributeName.as(IdentifierTypeSyntax.self)?.name.trimmed.text == name 8 | } else { 9 | false 10 | } 11 | } 12 | } 13 | } 14 | 15 | extension DeclModifierListSyntax { 16 | func hasKeyword(_ keyword: Keyword) -> Bool { 17 | lazy.map(\.name.tokenKind).contains(.keyword(keyword)) 18 | } 19 | } 20 | 21 | extension AttributeSyntax { 22 | var nodeAttributes: ExprSyntax? { 23 | if case .argumentList(let tuple) = arguments, let elt = tuple.first { 24 | elt.expression 25 | } else { 26 | nil 27 | } 28 | } 29 | } 30 | 31 | extension FunctionSignatureSyntax { 32 | var functionType: FunctionTypeSyntax { 33 | FunctionTypeSyntax( 34 | parameters: TupleTypeElementListSyntax { 35 | for parameter in parameterClause.parameters { 36 | TupleTypeElementSyntax(type: parameter.type, trailingComma: parameter.trailingComma) 37 | } 38 | }, 39 | effectSpecifiers: effectSpecifiers?.typeEffectSpecifiers, 40 | returnClause: .init(type: returnClause?.type.trimmed ?? "Void") 41 | ) 42 | } 43 | 44 | var arguments: DeclNameArgumentsSyntax { 45 | if parameterClause.parameters.isEmpty { 46 | DeclNameArgumentsSyntax(leftParen: .unknown(""), arguments: [], rightParen: .unknown("")) 47 | } else { 48 | DeclNameArgumentsSyntax(arguments: .init(parameterClause.parameters.map { .init(name: $0.firstName.trimmed) })) 49 | } 50 | } 51 | } 52 | 53 | extension FunctionEffectSpecifiersSyntax { 54 | var typeEffectSpecifiers: TypeEffectSpecifiersSyntax { 55 | TypeEffectSpecifiersSyntax( 56 | asyncSpecifier: asyncSpecifier, 57 | throwsClause: throwsClause 58 | ) 59 | } 60 | } 61 | 62 | extension VariableDeclSyntax { 63 | var identifier: TokenSyntax? { 64 | guard bindings.count == 1 else { return nil } 65 | return bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier 66 | } 67 | } 68 | 69 | extension TokenSyntax { 70 | var textWithoutBackticks: String { 71 | var text = text 72 | if text.count >= 2 && text.first == "`" && text.last == "`" { 73 | text = String(text.dropFirst().dropLast()) 74 | } 75 | return text 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/NodeJSC/NodeJSC.swift: -------------------------------------------------------------------------------- 1 | import CNodeJSC 2 | import NodeAPI 3 | 4 | extension NodeEnvironment { 5 | public nonisolated static func withJSC( 6 | context: JSContext? = nil, 7 | _ perform: @NodeActor @Sendable () throws -> R 8 | ) -> R? { 9 | let context = context ?? JSContext()! 10 | let executor = napi_executor( 11 | version: 1, 12 | context: nil, 13 | free: { _ in }, 14 | assert_current: { _ in }, 15 | dispatch_async: { _, cb, ctx in 16 | let sendable = UncheckedSendable(ctx) 17 | DispatchQueue.main.async { cb?(sendable.value) } 18 | } 19 | ) 20 | let raw = napi_env_jsc_create(context.jsGlobalContextRef, executor)! 21 | return performUnsafe(raw) { 22 | try perform() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/NodeModuleSupport/include/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kabiroberai/node-swift/8af75ea6741c5cd48ed2ca90099b2ff45ac933c5/Sources/NodeModuleSupport/include/.keep -------------------------------------------------------------------------------- /Sources/NodeModuleSupport/node_gyp_LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Nathan Rajlich 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /Sources/NodeModuleSupport/register.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../../CNodeAPI/vendored/node_api.h" 3 | 4 | NAPI_MODULE_INIT() { 5 | napi_value node_swift_register(napi_env); 6 | return node_swift_register(env); 7 | } 8 | -------------------------------------------------------------------------------- /Sources/NodeModuleSupport/win_delay_load_hook.cc: -------------------------------------------------------------------------------- 1 | // from node-gyp (see node_gyp_LICENSE) 2 | 3 | /* 4 | * When this file is linked to a DLL, it sets up a delay-load hook that 5 | * intervenes when the DLL is trying to load the host executable 6 | * dynamically. Instead of trying to locate the .exe file it'll just 7 | * return a handle to the process image. 8 | * 9 | * This allows compiled addons to work when the host executable is renamed. 10 | */ 11 | 12 | #ifdef _MSC_VER 13 | 14 | #pragma managed(push, off) 15 | 16 | #ifndef WIN32_LEAN_AND_MEAN 17 | #define WIN32_LEAN_AND_MEAN 18 | #endif 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) { 26 | HMODULE m; 27 | if (event != dliNotePreLoadLibrary) 28 | return NULL; 29 | 30 | if (_stricmp(info->szDll, "node.exe") != 0) 31 | return NULL; 32 | 33 | m = GetModuleHandle(NULL); 34 | return (FARPROC) m; 35 | } 36 | 37 | decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = load_exe_hook; 38 | 39 | #pragma managed(pop) 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /Tests/NodeAPIMacrosTests/NodeClassMacroTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NodeAPIMacros 2 | import MacroTesting 3 | import XCTest 4 | 5 | final class NodeClassMacroTests: XCTestCase { 6 | override func invokeTest() { 7 | withMacroTesting( 8 | isRecording: false, 9 | macros: ["NodeClass": NodeClassMacro.self] 10 | ) { 11 | super.invokeTest() 12 | } 13 | } 14 | 15 | func testEmpty() { 16 | assertMacro { 17 | #""" 18 | @NodeClass final class Foo { 19 | } 20 | """# 21 | } expansion: { 22 | """ 23 | final class Foo { 24 | } 25 | 26 | extension Foo { 27 | @NodeActor public static let properties: NodeClassPropertyList = [:] 28 | } 29 | """ 30 | } 31 | } 32 | 33 | func testBasic() { 34 | assertMacro { 35 | #""" 36 | @NodeClass final class Foo { 37 | @NodeProperty var x = 5 38 | @NodeProperty var y = 6 39 | var z = 7 40 | 41 | @NodeMethod func foo() {} 42 | func bar() {} 43 | @NodeMethod func baz() {} 44 | } 45 | """# 46 | } expansion: { 47 | """ 48 | final class Foo { 49 | @NodeProperty var x = 5 50 | @NodeProperty var y = 6 51 | var z = 7 52 | 53 | @NodeMethod func foo() {} 54 | func bar() {} 55 | @NodeMethod func baz() {} 56 | } 57 | 58 | extension Foo { 59 | @NodeActor public static let properties: NodeClassPropertyList = ["x": $x, "y": $y, "foo": $foo, "baz": $baz] 60 | } 61 | """ 62 | } 63 | } 64 | 65 | func testNonClass() { 66 | assertMacro { 67 | #""" 68 | @NodeClass struct Foo {} 69 | """# 70 | } diagnostics: { 71 | """ 72 | @NodeClass struct Foo {} 73 | ┬─────────────────────── 74 | ╰─ 🛑 @NodeClass can only be applied to a class 75 | """ 76 | } 77 | } 78 | 79 | func testNonFinal() { 80 | assertMacro { 81 | #""" 82 | @NodeClass class Foo {} 83 | """# 84 | } diagnostics: { 85 | """ 86 | @NodeClass class Foo {} 87 | ┬────────────────────── 88 | ╰─ 🛑 @NodeClass classes must be final 89 | """ 90 | } 91 | } 92 | 93 | func testEscaping() { 94 | assertMacro(.node) { 95 | #""" 96 | @NodeClass final class Escaping { 97 | @NodeProperty var `case` = 0 98 | 99 | @NodeMethod func `default`() {} 100 | } 101 | """# 102 | } expansion: { 103 | #""" 104 | final class Escaping { 105 | var `case` = 0 106 | 107 | @NodeActor static let $case 108 | = NodeProperty(attributes: .defaultProperty, \_NodeSelf.`case`) 109 | 110 | func `default`() {} 111 | 112 | @NodeActor static let $default 113 | = NodeMethod(attributes: .defaultMethod, { 114 | $0.`default` 115 | } as (_NodeSelf) -> @NodeActor () -> Void) 116 | } 117 | 118 | extension Escaping { 119 | @NodeActor public static let properties: NodeClassPropertyList = ["case": $case, "default": $default] 120 | } 121 | """# 122 | } 123 | } 124 | 125 | func testIntegration() { 126 | assertMacro(.node) { 127 | #""" 128 | @NodeClass final class Foo { 129 | @NodeProperty var x = 5 130 | @NodeProperty(.enumerable) var y = "hello" 131 | var z = 7 132 | 133 | @NodeMethod func foo(_ x: String) async throws { 134 | throw SomeError(x) 135 | } 136 | 137 | func bar() {} 138 | 139 | @NodeMethod func baz(returnNumber: Bool) async throws -> any NodeValueConvertible { 140 | try await Task.sleep(for: .seconds(2)) 141 | return returnNumber ? 5 : "hi" 142 | } 143 | 144 | @NodeConstructor init(x: Int) throws { 145 | self.x = x 146 | } 147 | } 148 | """# 149 | } expansion: { 150 | #""" 151 | final class Foo { 152 | var x = 5 153 | 154 | @NodeActor static let $x 155 | = NodeProperty(attributes: .defaultProperty, \_NodeSelf.x) 156 | var y = "hello" 157 | 158 | @NodeActor static let $y 159 | = NodeProperty(attributes: .enumerable, \_NodeSelf.y) 160 | var z = 7 161 | 162 | func foo(_ x: String) async throws { 163 | throw SomeError(x) 164 | } 165 | 166 | @NodeActor static let $foo 167 | = NodeMethod(attributes: .defaultMethod, { 168 | $0.foo 169 | } as (_NodeSelf) -> @NodeActor (String) async throws -> Void) 170 | 171 | func bar() {} 172 | 173 | func baz(returnNumber: Bool) async throws -> any NodeValueConvertible { 174 | try await Task.sleep(for: .seconds(2)) 175 | return returnNumber ? 5 : "hi" 176 | } 177 | 178 | @NodeActor static let $baz 179 | = NodeMethod(attributes: .defaultMethod, { 180 | $0.baz 181 | } as (_NodeSelf) -> @NodeActor (Bool) async throws -> any NodeValueConvertible) 182 | 183 | init(x: Int) throws { 184 | self.x = x 185 | } 186 | 187 | @NodeActor public static let construct 188 | = NodeConstructor(_NodeSelf.init(x:) as @NodeActor (Int) throws -> _NodeSelf) 189 | } 190 | 191 | extension Foo { 192 | @NodeActor public static let properties: NodeClassPropertyList = ["x": $x, "y": $y, "foo": $foo, "baz": $baz] 193 | } 194 | """# 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Tests/NodeAPIMacrosTests/NodeConstructorMacroTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import XCTest 3 | 4 | final class NodeConstructorMacroTests: NodeMacroTest { 5 | func testBasic() { 6 | assertMacro { 7 | #""" 8 | @NodeConstructor init() {} 9 | """# 10 | } expansion: { 11 | """ 12 | init() {} 13 | 14 | @NodeActor public static let construct 15 | = NodeConstructor(_NodeSelf.init as @NodeActor () -> _NodeSelf) 16 | """ 17 | } 18 | } 19 | 20 | func testNonConstructor() { 21 | assertMacro { 22 | #""" 23 | @NodeConstructor func foo() {} 24 | """# 25 | } diagnostics: { 26 | """ 27 | @NodeConstructor func foo() {} 28 | ┬───────────────────────────── 29 | ╰─ 🛑 @NodeConstructor can only be applied to an initializer 30 | """ 31 | } 32 | } 33 | 34 | func testEffectful() { 35 | assertMacro { 36 | #""" 37 | @NodeConstructor init() async throws {} 38 | """# 39 | } expansion: { 40 | """ 41 | init() async throws {} 42 | 43 | @NodeActor public static let construct 44 | = NodeConstructor(_NodeSelf.init as @NodeActor () async throws -> _NodeSelf) 45 | """ 46 | } 47 | } 48 | 49 | func testArguments() { 50 | assertMacro { 51 | #""" 52 | @NodeConstructor init(_ x: Int, y: String) { 53 | print("hi") 54 | } 55 | """# 56 | } expansion: { 57 | """ 58 | init(_ x: Int, y: String) { 59 | print("hi") 60 | } 61 | 62 | @NodeActor public static let construct 63 | = NodeConstructor(_NodeSelf.init(_:y:) as @NodeActor (Int, String) -> _NodeSelf) 64 | """ 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/NodeAPIMacrosTests/NodeMacroTest.swift: -------------------------------------------------------------------------------- 1 | @testable import NodeAPIMacros 2 | import SwiftSyntaxMacros 3 | import MacroTesting 4 | import XCTest 5 | 6 | typealias NodeMacroTest = NodeMacroTestBase & NodeMacroTestProtocol 7 | 8 | extension [String: any Macro.Type] { 9 | static let node: Self = [ 10 | "NodeMethod": NodeMethodMacro.self, 11 | "NodeProperty": NodePropertyMacro.self, 12 | "NodeConstructor": NodeConstructorMacro.self, 13 | "NodeClass": NodeClassMacro.self, 14 | "NodeModule": NodeModuleMacro.self, 15 | ] 16 | } 17 | 18 | class NodeMacroTestBase: XCTestCase { 19 | override func invokeTest() { 20 | withMacroTesting( 21 | isRecording: (self as? NodeMacroTestProtocol)?.isRecording, 22 | macros: .node 23 | ) { 24 | super.invokeTest() 25 | } 26 | } 27 | } 28 | 29 | protocol NodeMacroTestProtocol { 30 | var isRecording: Bool { get } 31 | } 32 | 33 | extension NodeMacroTestProtocol { 34 | var isRecording: Bool { false } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/NodeAPIMacrosTests/NodeMethodMacroTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import XCTest 3 | 4 | final class NodeMethodMacrosTests: NodeMacroTest { 5 | func testBasicMethod() { 6 | assertMacro { 7 | """ 8 | @NodeMethod 9 | func foo() {} 10 | """ 11 | } expansion: { 12 | """ 13 | func foo() {} 14 | 15 | @NodeActor static let $foo 16 | = NodeMethod(attributes: .defaultMethod, { 17 | $0.foo 18 | } as (_NodeSelf) -> @NodeActor () -> Void) 19 | """ 20 | } 21 | } 22 | 23 | func testNonMethod() { 24 | assertMacro { 25 | """ 26 | @NodeMethod 27 | let foo = 5 28 | """ 29 | } diagnostics: { 30 | """ 31 | @NodeMethod 32 | ╰─ 🛑 @NodeMethod can only be applied to a function 33 | let foo = 5 34 | """ 35 | } 36 | } 37 | 38 | func testBasicMethodInline() { 39 | assertMacro { 40 | """ 41 | @NodeMethod func foo() { 42 | print("hi") 43 | } 44 | """ 45 | } expansion: { 46 | """ 47 | func foo() { 48 | print("hi") 49 | } 50 | 51 | @NodeActor static let $foo 52 | = NodeMethod(attributes: .defaultMethod, { 53 | $0.foo 54 | } as (_NodeSelf) -> @NodeActor () -> Void) 55 | """ 56 | } 57 | } 58 | 59 | func testMethodAttributes() { 60 | assertMacro { 61 | """ 62 | @NodeMethod(.writable) 63 | func foo() { 64 | print("hi") 65 | } 66 | """ 67 | } expansion: { 68 | """ 69 | func foo() { 70 | print("hi") 71 | } 72 | 73 | @NodeActor static let $foo 74 | = NodeMethod(attributes: .writable, { 75 | $0.foo 76 | } as (_NodeSelf) -> @NodeActor () -> Void) 77 | """ 78 | } 79 | } 80 | 81 | func testMethodNoAttributes() { 82 | assertMacro { 83 | """ 84 | @NodeMethod() 85 | func foo() { 86 | print("hi") 87 | } 88 | """ 89 | } expansion: { 90 | """ 91 | func foo() { 92 | print("hi") 93 | } 94 | 95 | @NodeActor static let $foo 96 | = NodeMethod(attributes: .defaultMethod, { 97 | $0.foo 98 | } as (_NodeSelf) -> @NodeActor () -> Void) 99 | """ 100 | } 101 | } 102 | 103 | func testMethodEffects() { 104 | assertMacro { 105 | """ 106 | @NodeMethod 107 | func foo() async throws { 108 | print("hi") 109 | } 110 | """ 111 | } expansion: { 112 | """ 113 | func foo() async throws { 114 | print("hi") 115 | } 116 | 117 | @NodeActor static let $foo 118 | = NodeMethod(attributes: .defaultMethod, { 119 | $0.foo 120 | } as (_NodeSelf) -> @NodeActor () async throws -> Void) 121 | """ 122 | } 123 | } 124 | 125 | func testTypedThrows() throws { 126 | assertMacro { 127 | """ 128 | @NodeMethod 129 | func foo() throws(CancellationError) { 130 | throw CancellationError() 131 | } 132 | """ 133 | } expansion: { 134 | """ 135 | func foo() throws(CancellationError) { 136 | throw CancellationError() 137 | } 138 | 139 | @NodeActor static let $foo 140 | = NodeMethod(attributes: .defaultMethod, { 141 | $0.foo 142 | } as (_NodeSelf) -> @NodeActor () throws(CancellationError) -> Void) 143 | """ 144 | } 145 | } 146 | 147 | func testMethodReturn() { 148 | assertMacro { 149 | """ 150 | @NodeMethod 151 | func foo() throws -> String { 152 | return "abc" 153 | } 154 | """ 155 | } expansion: { 156 | """ 157 | func foo() throws -> String { 158 | return "abc" 159 | } 160 | 161 | @NodeActor static let $foo 162 | = NodeMethod(attributes: .defaultMethod, { 163 | $0.foo 164 | } as (_NodeSelf) -> @NodeActor () throws -> String) 165 | """ 166 | } 167 | } 168 | 169 | func testMethodArgs() { 170 | assertMacro { 171 | #""" 172 | @NodeMethod 173 | func foo(x: Int) async -> String { 174 | return "\(x)" 175 | } 176 | """# 177 | } expansion: { 178 | #""" 179 | func foo(x: Int) async -> String { 180 | return "\(x)" 181 | } 182 | 183 | @NodeActor static let $foo 184 | = NodeMethod(attributes: .defaultMethod, { 185 | $0.foo 186 | } as (_NodeSelf) -> @NodeActor (Int) async -> String) 187 | """# 188 | } 189 | } 190 | 191 | func testMethodManyArgs() { 192 | assertMacro { 193 | #""" 194 | @NodeMethod 195 | func foo(x: Int, _ y: Double) throws { 196 | return "\(x)" 197 | } 198 | """# 199 | } expansion: { 200 | #""" 201 | func foo(x: Int, _ y: Double) throws { 202 | return "\(x)" 203 | } 204 | 205 | @NodeActor static let $foo 206 | = NodeMethod(attributes: .defaultMethod, { 207 | $0.foo 208 | } as (_NodeSelf) -> @NodeActor (Int, Double) throws -> Void) 209 | """# 210 | } 211 | } 212 | 213 | func testStaticMethod() { 214 | assertMacro { 215 | #""" 216 | @NodeMethod 217 | static func foo(x: Int) throws -> String { 218 | return "\(x)" 219 | } 220 | """# 221 | } expansion: { 222 | #""" 223 | static func foo(x: Int) throws -> String { 224 | return "\(x)" 225 | } 226 | 227 | @NodeActor static let $foo 228 | = NodeMethod(attributes: .defaultMethod, _NodeSelf.foo as @NodeActor (Int) throws -> String) 229 | """# 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Tests/NodeAPIMacrosTests/NodeModuleMacroTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import XCTest 3 | 4 | final class NodeModuleMacrosTests: NodeMacroTest { 5 | func testClosure() { 6 | assertMacro { 7 | """ 8 | #NodeModule { 9 | return 0 10 | } 11 | """ 12 | } expansion: { 13 | """ 14 | @_cdecl("node_swift_register") 15 | public func __macro_local_8registerfMu_(env: Swift.OpaquePointer) -> Swift.OpaquePointer? { 16 | #sourceLocation(file: "Test.swift", line: 1) 17 | NodeAPI.NodeModuleRegistrar(env).register{ 18 | return 0 19 | } 20 | #sourceLocation() 21 | } 22 | """ 23 | } 24 | } 25 | 26 | func testClosureExplicit() { 27 | assertMacro { 28 | """ 29 | #NodeModule(init: { 30 | return 0 31 | }) 32 | """ 33 | } expansion: { 34 | """ 35 | @_cdecl("node_swift_register") 36 | public func __macro_local_8registerfMu_(env: Swift.OpaquePointer) -> Swift.OpaquePointer? { 37 | #sourceLocation(file: "Test.swift", line: 1) 38 | NodeAPI.NodeModuleRegistrar(env).register(init: { 39 | return 0 40 | }) 41 | #sourceLocation() 42 | } 43 | """ 44 | } 45 | } 46 | 47 | func testExports() { 48 | // This MUST be rewritten to a closure because autoclosures appear to use 49 | // caller isolation instead of callee isolation as of Swift 5.10. This means 50 | // anything that requires @NodeActor isolation would otherwise fail. 51 | // 52 | // See: https://github.com/kabiroberai/node-swift/issues/26 53 | assertMacro { 54 | """ 55 | #NodeModule(exports: ["foo": 1]) 56 | """ 57 | } expansion: { 58 | """ 59 | @_cdecl("node_swift_register") 60 | public func __macro_local_8registerfMu_(env: Swift.OpaquePointer) -> Swift.OpaquePointer? { 61 | #sourceLocation(file: "Test.swift", line: 1) 62 | NodeAPI.NodeModuleRegistrar(env).register { ["foo": 1] } 63 | #sourceLocation() 64 | } 65 | """ 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/NodeAPIMacrosTests/NodePropertyMacroTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import XCTest 3 | 4 | final class NodePropertyMacroTests: NodeMacroTest { 5 | func testBasic() { 6 | assertMacro { 7 | #""" 8 | @NodeProperty let x = 5 9 | """# 10 | } expansion: { 11 | #""" 12 | let x = 5 13 | 14 | @NodeActor static let $x 15 | = NodeProperty(attributes: .defaultProperty, \_NodeSelf.x) 16 | """# 17 | } 18 | } 19 | 20 | func testVar() { 21 | assertMacro { 22 | #""" 23 | @NodeProperty var x = 5 24 | """# 25 | } expansion: { 26 | #""" 27 | var x = 5 28 | 29 | @NodeActor static let $x 30 | = NodeProperty(attributes: .defaultProperty, \_NodeSelf.x) 31 | """# 32 | } 33 | } 34 | 35 | func testParens() { 36 | assertMacro { 37 | #""" 38 | @NodeProperty() let x = 5 39 | """# 40 | } expansion: { 41 | #""" 42 | let x = 5 43 | 44 | @NodeActor static let $x 45 | = NodeProperty(attributes: .defaultProperty, \_NodeSelf.x) 46 | """# 47 | } 48 | } 49 | 50 | func testAttributes() { 51 | assertMacro { 52 | #""" 53 | @NodeProperty(.enumerable) let x = 5 54 | """# 55 | } expansion: { 56 | #""" 57 | let x = 5 58 | 59 | @NodeActor static let $x 60 | = NodeProperty(attributes: .enumerable, \_NodeSelf.x) 61 | """# 62 | } 63 | } 64 | 65 | func testNonProperty() { 66 | assertMacro { 67 | #""" 68 | @NodeProperty(.enumerable) func foo() {} 69 | """# 70 | } diagnostics: { 71 | """ 72 | @NodeProperty(.enumerable) func foo() {} 73 | ┬─────────────────────────────────────── 74 | ╰─ 🛑 @NodeProperty can only be applied to a property 75 | """ 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/NodeJSCTests/JSContext+GC.swift: -------------------------------------------------------------------------------- 1 | @preconcurrency import JavaScriptCore 2 | import NodeAPI 3 | 4 | extension JSContext { 5 | func debugGCSync() { 6 | JSSynchronousGarbageCollectForDebugging(jsGlobalContextRef) 7 | } 8 | 9 | @NodeActor func debugGC() async { 10 | // we have to executor-switch to ensure that any existing NodeContext.withContext 11 | // completes and protects escaped values before GCing 12 | await MainActor.run { debugGCSync() } 13 | } 14 | } 15 | 16 | @_silgen_name("JSSynchronousGarbageCollectForDebugging") 17 | private func JSSynchronousGarbageCollectForDebugging(_ context: JSContextRef) 18 | -------------------------------------------------------------------------------- /Tests/NodeJSCTests/NodeJSCTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NodeAPI 2 | import NodeJSC 3 | import XCTest 4 | import JavaScriptCore 5 | 6 | final class NodeJSCTests: XCTestCase { 7 | private let sutBox = Box(nil) 8 | private var sut: JSContext { sutBox.value! } 9 | 10 | override func invokeTest() { 11 | var global: JSManagedValue? 12 | autoreleasepool { 13 | guard let sut = JSContext() else { fatalError("Could not create JSContext") } 14 | sutBox.value = sut 15 | global = JSManagedValue(value: sut.globalObject) 16 | let queue = NodeEnvironment.withJSC(context: sut) { 17 | try NodeAsyncQueue(label: "queue").handle() 18 | } 19 | guard let queue else { fatalError("Could not obtain NodeAsyncQueue") } 20 | NodeActor.$target.withValue(queue) { 21 | super.invokeTest() 22 | } 23 | self.sutBox.value = nil 24 | sut.debugGCSync() 25 | } 26 | if let global { 27 | // TODO: call napi_env_jsc_delete when the time is right 28 | // we might want to use refs as the source of truth 29 | // instead of relying on a unique owner 30 | _ = global 31 | // XCTAssertNil(global.value) 32 | } else { 33 | XCTFail("global == nil") 34 | } 35 | } 36 | 37 | @NodeActor func testBasic() async throws { 38 | let string = try NodeString("Hello, world!") 39 | XCTAssertEqual(try string.string(), "Hello, world!") 40 | } 41 | 42 | @NodeActor func testGC() async throws { 43 | var finalized = false 44 | try autoreleasepool { 45 | let obj = try NodeObject() 46 | try obj.addFinalizer { 47 | finalized = true 48 | } 49 | } 50 | await sut.debugGC() 51 | XCTAssert(finalized) 52 | 53 | finalized = false 54 | let obj = try NodeObject() 55 | try obj.addFinalizer { 56 | finalized = true 57 | } 58 | await sut.debugGC() 59 | _ = finalized 60 | XCTAssertFalse(finalized) 61 | } 62 | 63 | @NodeActor func testWrappedValue() async throws { 64 | let key1 = NodeWrappedDataKey() 65 | let key2 = NodeWrappedDataKey() 66 | let object = try NodeObject() 67 | XCTAssertNil(try object.wrappedValue(forKey: key1)) 68 | try object.setWrappedValue("One", forKey: key1) 69 | try object.setWrappedValue(2, forKey: key2) 70 | XCTAssertEqual(try object.wrappedValue(forKey: key1), "One") 71 | XCTAssertEqual(try object.wrappedValue(forKey: key2), 2) 72 | } 73 | 74 | @NodeActor func testWrappedValueDeinit() async throws { 75 | weak var value: NSObject? 76 | var objectRef: NodeObject? 77 | try autoreleasepool { 78 | let object = try NodeObject() 79 | let key = NodeWrappedDataKey() 80 | let obj = NSObject() 81 | value = obj 82 | try object.setWrappedValue(obj, forKey: key) 83 | objectRef = object 84 | } 85 | await sut.debugGC() 86 | XCTAssertNotNil(value) 87 | _ = objectRef 88 | objectRef = nil 89 | await sut.debugGC() 90 | await sut.debugGC() 91 | XCTAssertNil(value) 92 | } 93 | 94 | @NodeActor func testNodeClassGC() async throws { 95 | nonisolated(unsafe) var finalized1 = false 96 | nonisolated(unsafe) var finalized2 = false 97 | try autoreleasepool { 98 | let obj1 = MyClass { finalized1 = true } 99 | let obj2 = MyClass { finalized2 = true } 100 | try Node.global.stored1.set(to: obj1) 101 | try Node.global.stored2.set(to: obj2) 102 | try Node.global.stored2.set(to: null) 103 | } 104 | await sut.debugGC() 105 | XCTAssertFalse(finalized1) 106 | XCTAssertTrue(finalized2) 107 | } 108 | 109 | @NodeActor func testPromise() async throws { 110 | try Node.tick.set(to: NodeFunction { _ in 111 | await Task.yield() 112 | }) 113 | let obj = try Node.run(script: """ 114 | (async () => { 115 | await tick() 116 | return 123 117 | })() 118 | """) 119 | let value = try await obj.as(NodePromise.self)?.value.as(NodeNumber.self)?.double() 120 | XCTAssertEqual(value, 123) 121 | } 122 | 123 | @NodeActor func testThrowing() async throws { 124 | var threw = false 125 | do { 126 | try Node.run(script: "blah") 127 | } catch { 128 | threw = true 129 | let unwrapped = try XCTUnwrap((error as? AnyNodeValue)?.as(NodeError.self)) 130 | let name = try unwrapped.name.as(String.self) 131 | XCTAssertEqual(name, "ReferenceError") 132 | let message = try unwrapped.message.as(String.self) 133 | XCTAssertEqual(message, "Can't find variable: blah") 134 | } 135 | XCTAssert(threw, "Expected script to throw") 136 | } 137 | 138 | @NodeActor func testPropertyNames() async throws { 139 | let object = try NodeObject() 140 | try object.foo.set(to: "bar") 141 | 142 | let names = try XCTUnwrap(object.propertyNames( 143 | collectionMode: .includePrototypes, 144 | filter: .allProperties, 145 | conversion: .numbersToStrings 146 | ).as([String].self)) 147 | XCTAssert(Set(["constructor", "__proto__", "toString", "foo"]).subtracting(names).isEmpty) 148 | 149 | let ownNames = try XCTUnwrap(object.propertyNames( 150 | collectionMode: .ownOnly, 151 | filter: .allProperties, 152 | conversion: .numbersToStrings 153 | ).as([String].self)) 154 | XCTAssertEqual(ownNames, ["foo"]) 155 | } 156 | } 157 | 158 | @NodeClass final class MyClass { 159 | let onDeinit: @Sendable () -> Void 160 | init(onDeinit: @escaping @Sendable () -> Void) { 161 | self.onDeinit = onDeinit 162 | } 163 | 164 | deinit { onDeinit() } 165 | } 166 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /package-lock.json 2 | /Package.resolved 3 | /.swiftpm 4 | -------------------------------------------------------------------------------- /example/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "MyExample", 7 | platforms: [.macOS(.v10_15)], 8 | products: [ 9 | .library( 10 | name: "MyExample", 11 | targets: ["MyExample"] 12 | ), 13 | .library( 14 | name: "Module", 15 | type: .dynamic, 16 | targets: ["MyExample"] 17 | ) 18 | ], 19 | dependencies: [ 20 | .package(path: "node_modules/node-swift") 21 | ], 22 | targets: [ 23 | .target( 24 | name: "MyExample", 25 | dependencies: [ 26 | .product(name: "NodeAPI", package: "node-swift"), 27 | .product(name: "NodeModuleSupport", package: "node-swift"), 28 | ] 29 | ) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /example/Sources/MyExample/MyExample.swift: -------------------------------------------------------------------------------- 1 | import NodeAPI 2 | 3 | #NodeModule(exports: [ 4 | "nums": [Double.pi.rounded(.down), Double.pi.rounded(.up)], 5 | "str": String(repeating: "NodeSwift! ", count: 3), 6 | "add": try NodeFunction { (a: Double, b: Double) in 7 | print("calculating...") 8 | try await Task.sleep(nanoseconds: 500_000_000) 9 | return "\(a) + \(b) = \(a + b)" 10 | }, 11 | ]) 12 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const { nums, str, add } = require("./.build/Module.node"); 2 | console.log(nums); // [ 3, 4 ] 3 | console.log(str); // NodeSwift! NodeSwift! NodeSwift! 4 | add(5, 10).then(console.log); // 5.0 + 10.0 = 15.0 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-swift-example", 3 | "version": "1.0.0", 4 | "description": "Example node-swift package", 5 | "main": "index.js", 6 | "scripts": { 7 | "install": "node-swift rebuild" 8 | }, 9 | "author": "Kabir Oberai", 10 | "license": "CC0-1.0", 11 | "dependencies": { 12 | "node-swift": "file:.." 13 | }, 14 | "private": true 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-swift", 3 | "version": "1.0.0", 4 | "repository": "kabiroberai/node-swift", 5 | "description": "Support for creating Node modules in Swift", 6 | "bin": "./lib/cli.js", 7 | "main": "./lib/builder.js", 8 | "scripts": { 9 | "test": "node test", 10 | "build": "tsc", 11 | "prepare": "npm run build", 12 | "lint": "eslint ." 13 | }, 14 | "files": [ 15 | "lib", 16 | "Sources", 17 | "vendored", 18 | "Package.swift" 19 | ], 20 | "author": "Kabir Oberai", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/node": "^16.9.0", 24 | "@typescript-eslint/eslint-plugin": "^4.31.0", 25 | "@typescript-eslint/parser": "^4.31.0", 26 | "eslint": "^7.2.0", 27 | "eslint-config-airbnb-base": "^14.2.1", 28 | "eslint-config-airbnb-typescript": "^14.0.0", 29 | "eslint-plugin-import": "^2.22.1", 30 | "mocha": "^9.1.0", 31 | "typescript": "^4.4.2" 32 | }, 33 | "dependencies": { 34 | "import-cwd": "^3.0.0" 35 | }, 36 | "private": true 37 | } 38 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as builder from "./builder"; 4 | 5 | function usage(): never { 6 | console.log("Usage: node-swift [rebuild [--debug] | build [--debug] | clean]"); 7 | process.exit(1); 8 | } 9 | 10 | async function doClean(checkArgs: boolean = false) { 11 | if (checkArgs && process.argv.length !== 3) usage(); 12 | await builder.clean(); 13 | } 14 | 15 | async function doBuild() { 16 | let mode: builder.BuildMode; 17 | if (process.argv.length === 3) { 18 | mode = "release"; 19 | } else if (process.argv.length === 4 && process.argv[3] === "--debug") { 20 | mode = "debug"; 21 | } else { 22 | usage(); 23 | } 24 | const config = require("import-cwd")("./package.json").swift || {}; 25 | await builder.build(mode, config); 26 | } 27 | 28 | (async () => { 29 | if (process.argv.length == 2) { 30 | process.argv.push("rebuild"); 31 | } 32 | 33 | switch (process.argv[2]) { 34 | case "build": 35 | await doBuild(); 36 | break; 37 | case "clean": 38 | await doClean(true); 39 | break; 40 | case "rebuild": 41 | await doClean(); 42 | await doBuild(); 43 | break; 44 | default: 45 | usage(); 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { symlink, unlink } from "fs/promises"; 2 | 3 | async function forceSymlink(target: string, path: string) { 4 | try { 5 | await unlink(path); 6 | } catch (e) {} 7 | await symlink(target, path); 8 | } 9 | 10 | export { forceSymlink }; 11 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /Package.resolved 2 | -------------------------------------------------------------------------------- /test/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | import Foundation 5 | 6 | // using ".." doesn't work on Windows. Also, 7 | // relative paths work via the CLI but not with Xcode. 8 | let testDir = URL(filePath: #filePath).deletingLastPathComponent() 9 | let suites = testDir.appending(component: "suites") 10 | let nodeSwiftPath = testDir.deletingLastPathComponent().path 11 | 12 | let package = Package( 13 | name: "NodeAPITests", 14 | platforms: [.macOS("10.15")], 15 | dependencies: [.package(name: "node-swift", path: nodeSwiftPath)] 16 | ) 17 | 18 | @MainActor func addSuite(_ suite: String) { 19 | package.products.append(.library( 20 | name: suite, 21 | type: .dynamic, 22 | targets: [suite] 23 | )) 24 | package.products.append(.library( 25 | name: "\(suite)-Automatic", 26 | targets: [suite] 27 | )) 28 | package.targets.append(.target( 29 | name: suite, 30 | dependencies: [ 31 | .product(name: "NodeAPI", package: "node-swift"), 32 | .product(name: "NodeModuleSupport", package: "node-swift"), 33 | ], 34 | path: "suites/\(suite)", 35 | exclude: ["index.js"], 36 | swiftSettings: [ 37 | .unsafeFlags(["-Xfrontend", "-warn-concurrency"]) 38 | ] 39 | )) 40 | } 41 | 42 | for suite in try FileManager.default.contentsOfDirectory(atPath: suites.path) 43 | where !suite.hasPrefix(".") { 44 | addSuite(suite) 45 | } 46 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs").promises; 2 | const builder = require("../lib/builder"); 3 | const { spawnSync } = require("child_process"); 4 | 5 | process.chdir(__dirname); 6 | 7 | function usage() { 8 | console.log("Usage: test [all|suite ]"); 9 | process.exit(1); 10 | } 11 | 12 | async function runSuite(suite, isChild) { 13 | console.log(`Running suite '${suite}'`); 14 | await builder.build("debug", { product: suite }); 15 | require(`./suites/${suite}`); 16 | } 17 | 18 | async function runAll() { 19 | const suites = (await fs.readdir("suites")).filter(f => !f.startsWith(".")); 20 | let hasFailure = false; 21 | for (const suite of suites) { 22 | // invoke isChild processes because that way lifetime stuff 23 | // is handled on a per-test basis 24 | const status = spawnSync( 25 | "node", [__filename, "_suite", suite], 26 | { stdio: [process.stdin, process.stdout, process.stderr] } 27 | ).status; 28 | if (status === 0) { 29 | console.log(`Suite '${suite}' passed!`); 30 | } else { 31 | hasFailure = true; 32 | console.log(`Suite '${suite}' failed: exit code ${status}`); 33 | } 34 | } 35 | if (!hasFailure) console.log("All tests passed!"); 36 | } 37 | 38 | (async () => { 39 | const command = process.argv[2] || "all"; 40 | let isChild = false; 41 | switch (command) { 42 | case "all": 43 | if (process.argv.length > 3) usage(); 44 | await builder.clean(); 45 | await runAll(); 46 | break; 47 | case "_suite": 48 | isChild = true; 49 | // fallthrough 50 | case "suite": 51 | if (process.argv.length !== 4) usage(); 52 | const suite = process.argv[3]; 53 | if (!isChild) await builder.clean(); 54 | await runSuite(suite, isChild); 55 | break; 56 | default: 57 | usage(); 58 | } 59 | })(); 60 | -------------------------------------------------------------------------------- /test/suites/Integration/Integration.swift: -------------------------------------------------------------------------------- 1 | import NodeAPI 2 | import Foundation 3 | 4 | final class CleanupHandler: Sendable { 5 | let global: NodeObject 6 | init(global: NodeObject) { 7 | self.global = global 8 | } 9 | deinit { 10 | print("Cleanup!") 11 | } 12 | 13 | @NodeInstanceData static var shared: CleanupHandler? 14 | } 15 | 16 | #NodeModule { 17 | let captured = try NodeString("hi") 18 | 19 | try Node.setTimeout(NodeFunction { 20 | print("Called our timeout! Captured: \(captured)") 21 | return undefined 22 | }, 1000) 23 | 24 | let res = try Node.run(script: "[1, 15]").as(NodeArray.self)! 25 | print("Count: \(try res.count())") 26 | print("Num: \(try res[1].nodeValue())") 27 | 28 | print("Symbol.iterator is a \(try Node.Symbol.iterator.nodeType())") 29 | 30 | let strObj = try Node.run(script: "('hello')") 31 | print("'\(strObj)' is a \(try strObj.nodeType())") 32 | 33 | let doStuff = try NodeFunction(name: "doStuff") { args in 34 | print("Called! Arg 0 type: \(try args.first?.nodeType() as Any)") 35 | return 5 36 | } 37 | let exports = doStuff 38 | try doStuff("hello", 15) 39 | 40 | let key = NodeWrappedDataKey() 41 | let obj = try NodeObject() 42 | try obj.setWrappedValue("hello", forKey: key) 43 | print("wrapped value: \(try obj.wrappedValue(forKey: key) ?? "NOT FOUND")") 44 | try obj.setWrappedValue(nil, forKey: key) 45 | print("wrapped value (shouldn't be found): \(try obj.wrappedValue(forKey: key) ?? "NOT FOUND")") 46 | 47 | try withExtendedLifetime(Node.global) { 48 | print("First copy of global: \($0)") 49 | } 50 | 51 | CleanupHandler.shared = CleanupHandler(global: try Node.global) 52 | 53 | Task { 54 | try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC) 55 | try print(Node.run(script: "1+1")) 56 | } 57 | 58 | let promise = try NodePromise { 59 | try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC) 60 | return 5 61 | } 62 | 63 | Task { 64 | print("PROMISE: \(try await promise.value)") 65 | } 66 | 67 | return exports 68 | } 69 | -------------------------------------------------------------------------------- /test/suites/Integration/index.js: -------------------------------------------------------------------------------- 1 | console.log("JS: startup"); 2 | 3 | const assert = require("assert"); 4 | const res = require("../../.build/Integration.node"); 5 | console.log("JS: called require()"); 6 | console.log(`JS: exports = ${res}`); 7 | const r = res(); 8 | assert.strictEqual(r, 5); 9 | 10 | setTimeout(() => { 11 | console.log("JS: setTimeout() called back"); 12 | }, 2000); 13 | -------------------------------------------------------------------------------- /test/suites/Test/Test.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NodeAPI 3 | 4 | @NodeClass @NodeActor final class File { 5 | static let extraProperties: NodeClassPropertyList = [ 6 | "contents": NodeProperty( 7 | of: File.self, 8 | get: { $0.contents }, 9 | set: { $0.setContents } 10 | ), 11 | "filename": NodeProperty( 12 | of: File.self, 13 | get: { $0.filename } 14 | ), 15 | ] 16 | 17 | @NodeProperty 18 | var x: Int = 0 19 | 20 | let url: URL 21 | 22 | init(url: URL) { 23 | self.url = url 24 | } 25 | 26 | @NodeConstructor 27 | init(_ path: String) { 28 | url = URL(fileURLWithPath: path) 29 | } 30 | 31 | @NodeMethod 32 | static func `default`() throws -> NodeValueConvertible { 33 | return try File(url: URL(fileURLWithPath: "default.txt")).wrapped() 34 | } 35 | 36 | func filename() throws -> String { 37 | url.lastPathComponent 38 | } 39 | 40 | func contents() throws -> Data { 41 | try Data(contentsOf: url) 42 | } 43 | 44 | func setContents(_ newValue: Data) throws { 45 | try newValue.write(to: url, options: .atomic) 46 | } 47 | 48 | // unrelated to files but an important test nonetheless 49 | @NodeMethod 50 | func reply(_ parameter: String?) -> String { 51 | "You said \(parameter ?? "nothing")" 52 | } 53 | 54 | @NodeMethod 55 | func unlink() throws -> NodeValueConvertible { 56 | try FileManager.default.removeItem(at: url) 57 | return undefined 58 | } 59 | } 60 | 61 | #NodeModule(exports: ["File": File.deferredConstructor]) 62 | -------------------------------------------------------------------------------- /test/suites/Test/index.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | 3 | const { File } = require("../../.build/Test.node"); 4 | 5 | assert.strictEqual(File.default().filename, "default.txt") 6 | 7 | const file = new File("test.txt"); 8 | assert.strictEqual(file.filename, "test.txt") 9 | 10 | let err = ""; 11 | try { 12 | file.contents 13 | } catch (e) { 14 | err = `${e}`; 15 | } 16 | assert(err.includes("NSPOSIXErrorDomain")) 17 | 18 | const toAdd = "hello, world!\n" 19 | file.contents = Buffer.from(toAdd); 20 | assert(file.contents.toString().endsWith(toAdd)); 21 | file.unlink(); 22 | 23 | assert.strictEqual(file.reply("hi"), "You said hi"); 24 | assert.strictEqual(file.reply(null), "You said nothing"); 25 | assert.strictEqual(file.reply(undefined), "You said nothing"); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "outDir": "lib", 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true 10 | }, 11 | "include": [ 12 | "./src" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /vendored/node/lib/node-win32-x64.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kabiroberai/node-swift/8af75ea6741c5cd48ed2ca90099b2ff45ac933c5/vendored/node/lib/node-win32-x64.lib --------------------------------------------------------------------------------