├── .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
--------------------------------------------------------------------------------