objc-js 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/build/Release/nobjc_native.node +0 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +112 -0
- package/dist/native.d.ts +4 -0
- package/dist/native.js +5 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iamEvan
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# objc-js
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This is not production ready.
|
|
5
|
+
|
|
6
|
+
**objc-js** is an Objective-C bridge for Node.js. This is a fork of [nobjc](https://github.com/nmggithub/nobjc) by [Noah Gregory](https://github.com/nmggithub).
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### Basic Usage
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { NobjcLibrary } from "objc-js";
|
|
14
|
+
|
|
15
|
+
// Load a framework
|
|
16
|
+
const foundation = new NobjcLibrary("/System/Library/Frameworks/Foundation.framework/Foundation");
|
|
17
|
+
|
|
18
|
+
// Get a class and call methods
|
|
19
|
+
const NSString = foundation["NSString"];
|
|
20
|
+
const str = NSString.stringWithUTF8String$("Hello, World!");
|
|
21
|
+
console.log(str.toString());
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Protocol Implementation
|
|
25
|
+
|
|
26
|
+
**objc-js** now supports creating Objective-C protocol implementations from JavaScript. This allows you to create delegate objects that can be passed to Objective-C APIs.
|
|
27
|
+
|
|
28
|
+
#### Creating a Protocol Implementation
|
|
29
|
+
|
|
30
|
+
Use `NobjcProtocol.implement()` to create an object that implements a protocol:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { NobjcProtocol } from "objc-js";
|
|
34
|
+
|
|
35
|
+
const delegate = NobjcProtocol.implement("ASAuthorizationControllerDelegate", {
|
|
36
|
+
authorizationController$didCompleteWithAuthorization$: (controller, authorization) => {
|
|
37
|
+
console.log("Authorization completed successfully!");
|
|
38
|
+
console.log("Authorization:", authorization);
|
|
39
|
+
},
|
|
40
|
+
authorizationController$didCompleteWithError$: (controller, error) => {
|
|
41
|
+
console.error("Authorization failed:", error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Pass the delegate to an Objective-C API
|
|
46
|
+
authController.setDelegate$(delegate);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Method Naming Convention
|
|
50
|
+
|
|
51
|
+
Method names use the `$` notation to represent colons in Objective-C selectors:
|
|
52
|
+
|
|
53
|
+
- Objective-C: `authorizationController:didCompleteWithAuthorization:`
|
|
54
|
+
- JavaScript: `authorizationController$didCompleteWithAuthorization$`
|
|
55
|
+
|
|
56
|
+
#### Argument and Return Value Marshalling
|
|
57
|
+
|
|
58
|
+
Arguments are automatically converted between JavaScript and Objective-C:
|
|
59
|
+
|
|
60
|
+
- **Primitives**: Numbers, booleans, and strings are automatically converted
|
|
61
|
+
- **Objects**: Objective-C objects are wrapped in `NobjcObject` instances
|
|
62
|
+
- **null/nil**: JavaScript `null` maps to Objective-C `nil` and vice versa
|
|
63
|
+
|
|
64
|
+
#### Memory Management
|
|
65
|
+
|
|
66
|
+
Memory is automatically managed:
|
|
67
|
+
|
|
68
|
+
- JavaScript callbacks are kept alive as long as the delegate object exists
|
|
69
|
+
- When the Objective-C object is deallocated, the callbacks are automatically released
|
|
70
|
+
- No manual cleanup is required
|
|
71
|
+
|
|
72
|
+
#### Example: WebAuthn/Passkeys with AuthenticationServices
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { NobjcLibrary, NobjcProtocol } from "objc-js";
|
|
76
|
+
|
|
77
|
+
const authServices = new NobjcLibrary(
|
|
78
|
+
"/System/Library/Frameworks/AuthenticationServices.framework/AuthenticationServices"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Create authorization requests
|
|
82
|
+
const ASAuthorizationController = authServices["ASAuthorizationController"];
|
|
83
|
+
const controller = ASAuthorizationController.alloc().initWithAuthorizationRequests$(requests);
|
|
84
|
+
|
|
85
|
+
// Create a delegate
|
|
86
|
+
const delegate = NobjcProtocol.implement("ASAuthorizationControllerDelegate", {
|
|
87
|
+
authorizationController$didCompleteWithAuthorization$: (controller, authorization) => {
|
|
88
|
+
// Handle successful authorization
|
|
89
|
+
const credential = authorization.credential();
|
|
90
|
+
console.log("Credential:", credential);
|
|
91
|
+
},
|
|
92
|
+
authorizationController$didCompleteWithError$: (controller, error) => {
|
|
93
|
+
// Handle error
|
|
94
|
+
console.error("Authorization error:", error.localizedDescription());
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Set the delegate and perform requests
|
|
99
|
+
controller.setDelegate$(delegate);
|
|
100
|
+
controller.performRequests();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Notes
|
|
104
|
+
|
|
105
|
+
- Protocol implementations are created at runtime using the Objective-C runtime APIs
|
|
106
|
+
- If a protocol is not found, a class is still created (useful for informal protocols)
|
|
107
|
+
- Method signatures are inferred from the protocol or from the method name
|
|
108
|
+
- Thread safety: Currently assumes single-threaded (main thread) usage
|
|
109
|
+
|
|
110
|
+
### API Reference
|
|
111
|
+
|
|
112
|
+
#### `NobjcLibrary`
|
|
113
|
+
|
|
114
|
+
Creates a proxy for accessing Objective-C classes from a framework.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const framework = new NobjcLibrary(path: string);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### `NobjcObject`
|
|
121
|
+
|
|
122
|
+
Wrapper for Objective-C objects. Methods can be called using the `$` notation.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const result = object.methodName$arg1$arg2$(arg1, arg2);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### `NobjcProtocol`
|
|
129
|
+
|
|
130
|
+
Static class for creating protocol implementations.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
NobjcProtocol.implement(
|
|
134
|
+
protocolName: string,
|
|
135
|
+
methodImplementations: Record<string, (...args: any[]) => any>
|
|
136
|
+
): NobjcObject
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Parameters:**
|
|
140
|
+
|
|
141
|
+
- `protocolName`: The name of the Objective-C protocol (e.g., "NSCopying", "ASAuthorizationControllerDelegate")
|
|
142
|
+
- `methodImplementations`: An object mapping method names (using `$` notation) to JavaScript functions
|
|
143
|
+
|
|
144
|
+
**Returns:** A `NobjcObject` that can be passed to Objective-C APIs expecting the protocol
|
|
Binary file
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NobjcNative } from "./native.js";
|
|
2
|
+
declare class NobjcLibrary {
|
|
3
|
+
[key: string]: NobjcObject;
|
|
4
|
+
constructor(library: string);
|
|
5
|
+
}
|
|
6
|
+
declare class NobjcObject {
|
|
7
|
+
[key: string]: NobjcMethod;
|
|
8
|
+
constructor(object: NobjcNative.ObjcObject);
|
|
9
|
+
}
|
|
10
|
+
declare class NobjcMethod {
|
|
11
|
+
constructor(object: NobjcNative.ObjcObject, methodName: string);
|
|
12
|
+
}
|
|
13
|
+
declare class NobjcProtocol {
|
|
14
|
+
static implement(protocolName: string, methodImplementations: Record<string, (...args: any[]) => any>): NobjcObject;
|
|
15
|
+
}
|
|
16
|
+
export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { LoadLibrary, GetClassObject, ObjcObject, CreateProtocolImplementation } from "./native.js";
|
|
2
|
+
const NATIVE_OBJC_OBJECT = Symbol("nativeObjcObject");
|
|
3
|
+
class NobjcLibrary {
|
|
4
|
+
constructor(library) {
|
|
5
|
+
const handler = {
|
|
6
|
+
wasLoaded: false,
|
|
7
|
+
get(_, className) {
|
|
8
|
+
if (!this.wasLoaded) {
|
|
9
|
+
LoadLibrary(library);
|
|
10
|
+
this.wasLoaded = true;
|
|
11
|
+
}
|
|
12
|
+
return new NobjcObject(GetClassObject(className));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
return new Proxy({}, handler);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function NobjcMethodNameToObjcSelector(methodName) {
|
|
19
|
+
return methodName.replace(/\$/g, ":");
|
|
20
|
+
}
|
|
21
|
+
// unused, might be useful for codegen later
|
|
22
|
+
function ObjcSelectorToNobjcMethodName(selector) {
|
|
23
|
+
return selector.replace(/:/g, "$");
|
|
24
|
+
}
|
|
25
|
+
class NobjcObject {
|
|
26
|
+
constructor(object) {
|
|
27
|
+
const handler = {
|
|
28
|
+
has(target, p) {
|
|
29
|
+
// Return true for the special Symbol to enable unwrapping
|
|
30
|
+
if (p === NATIVE_OBJC_OBJECT)
|
|
31
|
+
return true;
|
|
32
|
+
// guard against other symbols
|
|
33
|
+
if (typeof p === "symbol")
|
|
34
|
+
return Reflect.has(target, p);
|
|
35
|
+
// toString is always present
|
|
36
|
+
if (p === "toString")
|
|
37
|
+
return true;
|
|
38
|
+
// check if the object responds to the selector
|
|
39
|
+
return target.$msgSend("respondsToSelector:", NobjcMethodNameToObjcSelector(p.toString()));
|
|
40
|
+
},
|
|
41
|
+
get(target, methodName, receiver) {
|
|
42
|
+
// Return the underlying native object when Symbol is accessed
|
|
43
|
+
if (methodName === NATIVE_OBJC_OBJECT) {
|
|
44
|
+
return target;
|
|
45
|
+
}
|
|
46
|
+
// guard against symbols
|
|
47
|
+
if (typeof methodName === "symbol") {
|
|
48
|
+
return Reflect.get(object, methodName, receiver);
|
|
49
|
+
}
|
|
50
|
+
// handle toString separately
|
|
51
|
+
if (methodName === "toString") {
|
|
52
|
+
return () => String(object.$msgSend("description"));
|
|
53
|
+
}
|
|
54
|
+
if (!(methodName in receiver)) {
|
|
55
|
+
throw new Error(`Method ${methodName} not found on object ${receiver}`);
|
|
56
|
+
}
|
|
57
|
+
return new NobjcMethod(object, methodName);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return new Proxy(object, handler);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function unwrapArg(arg) {
|
|
64
|
+
if (arg && typeof arg === "object" && NATIVE_OBJC_OBJECT in arg) {
|
|
65
|
+
return arg[NATIVE_OBJC_OBJECT];
|
|
66
|
+
}
|
|
67
|
+
return arg;
|
|
68
|
+
}
|
|
69
|
+
class NobjcMethod {
|
|
70
|
+
constructor(object, methodName) {
|
|
71
|
+
const selector = NobjcMethodNameToObjcSelector(methodName);
|
|
72
|
+
// This cannot be an arrow function because we need to access `arguments`.
|
|
73
|
+
function methodFunc() {
|
|
74
|
+
const unwrappedArgs = Array.from(arguments).map(unwrapArg);
|
|
75
|
+
const result = object.$msgSend(selector, ...unwrappedArgs);
|
|
76
|
+
if (typeof result == "object" && result instanceof ObjcObject) {
|
|
77
|
+
return new NobjcObject(result);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const handler = {};
|
|
82
|
+
return new Proxy(methodFunc, handler);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
class NobjcProtocol {
|
|
86
|
+
static implement(protocolName, methodImplementations) {
|
|
87
|
+
// Convert method names from $ notation to : notation
|
|
88
|
+
const convertedMethods = {};
|
|
89
|
+
for (const [methodName, impl] of Object.entries(methodImplementations)) {
|
|
90
|
+
const selector = NobjcMethodNameToObjcSelector(methodName);
|
|
91
|
+
// Wrap the implementation to unwrap args and wrap return values
|
|
92
|
+
convertedMethods[selector] = function (...args) {
|
|
93
|
+
const unwrappedArgs = args.map(unwrapArg);
|
|
94
|
+
const result = impl(...unwrappedArgs);
|
|
95
|
+
// If the result is already a NobjcObject, unwrap it to get the native object
|
|
96
|
+
if (result && typeof result === "object" && NATIVE_OBJC_OBJECT in result) {
|
|
97
|
+
return result[NATIVE_OBJC_OBJECT];
|
|
98
|
+
}
|
|
99
|
+
// If the result is a native ObjcObject, return it as-is
|
|
100
|
+
if (typeof result === "object" && result instanceof ObjcObject) {
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Call native implementation
|
|
107
|
+
const nativeObj = CreateProtocolImplementation(protocolName, convertedMethods);
|
|
108
|
+
// Wrap in NobjcObject proxy
|
|
109
|
+
return new NobjcObject(nativeObj);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export { NobjcLibrary, NobjcObject, NobjcMethod, NobjcProtocol };
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as _binding from "#nobjc_native";
|
|
2
|
+
declare const LoadLibrary: typeof _binding.LoadLibrary, GetClassObject: typeof _binding.GetClassObject, ObjcObject: typeof _binding.ObjcObject, CreateProtocolImplementation: typeof _binding.CreateProtocolImplementation;
|
|
3
|
+
export { LoadLibrary, GetClassObject, ObjcObject, CreateProtocolImplementation };
|
|
4
|
+
export type { _binding as NobjcNative };
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const binding = require("#nobjc_native");
|
|
4
|
+
const { LoadLibrary, GetClassObject, ObjcObject, CreateProtocolImplementation } = binding;
|
|
5
|
+
export { LoadLibrary, GetClassObject, ObjcObject, CreateProtocolImplementation };
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "objc-js",
|
|
3
|
+
"private": false,
|
|
4
|
+
"access": "public",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"os": [
|
|
7
|
+
"darwin"
|
|
8
|
+
],
|
|
9
|
+
"repository": "https://github.com/iamEvanYT/objc-js",
|
|
10
|
+
"author": "iamEvan",
|
|
11
|
+
"imports": {
|
|
12
|
+
"#nobjc_native": "./build/Release/nobjc_native.node"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js",
|
|
16
|
+
"./native": "./build/Release/nobjc_native.node"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"build/Release/nobjc_native.node"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build-native": "node-gyp build",
|
|
24
|
+
"build-scripts": "tsc --project scripts/tsconfig.json",
|
|
25
|
+
"build-source": "tsc --project src/ts/tsconfig.json",
|
|
26
|
+
"prebuild": "node-gyp clean && node-gyp configure",
|
|
27
|
+
"build": "npm run build-native && npm run build-scripts && npm run build-source",
|
|
28
|
+
"pretest": "npm run build",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"test:native": "bun test tests/test-native-code.test.ts",
|
|
31
|
+
"test:js": "bun test tests/test-js-code.test.ts",
|
|
32
|
+
"test:string-lifetime": "bun test tests/test-string-lifetime.test.ts",
|
|
33
|
+
"test:object-arguments": "bun test tests/test-object-arguments.test.ts",
|
|
34
|
+
"test:protocol-implementation": "bun test tests/test-protocol-implementation.test.ts",
|
|
35
|
+
"make-clangd-config": "node ./scripts/make-clangd-config.js",
|
|
36
|
+
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
|
37
|
+
"preinstall-disabled": "npm run build-scripts && npm run make-clangd-config"
|
|
38
|
+
},
|
|
39
|
+
"version": "0.0.4",
|
|
40
|
+
"description": "Objective-C bridge for Node.js",
|
|
41
|
+
"main": "dist/index.js",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"node-addon-api": "^8.5.0",
|
|
46
|
+
"node-gyp": "^11.4.2",
|
|
47
|
+
"prettier": "^3.7.4",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"gypfile": true
|
|
51
|
+
}
|