├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── deno.json ├── deps.ts ├── mod.ts ├── src ├── autoreleasePool.ts ├── bindings.ts ├── block.ts ├── class.ts ├── common.ts ├── encoding.ts ├── imp.ts ├── ivar.ts ├── method.ts ├── objc.ts ├── object.ts ├── property.ts ├── protocol.ts ├── sel.ts └── util.ts ├── test ├── appkit.ts ├── deps.ts ├── encoding.ts ├── pasteboard.ts └── test.ts └── test_appkit ├── jsx.ts ├── main.tsx ├── mod.ts ├── sys.ts └── util.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@main 19 | with: 20 | deno-version: 'v1.x' 21 | 22 | - name: Formating 23 | run: deno fmt --check 24 | 25 | - name: Lint 26 | run: deno lint --unstable 27 | 28 | test: 29 | runs-on: macos-latest 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v2 34 | 35 | - name: Setup Deno 36 | uses: denoland/setup-deno@main 37 | with: 38 | deno-version: 'v1.x' 39 | 40 | - name: Test Basic 41 | run: deno task test 42 | 43 | - name: Test Encoding 44 | run: deno task test-encoding 45 | 46 | # - name: Test AppKit 47 | # run: deno task test-appkit 48 | 49 | - name: Test Pasteboard 50 | run: deno task test-pasteboard 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test.dylib 2 | .vscode/ 3 | build/ 4 | deno/ 5 | deno.lock 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 DjDeveloperr 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno Objective-C 2 | 3 | [![Tags](https://img.shields.io/github/release/DjDeveloperr/deno_objc)](https://github.com/DjDeveloperr/deno_objc/releases) 4 | [![Doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/objc@0.1.0/mod.ts) 5 | [![Checks](https://github.com/DjDeveloperr/deno_objc/actions/workflows/ci.yml/badge.svg)](https://github.com/DjDeveloperr/deno_objc/actions/workflows/ci.yml) 6 | [![License](https://img.shields.io/github/license/DjDeveloperr/deno_objc)](https://github.com/DjDeveloperr/deno_objc/blob/master/LICENSE) 7 | [![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/DjDeveloperr) 8 | 9 | Objective-C Runtime Bridge for Deno. 10 | 11 | ```ts 12 | import objc from "https://deno.land/x/objc@0.1.0/mod.ts"; 13 | 14 | objc.import("AppKit"); 15 | 16 | const { NSPasteboard } = objc.classes; 17 | 18 | const pasteboard = NSPasteboard.generalPasteboard(); 19 | 20 | const result = pasteboard.stringForType("public.utf8-plain-text"); 21 | // or 22 | const result = objc 23 | .send`${pasteboard} stringForType:${"public.utf8-plain-text"}`; 24 | 25 | // Convert to JS String 26 | console.log(result.UTF8String()); 27 | ``` 28 | 29 | ## Usage 30 | 31 | This is mainly for interfacing with macOS Frameworks, but it can also be used 32 | with GNUstep libobjc2. 33 | 34 | By default, `libobjc.dylib`, `libobjc.so` or `objc.dll` will be loaded depending 35 | on the platform. 36 | 37 | If you want to override that, use `DENO_OBJC_PATH` env variable. 38 | 39 | ## API 40 | 41 | To retrieve a class, use `objc.classes`: 42 | 43 | ```ts 44 | const { NSPasteboard } = objc.classes; 45 | ``` 46 | 47 | To retrieve a protocol, use `objc.protocols`: 48 | 49 | ```ts 50 | const { NSPasteboardReading } = objc.protocols; 51 | ``` 52 | 53 | In Obj-C, take the following method: 54 | 55 | ```cpp 56 | - (void)setString:(NSString *)string; 57 | ``` 58 | 59 | Now to send it, you do 60 | 61 | ```cpp 62 | [self setString:@"Hello World"]; 63 | ``` 64 | 65 | But in JavaScript, you can do it in two ways. One is using the proxied method: 66 | 67 | ```js 68 | self.setString_("Hello World"); 69 | // or 70 | self.setString("Hello World"); 71 | ``` 72 | 73 | Alternative way is to use `objc.send`: 74 | 75 | ```js 76 | objc.send`${self} setString:${"Hello World"}`; 77 | ``` 78 | 79 | When sending a message, the types of course need to be converted into native 80 | ones. For example, by default the native string type in Obj-C is actually just 81 | null terminated C string. So the JS string will be converted to that if the 82 | method needs. 83 | 84 | Otherwise, if we find that the method takes an `id` type, we will try to convert 85 | it to `NSString` instead. 86 | 87 | Importing frameworks at runtime is done via `NSBundle`. By default, only 88 | `Foundation` framework is loaded. 89 | 90 | ## Security 91 | 92 | Since this module makes heavy use of FFI, it is inherently unsafe and gives 93 | access to low level system primitives. 94 | 95 | As such, to use this module you need to pass these flags: 96 | 97 | - `--allow-env`: To check for a possible `DENO_OBJC_PATH` value 98 | - `--allow-ffi`: To load ObjC runtime dynamic library 99 | - `--unstable`: The FFI API in Deno is an unstable API (it can change) 100 | 101 | The allow-ffi permission basically breaks through entire security sandbox, so 102 | you can also just pass `-A`/`--allow-all`. 103 | 104 | ## License 105 | 106 | [Apache-2.0](./LICENSE) licensed. 107 | 108 | Copyright 2022 © DjDeveloperr 109 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "test": "deno run -A --unstable --no-check ./test/test.ts", 4 | "test-appkit": "deno run -A --unstable --no-check ./test/appkit.ts", 5 | "test-appkitx": "deno run -A --unstable --no-check ./test_appkit/main.tsx", 6 | "test-pasteboard": "deno test -A --unstable --no-check ./test/pasteboard.ts", 7 | "test-encoding": "deno test -A --unstable --no-check ./test/encoding.ts", 8 | "local": "./deno/target/release/deno run -A --unstable" 9 | }, 10 | "lint": { 11 | "rules": { 12 | "exclude": [ 13 | "no-explicit-any" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { fromFileUrl } from "https://deno.land/std@0.161.0/path/mod.ts"; 2 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { ObjC } from "./src/objc.ts"; 2 | 3 | export default ObjC; 4 | export { default as sys } from "./src/bindings.ts"; 5 | export * from "./src/class.ts"; 6 | export * from "./src/encoding.ts"; 7 | export * from "./src/imp.ts"; 8 | export * from "./src/ivar.ts"; 9 | export * from "./src/method.ts"; 10 | export * from "./src/objc.ts"; 11 | export * from "./src/object.ts"; 12 | export * from "./src/protocol.ts"; 13 | export * from "./src/sel.ts"; 14 | export * from "./src/block.ts"; 15 | export * from "./src/autoreleasePool.ts"; 16 | -------------------------------------------------------------------------------- /src/autoreleasePool.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | 3 | export function autoreleasePoolPush() { 4 | return sys.objc_autoreleasePoolPush(); 5 | } 6 | 7 | export function autoreleasePoolPop(pool: Deno.PointerValue) { 8 | sys.objc_autoreleasePoolPop(pool); 9 | } 10 | 11 | /** 12 | * Wrap a callback in an autorelease pool. 13 | */ 14 | export function autoreleasepool(callback: () => void) { 15 | const pool = autoreleasePoolPush(); 16 | callback(); 17 | autoreleasePoolPop(pool); 18 | } 19 | -------------------------------------------------------------------------------- /src/bindings.ts: -------------------------------------------------------------------------------- 1 | // https://developer.apple.com/documentation/objectivec/objective-c_runtime 2 | 3 | const SYMBOLS = { 4 | // Working with Classes 5 | 6 | class_getName: { 7 | parameters: ["pointer"], 8 | result: "pointer", 9 | }, 10 | 11 | class_getSuperclass: { 12 | parameters: ["pointer"], 13 | result: "pointer", 14 | }, 15 | 16 | class_isMetaClass: { 17 | parameters: ["pointer"], 18 | result: "u8", 19 | }, 20 | 21 | class_getInstanceSize: { 22 | parameters: ["pointer"], 23 | result: "usize", 24 | }, 25 | 26 | class_getInstanceVariable: { 27 | parameters: ["pointer", "buffer"], 28 | result: "pointer", // ig..? yes 29 | }, 30 | 31 | class_getClassVariable: { 32 | parameters: ["pointer", "buffer"], 33 | result: "pointer", 34 | }, 35 | 36 | class_addIvar: { 37 | parameters: ["pointer", "buffer", "isize", "u8", "buffer"], 38 | result: "u8", 39 | }, 40 | 41 | class_copyIvarList: { 42 | parameters: ["pointer", "buffer"], 43 | result: "pointer", 44 | }, 45 | 46 | class_getIvarLayout: { 47 | parameters: ["pointer"], 48 | result: "pointer", 49 | }, 50 | 51 | class_setIvarLayout: { 52 | parameters: ["pointer", "pointer"], 53 | result: "void", 54 | }, 55 | 56 | class_getWeakIvarLayout: { 57 | parameters: ["pointer"], 58 | result: "pointer", 59 | }, 60 | 61 | class_setWeakIvarLayout: { 62 | parameters: ["pointer", "pointer"], 63 | result: "void", 64 | }, 65 | 66 | class_getProperty: { 67 | parameters: ["pointer", "buffer"], 68 | result: "pointer", 69 | }, 70 | 71 | class_copyPropertyList: { 72 | parameters: ["pointer", "buffer"], 73 | result: "pointer", 74 | }, 75 | 76 | class_addMethod: { 77 | parameters: ["pointer", "pointer", "pointer", "buffer"], 78 | result: "u8", 79 | }, 80 | 81 | class_getInstanceMethod: { 82 | parameters: ["pointer", "pointer"], 83 | result: "pointer", 84 | }, 85 | 86 | class_getClassMethod: { 87 | parameters: ["pointer", "pointer"], 88 | result: "pointer", 89 | }, 90 | 91 | class_copyMethodList: { 92 | parameters: ["pointer", "buffer"], 93 | result: "pointer", 94 | }, 95 | 96 | class_replaceMethod: { 97 | parameters: ["pointer", "pointer", "pointer", "buffer"], 98 | result: "u8", 99 | }, 100 | 101 | class_getMethodImplementation: { 102 | parameters: ["pointer", "pointer"], 103 | result: "pointer", 104 | }, 105 | 106 | // TODO: investigate symbol not found later 107 | // class_getMethodImplementation_stret: { 108 | // parameters: ["pointer", "pointer"], 109 | // result: "pointer", 110 | // }, 111 | 112 | class_respondsToSelector: { 113 | parameters: ["pointer", "pointer"], 114 | result: "u8", 115 | }, 116 | 117 | class_addProtocol: { 118 | parameters: ["pointer", "pointer"], 119 | result: "u8", 120 | }, 121 | 122 | class_addProperty: { 123 | parameters: ["pointer", "buffer", "buffer", "u32"], 124 | result: "u8", 125 | }, 126 | 127 | class_replaceProperty: { 128 | parameters: ["pointer", "pointer", "pointer", "u32"], 129 | result: "void", 130 | }, 131 | 132 | class_conformsToProtocol: { 133 | parameters: ["pointer", "pointer"], 134 | result: "u8", 135 | }, 136 | 137 | class_copyProtocolList: { 138 | parameters: ["pointer", "pointer"], 139 | result: "pointer", 140 | }, 141 | 142 | class_getVersion: { 143 | parameters: ["pointer"], 144 | result: "i32", 145 | }, 146 | 147 | class_setVersion: { 148 | parameters: ["pointer", "i32"], 149 | result: "void", 150 | }, 151 | 152 | // Adding Classes 153 | 154 | objc_allocateClassPair: { 155 | parameters: ["pointer", "buffer", "isize"], 156 | result: "pointer", 157 | }, 158 | 159 | objc_disposeClassPair: { 160 | parameters: ["pointer"], 161 | result: "void", 162 | }, 163 | 164 | objc_registerClassPair: { 165 | parameters: ["pointer"], 166 | result: "void", 167 | }, 168 | 169 | /*objc_duplicateClass: { 170 | parameters: ["pointer", "pointer", "isize"], 171 | result: "pointer", 172 | },*/ 173 | 174 | // Working with Instances 175 | 176 | object_getIndexedIvars: { 177 | parameters: ["pointer"], 178 | result: "pointer", 179 | }, 180 | 181 | object_getIvar: { 182 | parameters: ["pointer", "pointer"], 183 | result: "pointer", 184 | }, 185 | 186 | object_setIvar: { 187 | parameters: ["pointer", "pointer", "pointer"], 188 | result: "void", 189 | }, 190 | 191 | object_getClassName: { 192 | parameters: ["pointer"], 193 | result: "pointer", 194 | }, 195 | 196 | object_getClass: { 197 | parameters: ["pointer"], 198 | result: "pointer", 199 | }, 200 | 201 | object_setClass: { 202 | parameters: ["pointer", "pointer"], 203 | result: "void", 204 | }, 205 | 206 | // Obtaining Class Definitions 207 | 208 | objc_getClassList: { 209 | parameters: ["buffer", "isize"], 210 | result: "i32", 211 | }, 212 | 213 | objc_copyClassList: { 214 | parameters: ["buffer"], 215 | result: "pointer", 216 | }, 217 | 218 | objc_lookUpClass: { 219 | parameters: ["buffer"], 220 | result: "pointer", 221 | }, 222 | 223 | objc_getClass: { 224 | parameters: ["buffer"], 225 | result: "pointer", 226 | }, 227 | 228 | objc_getRequiredClass: { 229 | parameters: ["pointer"], 230 | result: "pointer", 231 | }, 232 | 233 | objc_getMetaClass: { 234 | parameters: ["pointer"], 235 | result: "pointer", 236 | }, 237 | 238 | // Working with Instance Variables 239 | 240 | ivar_getName: { 241 | parameters: ["pointer"], 242 | result: "pointer", 243 | }, 244 | 245 | ivar_getTypeEncoding: { 246 | parameters: ["pointer"], 247 | result: "pointer", 248 | }, 249 | 250 | ivar_getOffset: { 251 | parameters: ["pointer"], 252 | result: "isize", 253 | }, 254 | 255 | // Associative References 256 | 257 | objc_setAssociatedObject: { 258 | parameters: ["pointer", "pointer", "pointer", "u32"], 259 | result: "void", 260 | }, 261 | 262 | objc_getAssociatedObject: { 263 | parameters: ["pointer", "pointer"], 264 | result: "pointer", 265 | }, 266 | 267 | objc_removeAssociatedObjects: { 268 | parameters: ["pointer"], 269 | result: "void", 270 | }, 271 | 272 | // Working with Methods 273 | 274 | method_getName: { 275 | parameters: ["pointer"], 276 | result: "pointer", 277 | }, 278 | 279 | method_getImplementation: { 280 | parameters: ["pointer"], 281 | result: "pointer", 282 | }, 283 | 284 | method_getTypeEncoding: { 285 | parameters: ["pointer"], 286 | result: "pointer", 287 | }, 288 | 289 | method_copyReturnType: { 290 | parameters: ["pointer"], 291 | result: "pointer", 292 | }, 293 | 294 | method_copyArgumentType: { 295 | parameters: ["pointer", "u32"], 296 | result: "pointer", 297 | }, 298 | 299 | method_getReturnType: { 300 | parameters: ["pointer", "pointer", "isize"], 301 | result: "void", 302 | }, 303 | 304 | method_getNumberOfArguments: { 305 | parameters: ["pointer"], 306 | result: "u32", 307 | }, 308 | 309 | method_getArgumentType: { 310 | parameters: ["pointer", "u32", "pointer", "isize"], 311 | result: "void", 312 | }, 313 | 314 | /*method_getDescription: { 315 | parameters: ["pointer"], 316 | result: "pointer", 317 | },*/ 318 | 319 | method_setImplementation: { 320 | parameters: ["pointer", "pointer"], 321 | result: "void", 322 | }, 323 | 324 | method_exchangeImplementations: { 325 | parameters: ["pointer", "pointer"], 326 | result: "void", 327 | }, 328 | 329 | // Working with Selectors 330 | 331 | sel_getName: { 332 | parameters: ["pointer"], 333 | result: "pointer", 334 | }, 335 | 336 | sel_registerName: { 337 | parameters: ["buffer"], 338 | result: "pointer", 339 | }, 340 | 341 | sel_getUid: { 342 | parameters: ["buffer"], 343 | result: "pointer", 344 | }, 345 | 346 | sel_isEqual: { 347 | parameters: ["pointer", "pointer"], 348 | result: "u8", 349 | }, 350 | 351 | // Working with Protocols 352 | 353 | objc_getProtocol: { 354 | parameters: ["buffer"], 355 | result: "pointer", 356 | }, 357 | 358 | objc_copyProtocolList: { 359 | parameters: ["buffer"], 360 | result: "pointer", 361 | }, 362 | 363 | objc_allocateProtocol: { 364 | parameters: ["pointer"], 365 | result: "pointer", 366 | }, 367 | 368 | objc_registerProtocol: { 369 | parameters: ["pointer"], 370 | result: "void", 371 | }, 372 | 373 | protocol_addMethodDescription: { 374 | parameters: ["pointer", "pointer", "pointer", "u8", "u8"], 375 | result: "void", 376 | }, 377 | 378 | protocol_addProtocol: { 379 | parameters: ["pointer", "pointer"], 380 | result: "void", 381 | }, 382 | 383 | protocol_addProperty: { 384 | parameters: ["pointer", "pointer", "pointer", "u32", "u8", "u8"], 385 | result: "void", 386 | }, 387 | 388 | protocol_getName: { 389 | parameters: ["pointer"], 390 | result: "pointer", 391 | }, 392 | 393 | protocol_isEqual: { 394 | parameters: ["pointer", "pointer"], 395 | result: "u8", 396 | }, 397 | 398 | protocol_copyMethodDescriptionList: { 399 | parameters: ["pointer", "u8", "u8", "pointer"], 400 | result: "pointer", 401 | }, 402 | 403 | protocol_getMethodDescription: { 404 | parameters: ["pointer", "pointer", "u8", "u8"], 405 | result: "pointer", 406 | }, 407 | 408 | protocol_copyPropertyList: { 409 | parameters: ["pointer", "pointer"], 410 | result: "pointer", 411 | }, 412 | 413 | protocol_getProperty: { 414 | parameters: ["pointer", "pointer"], 415 | result: "pointer", 416 | }, 417 | 418 | protocol_copyProtocolList: { 419 | parameters: ["pointer", "pointer"], 420 | result: "pointer", 421 | }, 422 | 423 | protocol_conformsToProtocol: { 424 | parameters: ["pointer", "pointer"], 425 | result: "u8", 426 | }, 427 | 428 | // Working with Properties 429 | 430 | property_getName: { 431 | parameters: ["pointer"], 432 | result: "pointer", 433 | }, 434 | 435 | property_getAttributes: { 436 | parameters: ["pointer"], 437 | result: "pointer", 438 | }, 439 | 440 | property_copyAttributeValue: { 441 | parameters: ["pointer", "buffer"], 442 | result: "pointer", 443 | }, 444 | 445 | property_copyAttributeList: { 446 | parameters: ["pointer", "buffer"], 447 | result: "pointer", 448 | }, 449 | 450 | // Using Objective-C Language Features 451 | 452 | imp_implementationWithBlock: { 453 | parameters: ["pointer"], 454 | result: "pointer", 455 | }, 456 | 457 | imp_getBlock: { 458 | parameters: ["pointer"], 459 | result: "pointer", 460 | }, 461 | 462 | imp_removeBlock: { 463 | parameters: ["pointer"], 464 | result: "void", 465 | }, 466 | 467 | objc_loadWeak: { 468 | parameters: ["pointer"], 469 | result: "pointer", 470 | }, 471 | 472 | objc_storeWeak: { 473 | parameters: ["pointer", "pointer"], 474 | result: "pointer", 475 | }, 476 | 477 | objc_msgSend: { 478 | type: "pointer", 479 | }, 480 | 481 | // Autorelease Pool 482 | 483 | objc_autoreleasePoolPush: { 484 | parameters: [], 485 | result: "pointer", 486 | }, 487 | 488 | objc_autoreleasePoolPop: { 489 | parameters: ["pointer"], 490 | result: "void", 491 | }, 492 | } as const; 493 | 494 | /** Low-level Objective-C runtime bindings */ 495 | let sys!: Deno.DynamicLibrary["symbols"]; 496 | 497 | // Ignore permission error in case --allow-env is not passed. 498 | let env: string | undefined = undefined; 499 | try { 500 | env = Deno.env.get("DENO_OBJC_PATH"); 501 | } catch (_) { 502 | // noop 503 | } 504 | 505 | try { 506 | sys = Deno.dlopen( 507 | env ?? 508 | (Deno.build.os === "windows" 509 | ? "objc.dll" 510 | : Deno.build.os === "linux" 511 | ? "libobjc.so" 512 | : "libobjc.dylib"), 513 | SYMBOLS, 514 | ).symbols; 515 | 516 | // Load Foundation by default. 517 | // This will give us access to classes like NSString, NSBundle, etc. 518 | if (Deno.build.os === "darwin") { 519 | Deno.dlopen( 520 | "/System/Library/Frameworks/Foundation.framework/Foundation", 521 | {}, 522 | ); 523 | } 524 | } catch (e) { 525 | // Lazy errors, only when you use the library. 526 | sys = new Proxy({}, { 527 | get: () => { 528 | throw e; 529 | }, 530 | }) as any; 531 | } 532 | 533 | export default sys; 534 | -------------------------------------------------------------------------------- /src/block.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CTypeEncodable, 3 | fromNative, 4 | toNative, 5 | toNativeType, 6 | } from "./encoding.ts"; 7 | import { _handle } from "./util.ts"; 8 | import { CObject } from "./object.ts"; 9 | import common from "./common.ts"; 10 | import { Class } from "./class.ts"; 11 | 12 | const LE = 13 | (new Uint32Array((new Uint8Array([1, 2, 3, 4])).buffer))[0] === 0x04030201; 14 | 15 | const { 16 | symbols: { 17 | _NSConcreteStackBlock, 18 | _Block_copy, 19 | _Block_release, 20 | }, 21 | } = Deno.build.os === "darwin" 22 | ? Deno.dlopen( 23 | "libSystem.dylib", 24 | { 25 | _NSConcreteStackBlock: { 26 | type: "pointer", 27 | }, 28 | 29 | _Block_copy: { 30 | parameters: ["pointer"], 31 | result: "pointer", 32 | }, 33 | 34 | _Block_release: { 35 | parameters: ["pointer"], 36 | result: "void", 37 | }, 38 | } as any, 39 | ) 40 | : null as any; 41 | 42 | const copyHelper = new Deno.UnsafeCallback({ 43 | parameters: ["pointer", "pointer"], 44 | result: "void", 45 | }, () => { 46 | // noop 47 | }); 48 | 49 | const disposeHelper = new Deno.UnsafeCallback({ 50 | parameters: ["pointer"], 51 | result: "void", 52 | }, () => { 53 | // noop 54 | }); 55 | 56 | export interface BlockOptions { 57 | parameters: CTypeEncodable[]; 58 | result: CTypeEncodable; 59 | fn: (...args: any[]) => any; 60 | } 61 | 62 | export class Block { 63 | cb: Deno.UnsafeCallback; 64 | inner: Uint8Array; 65 | innerDesc: Uint8Array; 66 | [_handle]: Deno.PointerValue; 67 | 68 | constructor(options: BlockOptions) { 69 | this.inner = new Uint8Array(8 + 4 + 4 + 8 + 8); 70 | this.innerDesc = new Uint8Array(8 + 8 + 8 + 8); 71 | this[_handle] = Deno.UnsafePointer.of(this.inner); 72 | 73 | const blockDescView = new DataView(this.innerDesc.buffer); 74 | const blockView = new DataView(this.inner.buffer); 75 | 76 | this.cb = new Deno.UnsafeCallback({ 77 | parameters: options.parameters.map((p) => 78 | toNativeType(p) 79 | ) as Deno.NativeType[], 80 | result: toNativeType(options.result), 81 | } as any, (...args: any[]): any => { 82 | const result = options.fn( 83 | ...(args.map((e, i) => { 84 | const v = fromNative(options.parameters[i], e); 85 | if ( 86 | v !== null && typeof v === "object" && 87 | (v instanceof CObject || v instanceof Class) 88 | ) { 89 | return common.createProxy(v); 90 | } else return v; 91 | })), 92 | ); 93 | return toNative(options.result, result); 94 | }); 95 | 96 | // 0x00: class/isa 97 | blockView.setBigUint64( 98 | 0, 99 | BigInt(Deno.UnsafePointer.value(_NSConcreteStackBlock)), 100 | LE, 101 | ); 102 | // 0x08: flags 103 | blockView.setInt32(8, 1 << 25, LE); 104 | // 0x0c: reserved 105 | blockView.setInt32(8 + 4, 0, LE); 106 | // 0x10: invoke 107 | blockView.setBigUint64( 108 | 8 + 4 + 4, 109 | BigInt(Deno.UnsafePointer.value(this.cb.pointer)), 110 | LE, 111 | ); 112 | // 0x18: desc 113 | blockView.setBigUint64( 114 | 8 + 4 + 4 + 8, 115 | BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(this.innerDesc))), 116 | LE, 117 | ); 118 | 119 | // 0x00: reserved 120 | blockDescView.setBigUint64(0, 0n, LE); 121 | // 0x08: size 122 | blockDescView.setBigUint64(8, BigInt(this.inner.byteLength), LE); 123 | // 0x10: copy 124 | blockDescView.setBigUint64( 125 | 8 + 8, 126 | BigInt(Deno.UnsafePointer.value(copyHelper.pointer)), 127 | LE, 128 | ); 129 | // 0x18: dispose 130 | blockDescView.setBigUint64( 131 | 8 + 8 + 8, 132 | BigInt(Deno.UnsafePointer.value(disposeHelper.pointer)), 133 | LE, 134 | ); 135 | } 136 | 137 | copy() { 138 | return _Block_copy(this[_handle]); 139 | } 140 | 141 | release() { 142 | _Block_release(this[_handle]); 143 | this.inner = new Uint8Array(0); 144 | this.innerDesc = new Uint8Array(0); 145 | this[_handle] = null; 146 | this.cb.close(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/class.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { 3 | CTypeEncodable, 4 | CTypeInfo, 5 | encodeCType, 6 | fromNative, 7 | getCTypeSize, 8 | toNative, 9 | toNativeType, 10 | } from "./encoding.ts"; 11 | import { Ivar } from "./ivar.ts"; 12 | import { Method } from "./method.ts"; 13 | import { CObject } from "./object.ts"; 14 | import { Property } from "./property.ts"; 15 | import type { Protocol } from "./protocol.ts"; 16 | import { Sel } from "./sel.ts"; 17 | import { _handle, toCString } from "./util.ts"; 18 | import common from "./common.ts"; 19 | 20 | const CSTR_REFS = new Set(); 21 | 22 | export interface ClassIvarOptions { 23 | name: string; 24 | type: CTypeEncodable; 25 | } 26 | 27 | export interface ClassMethodThis { 28 | class: Class; 29 | self: any; 30 | view: Deno.UnsafePointerView; 31 | object: CObject; 32 | pointer: Deno.PointerValue; 33 | selector: Sel; 34 | } 35 | 36 | export interface ClassMethodOptions< 37 | P extends CTypeEncodable[] = CTypeEncodable[], 38 | R extends CTypeEncodable = CTypeEncodable, 39 | > { 40 | name: string; 41 | parameters: P; 42 | result: R; 43 | // TODO: map CType generics to JS value equivalents 44 | fn: (this: ClassMethodThis, ...args: any[]) => any; 45 | } 46 | 47 | // export interface ClassPropertyOptions { 48 | // name: string; 49 | // type: CTypeEncodable; 50 | // getter?: string; 51 | // setter?: string; 52 | // } 53 | 54 | export interface ClassCreateOptions { 55 | name: string; 56 | superclass?: Class; 57 | protocols?: Protocol[]; 58 | ivars?: ClassIvarOptions[]; 59 | methods?: ClassMethodOptions[]; 60 | properties?: string[]; 61 | } 62 | 63 | /** 64 | * Represents an Objective-C class. 65 | */ 66 | export class Class { 67 | [_handle]: Deno.PointerValue; 68 | 69 | get handle() { 70 | return this[_handle]; 71 | } 72 | 73 | get proxy() { 74 | return common.createProxy(this); 75 | } 76 | 77 | constructor(handle: Deno.PointerValue) { 78 | this[_handle] = handle; 79 | } 80 | 81 | static alloc(name: string, superclass?: Class, extraBytes?: number) { 82 | const nameCstr = toCString(name); 83 | return new Class( 84 | sys.objc_allocateClassPair( 85 | superclass?.[_handle] ?? null, 86 | nameCstr, 87 | extraBytes ?? 0, 88 | ), 89 | ); 90 | } 91 | 92 | get name() { 93 | const ptr = sys.class_getName(this[_handle]); 94 | return Deno.UnsafePointerView.getCString(ptr!); 95 | } 96 | 97 | get superclass(): Class | undefined { 98 | const superclass = sys.class_getSuperclass(this[_handle]); 99 | if (superclass === null) { 100 | return undefined; 101 | } else return new Class(superclass); 102 | } 103 | 104 | get metaclass() { 105 | return new Class(sys.object_getClass(this[_handle])); 106 | } 107 | 108 | get instanceSize() { 109 | return sys.class_getInstanceSize(this[_handle]); 110 | } 111 | 112 | // get imageName() { 113 | // const ptr = sys.class_getImageName(this[_handle]); 114 | // const ptrView = new Deno.UnsafePointerView(ptr); 115 | // return ptrView.getCString(); 116 | // } 117 | 118 | getInstanceMethod(sel: Sel) { 119 | const ptr = sys.class_getInstanceMethod(this[_handle], sel[_handle]); 120 | if (ptr === null) return undefined; 121 | else return new Method(ptr); 122 | } 123 | 124 | getClassMethod(sel: Sel) { 125 | const ptr = sys.class_getClassMethod(this[_handle], sel[_handle]); 126 | if (ptr === null) return undefined; 127 | else return new Method(ptr); 128 | } 129 | 130 | getInstanceVariable(name: string) { 131 | const nameCstr = toCString(name); 132 | const ptr = sys.class_getInstanceVariable(this[_handle], nameCstr); 133 | if (ptr === null) return undefined; 134 | else return new Ivar(ptr); 135 | } 136 | 137 | addInstanceVariable(name: string, type: string, size: number) { 138 | const nameCstr = toCString(name); 139 | const typeCstr = toCString(type); 140 | return Boolean( 141 | sys.class_addIvar(this[_handle], nameCstr, size, 0, typeCstr), 142 | ); 143 | } 144 | 145 | getClassVariable(name: string) { 146 | const nameCstr = toCString(name); 147 | const ptr = sys.class_getClassVariable(this[_handle], nameCstr); 148 | if (ptr === null) return undefined; 149 | else return new Ivar(ptr); 150 | } 151 | 152 | getProperty(name: string) { 153 | const nameCstr = toCString(name); 154 | const ptr = sys.class_getProperty(this[_handle], nameCstr); 155 | if (ptr === null) return undefined; 156 | else return new Property(ptr); 157 | } 158 | 159 | addProperty(name: string, attributes: [string, string][]) { 160 | const nameCstr = toCString(name); 161 | const attrs = new Array(attributes.length); 162 | for (let i = 0; i < attributes.length; i++) { 163 | const cstr = toCString(attributes[i][0]); 164 | CSTR_REFS.add(cstr); 165 | const cstr2 = toCString(attributes[i][1]); 166 | CSTR_REFS.add(cstr2); 167 | CSTR_REFS.add( 168 | attrs[i] = new BigUint64Array([ 169 | BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(cstr))), 170 | BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(cstr2))), 171 | ]) as any, 172 | ); 173 | } 174 | return Boolean( 175 | sys.class_addProperty( 176 | this[_handle], 177 | nameCstr, 178 | attrs.length === 0 ? null : new BigUint64Array( 179 | attrs.map((e) => 180 | BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(e))) 181 | ), 182 | ), 183 | attrs.length, 184 | ), 185 | ); 186 | } 187 | 188 | addMethod(sel: Sel, imp: Deno.PointerValue, types?: string) { 189 | const typesCstr = types ? toCString(types) : null; 190 | return Boolean( 191 | sys.class_addMethod(this[_handle], sel[_handle], imp, typesCstr), 192 | ); 193 | } 194 | 195 | replaceMethod(sel: Sel, imp: Deno.PointerValue, types?: string) { 196 | const typesCstr = types ? toCString(types) : null; 197 | return Boolean( 198 | sys.class_replaceMethod(this[_handle], sel[_handle], imp, typesCstr), 199 | ); 200 | } 201 | 202 | addProtocol(protocol: Protocol) { 203 | return Boolean(sys.class_addProtocol(this[_handle], protocol[_handle])); 204 | } 205 | 206 | get instanceMethods() { 207 | const outCount = new Uint32Array(1); 208 | const methods = new Deno.UnsafePointerView( 209 | sys.class_copyMethodList(this[_handle], outCount)!, 210 | ); 211 | const methodsArray = new Array(outCount[0]); 212 | for (let i = 0; i < outCount[0]; i++) { 213 | methodsArray[i] = new Method( 214 | methods.getPointer(i * 8), 215 | ); 216 | } 217 | return methodsArray; 218 | } 219 | 220 | get instanceVariables() { 221 | const outCount = new Uint32Array(1); 222 | const ivars = new Deno.UnsafePointerView( 223 | sys.class_copyIvarList(this[_handle], outCount)!, 224 | ); 225 | const ivarsArray = new Array(outCount[0]); 226 | for (let i = 0; i < outCount[0]; i++) { 227 | ivarsArray[i] = new Ivar( 228 | ivars.getPointer(i * 8), 229 | ); 230 | } 231 | return ivarsArray; 232 | } 233 | 234 | get properties() { 235 | const outCount = new Uint32Array(1); 236 | const props = new Deno.UnsafePointerView( 237 | sys.class_copyPropertyList(this[_handle], outCount)!, 238 | ); 239 | const propsArray = new Array(outCount[0]); 240 | for (let i = 0; i < outCount[0]; i++) { 241 | propsArray[i] = new Property( 242 | props.getPointer(i * 8), 243 | ); 244 | } 245 | return propsArray; 246 | } 247 | 248 | respondsTo(sel: Sel) { 249 | return Boolean(sys.class_respondsToSelector(this[_handle], sel[_handle])); 250 | } 251 | 252 | [Symbol.for("Deno.customInspect")]() { 253 | return `[class ${this.name}]`; 254 | } 255 | 256 | static create(options: ClassCreateOptions) { 257 | const ivars = options.ivars?.map((ivar) => ({ 258 | name: ivar.name, 259 | size: getCTypeSize(ivar.type), 260 | encoded: encodeCType(ivar.type), 261 | })) ?? []; 262 | const size = ivars.reduce((acc, ivar) => acc + ivar.size, 0); 263 | const cls = Class.alloc(options.name, options.superclass, size); 264 | options.protocols?.forEach((protocol) => cls.addProtocol(protocol)); 265 | ivars.forEach((ivar) => 266 | cls.addInstanceVariable(ivar.name, ivar.encoded, ivar.size) 267 | ); 268 | options.properties?.forEach((prop) => { 269 | cls.addProperty(prop, []); 270 | }); 271 | options.methods?.forEach((method) => { 272 | const params: CTypeInfo[] = method.parameters.map((e) => 273 | typeof e === "string" ? ({ type: e }) : e 274 | ); 275 | const result: CTypeInfo = typeof method.result === "string" 276 | ? ({ type: method.result }) 277 | : method.result; 278 | const cb = new Deno.UnsafeCallback( 279 | { 280 | parameters: [ 281 | "pointer", 282 | "pointer", 283 | ...params.map((e) => toNativeType(e)) as Deno.NativeType[], 284 | ], 285 | result: toNativeType(result), 286 | } as any, 287 | ( 288 | self: Deno.PointerValue, 289 | cmd: Deno.PointerValue, 290 | ...args: any[] 291 | ): any => { 292 | const obj = new CObject(self); 293 | const jsargs = args.map((e, i) => { 294 | const v = fromNative(params[i], e); 295 | if ( 296 | v !== null && typeof v === "object" && 297 | (v instanceof CObject || v instanceof Class) 298 | ) { 299 | return common.createProxy(v); 300 | } else return v; 301 | }); 302 | const res = method.fn.bind({ 303 | class: cls, 304 | pointer: self, 305 | object: obj, 306 | self: common.createProxy(obj), 307 | view: new Deno.UnsafePointerView(self!), 308 | selector: new Sel(cmd), 309 | })(...jsargs); 310 | return toNative(result, res); 311 | }, 312 | ); 313 | const enc = `${encodeCType(result)}@:${ 314 | params.map((e) => encodeCType(e)).join("") 315 | }`; 316 | cls.addMethod(new Sel(method.name), cb.pointer, enc); 317 | }); 318 | sys.objc_registerClassPair(cls.handle); 319 | return cls; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | export default {} as unknown as { 2 | createProxy: (e: any) => any; 3 | }; 4 | -------------------------------------------------------------------------------- /src/encoding.ts: -------------------------------------------------------------------------------- 1 | // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100 2 | 3 | import { Class } from "./class.ts"; 4 | import { CObject } from "./object.ts"; 5 | import { Sel } from "./sel.ts"; 6 | import { _handle, isArrayBufferView, toCString } from "./util.ts"; 7 | 8 | const CHAR = "c"; 9 | const INT = "i"; 10 | const SHORT = "s"; 11 | const LONG = "l"; 12 | const LONG_LONG = "q"; 13 | const UNSIGNED_CHAR = "C"; 14 | const UNSIGNED_INT = "I"; 15 | const UNSIGNED_SHORT = "S"; 16 | const UNSIGNED_LONG = "L"; 17 | const UNSIGNED_LONG_LONG = "Q"; 18 | const FLOAT = "f"; 19 | const DOUBLE = "d"; 20 | const BOOL = "B"; 21 | const VOID = "v"; 22 | const STRING = "*"; 23 | const ID = "@"; 24 | const CLASS = "#"; 25 | const SEL = ":"; 26 | const ARRAY_BEGIN = "["; 27 | const ARRAY_END = "]"; 28 | const NAME_BEGIN = "{"; 29 | const NAME_END = "}"; 30 | const BITFIELD_BEGIN = "b"; 31 | const POINTER_BEGIN = "^"; 32 | const UNKNOWN = "?"; 33 | 34 | export type CType = 35 | | "char" 36 | | "int" 37 | | "short" 38 | | "long" 39 | | "long long" 40 | | "unsigned char" 41 | | "unsigned int" 42 | | "unsigned short" 43 | | "unsigned long" 44 | | "unsigned long long" 45 | | "float" 46 | | "double" 47 | | "bool" 48 | | "void" 49 | | "string" 50 | | "id" 51 | | "class" 52 | | "sel" 53 | | "array" 54 | | "struct" 55 | | "bitfield" 56 | | "pointer" 57 | | "unknown"; 58 | 59 | export interface BaseCTypeInfo { 60 | type: CType; 61 | } 62 | 63 | export interface CArrayTypeInfo extends BaseCTypeInfo { 64 | type: "array"; 65 | length: number; 66 | elementType: CTypeInfo; 67 | } 68 | 69 | export interface CStructTypeInfo extends BaseCTypeInfo { 70 | type: "struct"; 71 | name: string; 72 | fields: CTypeInfo[]; 73 | } 74 | 75 | export interface CBitfieldTypeInfo extends BaseCTypeInfo { 76 | type: "bitfield"; 77 | size: number; 78 | } 79 | 80 | export interface CPointerTypeInfo extends BaseCTypeInfo { 81 | type: "pointer"; 82 | pointeeType: CTypeInfo; 83 | } 84 | 85 | export type SimpleCType = Exclude< 86 | CType, 87 | "array" | "struct" | "bitfield" | "pointer" 88 | >; 89 | 90 | export interface RestCTypeInfo extends BaseCTypeInfo { 91 | type: SimpleCType; 92 | } 93 | 94 | export type CTypeInfo = 95 | | RestCTypeInfo 96 | | CArrayTypeInfo 97 | | CStructTypeInfo 98 | | CBitfieldTypeInfo 99 | | CPointerTypeInfo; 100 | 101 | export type CTypeEncodable = SimpleCType | CTypeInfo; 102 | 103 | export function encodeCType(ty: CTypeEncodable): string { 104 | if (typeof ty === "string") { 105 | ty = { type: ty }; 106 | } 107 | 108 | switch (ty.type) { 109 | case "char": 110 | return CHAR; 111 | case "int": 112 | return INT; 113 | case "short": 114 | return SHORT; 115 | case "long": 116 | return LONG; 117 | case "long long": 118 | return LONG_LONG; 119 | case "unsigned char": 120 | return UNSIGNED_CHAR; 121 | case "unsigned int": 122 | return UNSIGNED_INT; 123 | case "unsigned short": 124 | return UNSIGNED_SHORT; 125 | case "unsigned long": 126 | return UNSIGNED_LONG; 127 | case "unsigned long long": 128 | return UNSIGNED_LONG_LONG; 129 | case "float": 130 | return FLOAT; 131 | case "double": 132 | return DOUBLE; 133 | case "bool": 134 | return BOOL; 135 | case "void": 136 | return VOID; 137 | case "string": 138 | return STRING; 139 | case "id": 140 | return ID; 141 | case "class": 142 | return CLASS; 143 | case "sel": 144 | return SEL; 145 | case "array": 146 | return ARRAY_BEGIN + ty.length + encodeCType(ty.elementType) + ARRAY_END; 147 | case "struct": 148 | return NAME_BEGIN + ty.name + "=" + ty.fields.map(encodeCType).join("") + 149 | NAME_END; 150 | case "bitfield": 151 | return BITFIELD_BEGIN + ty.size; 152 | case "pointer": 153 | return POINTER_BEGIN + encodeCType(ty.pointeeType); 154 | case "unknown": 155 | return UNKNOWN; 156 | 157 | default: 158 | throw new Error("Unknown CType: " + Deno.inspect(ty)); 159 | } 160 | } 161 | 162 | export function getCTypeSize(ty: CTypeEncodable): number { 163 | if (typeof ty === "string") { 164 | ty = { type: ty }; 165 | } 166 | 167 | switch (ty.type) { 168 | case "char": 169 | return 1; 170 | case "int": 171 | return 4; 172 | case "short": 173 | return 2; 174 | case "long": 175 | return 4; 176 | case "long long": 177 | return 8; 178 | case "unsigned char": 179 | return 1; 180 | case "unsigned int": 181 | return 4; 182 | case "unsigned short": 183 | return 2; 184 | case "unsigned long": 185 | return 4; 186 | case "unsigned long long": 187 | return 8; 188 | case "float": 189 | return 4; 190 | case "double": 191 | return 8; 192 | case "bool": 193 | return 1; 194 | case "void": 195 | return 0; 196 | case "string": 197 | return 8; 198 | case "id": 199 | return 8; 200 | case "class": 201 | return 8; 202 | case "sel": 203 | return 8; 204 | case "array": 205 | return ty.length * getCTypeSize(ty.elementType); 206 | case "struct": 207 | return ty.fields.reduce((acc, field) => acc + getCTypeSize(field), 0); 208 | case "bitfield": 209 | return ty.size; 210 | case "pointer": 211 | return 8; 212 | case "unknown": 213 | return 0; 214 | 215 | default: 216 | throw new Error("Unknown CType: " + Deno.inspect(ty)); 217 | } 218 | } 219 | 220 | class CTypeParser { 221 | source: string; 222 | index: number; 223 | 224 | constructor(source: string) { 225 | this.source = source; 226 | this.index = 0; 227 | } 228 | 229 | get current() { 230 | return this.source[this.index]; 231 | } 232 | 233 | get peek() { 234 | return this.source[this.index + 1]; 235 | } 236 | 237 | next() { 238 | const char = this.source[this.index++]; 239 | if (char === undefined) { 240 | throw new Error("Unexpected end of source"); 241 | } 242 | return char; 243 | } 244 | 245 | parse(): CTypeInfo { 246 | const char = this.next(); 247 | switch (char) { 248 | case CHAR: 249 | return { type: "char" }; 250 | 251 | case INT: 252 | return { type: "int" }; 253 | 254 | case SHORT: 255 | return { type: "short" }; 256 | 257 | case LONG: 258 | return { type: "long" }; 259 | 260 | case LONG_LONG: 261 | return { type: "long long" }; 262 | 263 | case UNSIGNED_CHAR: 264 | return { type: "unsigned char" }; 265 | 266 | case UNSIGNED_INT: 267 | return { type: "unsigned int" }; 268 | 269 | case UNSIGNED_SHORT: 270 | return { type: "unsigned short" }; 271 | 272 | case UNSIGNED_LONG: 273 | return { type: "unsigned long" }; 274 | 275 | case UNSIGNED_LONG_LONG: 276 | return { type: "unsigned long long" }; 277 | 278 | case FLOAT: 279 | return { type: "float" }; 280 | 281 | case DOUBLE: 282 | return { type: "double" }; 283 | 284 | case BOOL: 285 | return { type: "bool" }; 286 | 287 | case VOID: 288 | return { type: "void" }; 289 | 290 | case STRING: 291 | return { type: "string" }; 292 | 293 | case ID: 294 | return { type: "id" }; 295 | 296 | case CLASS: 297 | return { type: "class" }; 298 | 299 | case SEL: 300 | return { type: "sel" }; 301 | 302 | case ARRAY_BEGIN: { 303 | let length = 0; 304 | while (/\d/.test(this.current)) { 305 | length = 10 * length + Number(this.next()); 306 | } 307 | const elementType = this.parse(); 308 | if (this.next() !== ARRAY_END) { 309 | throw new Error("Expected ']'"); 310 | } 311 | 312 | return { 313 | type: "array", 314 | length, 315 | elementType, 316 | }; 317 | } 318 | 319 | case NAME_BEGIN: { 320 | let name = ""; 321 | while (this.current !== "=") { 322 | name += this.current; 323 | this.next(); 324 | } 325 | this.next(); 326 | const fields: CTypeInfo[] = []; 327 | while ((this.current as string) !== NAME_END) { 328 | fields.push(this.parse()); 329 | } 330 | this.next(); 331 | return { 332 | type: "struct", 333 | name, 334 | fields, 335 | }; 336 | } 337 | 338 | case BITFIELD_BEGIN: { 339 | const size = parseInt(this.next(), 10); 340 | return { 341 | type: "bitfield", 342 | size, 343 | }; 344 | } 345 | 346 | case POINTER_BEGIN: { 347 | const pointeeType = this.parse(); 348 | return { 349 | type: "pointer", 350 | pointeeType, 351 | }; 352 | } 353 | 354 | case UNKNOWN: 355 | return { type: "unknown" }; 356 | 357 | case "r": 358 | case "n": 359 | case "N": 360 | case "o": 361 | case "O": 362 | case "R": 363 | case "v": 364 | return this.parse(); 365 | 366 | default: 367 | throw new Error(`Unexpected character: '${char}' in '${this.source}'`); 368 | } 369 | } 370 | } 371 | 372 | export function parseCType(source: string) { 373 | const parser = new CTypeParser(source); 374 | return parser.parse(); 375 | } 376 | 377 | function encodableAsType(enc: CTypeEncodable): CTypeInfo { 378 | return typeof enc === "string" ? ({ type: enc }) : enc; 379 | } 380 | 381 | export function toNativeType(enc: CTypeEncodable): Deno.NativeResultType { 382 | enc = encodableAsType(enc); 383 | switch (enc.type) { 384 | case "char": 385 | return "u8"; 386 | case "int": 387 | return "i32"; 388 | case "short": 389 | return "i16"; 390 | case "long": 391 | return "i32"; 392 | case "long long": 393 | return "i64"; 394 | case "unsigned char": 395 | return "u8"; 396 | case "unsigned int": 397 | return "u32"; 398 | case "unsigned short": 399 | return "u16"; 400 | case "unsigned long": 401 | return "u32"; 402 | case "unsigned long long": 403 | return "u64"; 404 | case "float": 405 | return "f32"; 406 | case "double": 407 | return "f64"; 408 | case "bool": 409 | return "u8"; 410 | case "void": 411 | return "void"; 412 | case "string": 413 | return "buffer"; 414 | case "id": 415 | return "pointer"; 416 | case "class": 417 | return "pointer"; 418 | case "sel": 419 | return "pointer"; 420 | case "array": 421 | return { 422 | struct: new Array(enc.length).fill(enc.elementType).map(toNativeType), 423 | } as any; 424 | case "struct": 425 | return { struct: enc.fields.map(toNativeType) } as any; 426 | case "pointer": 427 | return "pointer"; 428 | case "bitfield": 429 | return "u64"; 430 | case "unknown": 431 | return "pointer"; 432 | } 433 | } 434 | 435 | function expectNumber(v: any) { 436 | if ( 437 | typeof v !== "number" && typeof v !== "bigint" && typeof v !== "boolean" 438 | ) { 439 | throw new Error("Expected number, got " + typeof v); 440 | } 441 | } 442 | 443 | export function toNative(enc: CTypeEncodable, v: any) { 444 | enc = encodableAsType(enc); 445 | switch (enc.type) { 446 | case "char": { 447 | if (typeof v === "string") { 448 | return v.charCodeAt(0); 449 | } else { 450 | expectNumber(v); 451 | return Number(v); 452 | } 453 | } 454 | 455 | case "int": // i32 456 | case "short": // i16 457 | case "long": // i32 458 | case "unsigned char": // u8 459 | case "unsigned int": // u32 460 | case "unsigned short": // u16 461 | case "unsigned long": // u32 462 | case "float": // f32 463 | case "double": // f64 464 | case "bool": // u8 465 | case "bitfield": // u64 466 | expectNumber(v); 467 | return Number(v); 468 | 469 | case "unsigned long long": 470 | case "long long": { 471 | return BigInt(v); 472 | } 473 | 474 | case "void": // void 475 | return undefined; 476 | 477 | case "string": // pointer 478 | return toCString(v); 479 | 480 | case "array": 481 | case "struct": { 482 | if (isArrayBufferView(v)) { 483 | return v as any; 484 | } else { 485 | throw new Error("Expected ArrayBufferView"); 486 | } 487 | } 488 | 489 | case "id": 490 | case "class": 491 | case "sel": 492 | case "pointer": 493 | case "unknown": { 494 | if ( 495 | v === null || typeof v === "bigint" || typeof v === "number" 496 | ) { 497 | return v; 498 | } else if (isArrayBufferView(v)) { 499 | return Deno.UnsafePointer.of(v as any); 500 | } else if (enc.type === "sel" && typeof v === "string") { 501 | return new Sel(v)[_handle]; 502 | } else if (typeof v === "object" && _handle in v) { 503 | return v[_handle]; 504 | } else { 505 | throw new Error( 506 | `Cannot map ${Deno.inspect(v)} to Native Value of encoding ${ 507 | Deno.inspect(enc) 508 | }`, 509 | ); 510 | } 511 | } 512 | } 513 | } 514 | 515 | export function fromNative(enc: CTypeEncodable, v: any) { 516 | enc = encodableAsType(enc); 517 | switch (enc.type) { 518 | case "char": // u8 519 | return v; 520 | 521 | case "int": // i32 522 | case "short": // i16 523 | case "long": // i32 524 | case "unsigned char": // u8 525 | case "unsigned int": // u32 526 | case "unsigned short": // u16 527 | case "unsigned long": // u32 528 | case "float": // f32 529 | case "double": // f64 530 | case "bitfield": // u64 531 | return Number(v); 532 | 533 | case "unsigned long long": 534 | return new BigUint64Array(v.buffer)[0]; 535 | 536 | case "long long": 537 | return new BigInt64Array(v.buffer)[0]; 538 | 539 | case "bool": // u8 540 | return v !== 0; 541 | 542 | case "void": // void 543 | return undefined; 544 | 545 | case "string": // pointer 546 | return new Deno.UnsafePointerView(v).getCString(); 547 | 548 | case "id": 549 | case "class": 550 | case "sel": 551 | case "pointer": 552 | case "unknown": 553 | case "array": 554 | case "struct": { 555 | if (v === null || v.value === 0) { 556 | return null; 557 | } else if (enc.type === "pointer" || enc.type === "unknown") { 558 | if (v === 0) return null; 559 | return v; 560 | } else if (enc.type === "id") { 561 | if (v === 0) return null; 562 | return new CObject(v); 563 | } else if (enc.type === "class") { 564 | if (v === 0) return null; 565 | return new Class(v); 566 | } else if (enc.type === "sel") { 567 | if (v === 0) return null; 568 | return new Sel(v); 569 | } else if (enc.type === "struct") { 570 | return v; 571 | } 572 | } 573 | } 574 | } 575 | -------------------------------------------------------------------------------- /src/imp.ts: -------------------------------------------------------------------------------- 1 | // import sys from "./bindings.ts"; 2 | import { _handle } from "./util.ts"; 3 | 4 | /** Objective-C class method Implementation */ 5 | export class Imp { 6 | [_handle]: Deno.PointerValue; 7 | 8 | constructor(handle: Deno.PointerValue) { 9 | this[_handle] = handle; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ivar.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { _handle } from "./util.ts"; 3 | 4 | /** Represents an instance variable on class */ 5 | export class Ivar { 6 | [_handle]: Deno.PointerValue; 7 | 8 | constructor(handle: Deno.PointerValue) { 9 | this[_handle] = handle; 10 | } 11 | 12 | get name() { 13 | const ptr = sys.ivar_getName(this[_handle]); 14 | return Deno.UnsafePointerView.getCString(ptr!); 15 | } 16 | 17 | get offset() { 18 | return sys.ivar_getOffset(this[_handle]); 19 | } 20 | 21 | get typeEncoding() { 22 | const ptr = sys.ivar_getTypeEncoding(this[_handle]); 23 | return Deno.UnsafePointerView.getCString(ptr!); 24 | } 25 | 26 | [Symbol.for("Deno.customInspect")]() { 27 | return `[ivar ${this.name} (0x${ 28 | this.offset.toString(16).padStart(2, "0") 29 | })]`; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/method.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { Imp } from "./imp.ts"; 3 | import { Sel } from "./sel.ts"; 4 | import { _handle } from "./util.ts"; 5 | 6 | /** Represents a class/instance method */ 7 | export class Method { 8 | [_handle]: Deno.PointerValue; 9 | 10 | constructor(handle: Deno.PointerValue) { 11 | this[_handle] = handle; 12 | } 13 | 14 | get name() { 15 | const ptr = sys.method_getName(this[_handle]); 16 | return new Sel(ptr); 17 | } 18 | 19 | get returnType() { 20 | const ptr = sys.method_copyReturnType(this[_handle]); 21 | const ptrView = new Deno.UnsafePointerView(ptr!); 22 | return ptrView.getCString(); 23 | } 24 | 25 | getArgumentType(index: number) { 26 | const ptr = sys.method_copyArgumentType(this[_handle], index); 27 | if (ptr === null) return ""; 28 | const ptrView = new Deno.UnsafePointerView(ptr); 29 | return ptrView.getCString(); 30 | } 31 | 32 | get argumentCount() { 33 | return sys.method_getNumberOfArguments(this[_handle]); 34 | } 35 | 36 | get implementation() { 37 | const ptr = sys.method_getImplementation(this[_handle]); 38 | return new Imp(ptr); 39 | } 40 | 41 | [Symbol.for("Deno.customInspect")]() { 42 | return `[method ${this.name.name}] (${ 43 | new Array(this.argumentCount).fill("").map((_, i) => 44 | this.getArgumentType(i) 45 | ).join(", ") 46 | }) -> ${this.returnType}`; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/objc.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { Class, ClassCreateOptions } from "./class.ts"; 3 | import { 4 | CTypeInfo, 5 | fromNative, 6 | parseCType, 7 | toNative, 8 | toNativeType, 9 | } from "./encoding.ts"; 10 | import { CObject } from "./object.ts"; 11 | import { Sel } from "./sel.ts"; 12 | import { _handle, _proxied, toCString } from "./util.ts"; 13 | import { fromFileUrl } from "../deps.ts"; 14 | import common from "./common.ts"; 15 | import { Protocol } from "./protocol.ts"; 16 | import { autoreleasepool } from "./autoreleasePool.ts"; 17 | 18 | function toJS(c: any) { 19 | if (c instanceof Class || c instanceof CObject) { 20 | return createProxy(c); 21 | } else { 22 | return c; 23 | } 24 | } 25 | 26 | /** Creates method proxy for an Objective-C class/instance method */ 27 | export function createMethodProxy(self: Class | CObject, name: string) { 28 | return new Proxy(() => {}, { 29 | apply(_target, _self, args) { 30 | if (name.includes("_") && args.length > 0) { 31 | name = name.replaceAll("_", ":"); 32 | } 33 | if (args.length > 0 && !name.endsWith(":")) { 34 | name += ":"; 35 | } 36 | 37 | return ObjC.msgSend(self, name, ...args); 38 | }, 39 | 40 | get(target: any, prop) { 41 | if (typeof prop === "symbol") { 42 | if (prop.description === "Deno.customInspect") { 43 | return () => { 44 | const objclass = self instanceof Class ? self : self.class; 45 | let sel = new Sel(name.replaceAll("_", ":")); 46 | let method = self instanceof Class 47 | ? objclass.getClassMethod(sel) 48 | : objclass.getInstanceMethod(sel); 49 | if (!method) { 50 | sel = new Sel(name.replaceAll("_", ":") + ":"); 51 | method = self instanceof Class 52 | ? objclass.getClassMethod(sel) 53 | : objclass.getInstanceMethod(sel); 54 | } 55 | if (!method) return `[method nil]`; 56 | const parts = sel.name.split(":"); 57 | let args = ""; 58 | for (let i = 0; i < parts.length; i++) { 59 | const part = parts[i].trim(); 60 | if (part === "") continue; 61 | args += `${part}:${method.getArgumentType(i + 2)} `; 62 | } 63 | return `${ 64 | self instanceof Class 65 | ? `+ ${method.returnType} [${self.name}` 66 | : `- ${method.returnType} [${self.className}` 67 | } ${args.trim()}]`; 68 | }; 69 | } else { 70 | return target[prop]; 71 | } 72 | } else { 73 | return target[prop]; 74 | } 75 | }, 76 | 77 | has(_, p) { 78 | if (typeof p === "symbol" && p.description === "Deno.customInspect") { 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | }, 84 | }); 85 | } 86 | 87 | /** Creates a class/object proxy that gives access to methods via JS properties */ 88 | export function createProxy(self: Class | CObject) { 89 | const objclass = self instanceof Class ? self : self.class; 90 | const proxy: any = new Proxy(self, { 91 | get(target, prop) { 92 | if (typeof prop === "symbol") { 93 | if (prop === _proxied) { 94 | return self; 95 | } else if (prop === _handle) { 96 | return self[_handle]; 97 | } else if (prop.description === "Deno.customInspect") { 98 | return () => { 99 | try { 100 | const desc = proxy.description; 101 | return desc.UTF8String(); 102 | } catch (_) { 103 | return self instanceof Class 104 | ? `[class ${self.name}]` 105 | : `[cobject ${self.className}]`; 106 | } 107 | }; 108 | } else return (target as any)[prop]; 109 | } else { 110 | if (self instanceof CObject) { 111 | const property = objclass.getProperty(prop); 112 | if (property) { 113 | const getter = property.getAttributeValue("G") || prop; 114 | return createMethodProxy(self, getter)(); 115 | } 116 | } 117 | return createMethodProxy(self, prop); 118 | } 119 | }, 120 | set(_target, prop, value) { 121 | if (typeof prop !== "string") return false; 122 | if (self instanceof CObject) { 123 | const property = objclass.getProperty(prop); 124 | if (property) { 125 | const setter = property.getAttributeValue("S") || 126 | `set${prop[0].toUpperCase()}${prop.slice(1)}:`; 127 | ObjC.msgSend( 128 | self, 129 | setter, 130 | value, // toNative(parseCType(property.getAttributeValue("T") ?? "?"), value), 131 | ); 132 | return true; 133 | } else { 134 | const setter = proxy[`set${prop[0].toUpperCase()}${prop.slice(1)}:`]; 135 | if ( 136 | setter && 137 | setter[Symbol.for("Deno.customInspect")]?.() !== "[method nil]" 138 | ) { 139 | setter(value); 140 | return true; 141 | } 142 | } 143 | } 144 | throw new Error(`Cannot set property ${prop}`); 145 | }, 146 | has(target, prop) { 147 | if (typeof prop === "symbol") { 148 | return prop in target; 149 | } else { 150 | if (self instanceof CObject) { 151 | return !!objclass.getProperty(prop); 152 | } 153 | return false; 154 | } 155 | }, 156 | }); 157 | return proxy; 158 | } 159 | 160 | common.createProxy = createProxy; 161 | 162 | /** 163 | * Objective-C runtime bindings 164 | * 165 | * Overview: 166 | * - Get a registered class by it's name using either `getClass` or 167 | * the `classes` proxy to destructure and get multiple classes at 168 | * same time. 169 | * ```ts 170 | * const { NSString, NSObject } = objc.classes; 171 | * ``` 172 | * 173 | * - Import a framework bundle using `import`. 174 | * ```ts 175 | * objc.import("AppKit"); 176 | * ``` 177 | * 178 | * - Send messages, preferably using class proxies, which we provide 179 | * by default. In fact, you should never have to deal with other 180 | * classes exported from this module such as `Class`, `CObject`, etc. 181 | * Most of the Objective-C runtime is abstracted away by proxies. 182 | * ```ts 183 | * const { NSString } = objc.classes; 184 | * const emptyString = NSString.string(); 185 | * // or chain methods like 186 | * const emptyString = NSString.alloc().init(); 187 | * 188 | * const hello = NSString.stringWithUTF8String("Hello, world!"); 189 | * // Convert to JS string 190 | * const jsHello = hello.UTF8String(); 191 | * ``` 192 | * Only major difference is that instead of `:` in method names, put 193 | * `_` (underscores). They get replaced at the time of calling. 194 | * Even that is optional, but better than doing 195 | * `NSString["stringWithUTF8String:"](...)`. 196 | */ 197 | export class ObjC { 198 | static readonly autoreleasepool = autoreleasepool; 199 | 200 | /** 201 | * A proxy that gives access to all loaded classes via JS properties. 202 | * 203 | * Usage: 204 | * ```ts 205 | * const { 206 | * NSString, 207 | * NSBundle, 208 | * } = objc.classes; 209 | * ``` 210 | */ 211 | static readonly classes: Record = new Proxy({}, { 212 | get: (_, name) => { 213 | if (typeof name === "symbol") return; 214 | const cls = ObjC.getClass(name); 215 | if (!cls) return; 216 | else return createProxy(cls); 217 | }, 218 | }); 219 | 220 | static readonly protocols: Record = new Proxy({}, { 221 | get: (_, name) => { 222 | if (typeof name === "symbol") return; 223 | const protocol = ObjC.getProtocol(name); 224 | if (!protocol) return; 225 | else return protocol; 226 | }, 227 | }); 228 | 229 | /** Returns the number of currently registered classes */ 230 | static get classCount() { 231 | return sys.objc_getClassList(null, 0); 232 | } 233 | 234 | /** Returns array of all currently registered classes */ 235 | static get classList() { 236 | const outCount = new Uint32Array(1); 237 | const classPtrs = new Deno.UnsafePointerView( 238 | sys.objc_copyClassList(outCount)!, 239 | ); 240 | const classes = new Array(outCount[0]); 241 | for (let i = 0; i < outCount[0]; i++) { 242 | const ptr = classPtrs.getPointer(i * 8); 243 | classes[i] = new Class(ptr); 244 | } 245 | return classes; 246 | } 247 | 248 | /** Returns array of all currently registered classes */ 249 | static get protocolList() { 250 | const outCount = new Uint32Array(1); 251 | const ptrs = new Deno.UnsafePointerView( 252 | sys.objc_copyProtocolList(outCount)!, 253 | ); 254 | const protocols = new Array(outCount[0]); 255 | for (let i = 0; i < outCount[0]; i++) { 256 | const ptr = ptrs.getPointer(i * 8); 257 | protocols[i] = new Protocol(ptr); 258 | } 259 | return protocols; 260 | } 261 | 262 | /** Get a registered class by its name */ 263 | static getClass(name: string): Class | undefined { 264 | const nameCstr = toCString(name); 265 | const classPtr = sys.objc_getClass(nameCstr); 266 | if (classPtr === null) return undefined; 267 | return new Class(classPtr); 268 | } 269 | 270 | /** Get a registered class by its name */ 271 | static getProtocol(name: string): Protocol | undefined { 272 | const nameCstr = toCString(name); 273 | const classPtr = sys.objc_getProtocol(nameCstr); 274 | if (classPtr === null) return undefined; 275 | return new Protocol(classPtr); 276 | } 277 | 278 | // static get imageNames() { 279 | // const outCount = new Uint32Array(1); 280 | // const imageNames = new Array(); 281 | // const imagePtrs = new Deno.UnsafePointerView( 282 | // sys.objc_copyImageNames(outCount), 283 | // ); 284 | // for (let i = 0; i < outCount[0]; i++) { 285 | // const ptr = new Deno.UnsafePointer(imagePtrs.getBigUint64(i * 8)); 286 | // imageNames.push(new Deno.UnsafePointerView(ptr).getCString()); 287 | // } 288 | // return imageNames; 289 | // } 290 | 291 | /** Send a message (call class/instance method) to class/instance. */ 292 | static msgSend( 293 | obj: any, 294 | selector: string | Deno.PointerValue | Sel, 295 | ...args: any[] 296 | ): T { 297 | const sel = new Sel(selector); 298 | if (obj[_proxied]) { 299 | obj = obj[_proxied]; 300 | } 301 | const objptr = typeof obj === "bigint" ? obj : obj[_handle]; 302 | const objclass = obj instanceof Class 303 | ? obj 304 | : new Class(sys.object_getClass(objptr)); 305 | 306 | const method = obj instanceof Class 307 | ? objclass.getClassMethod(sel) 308 | : objclass.getInstanceMethod(sel); 309 | if (!method) { 310 | throw new Error(`${objclass.name} does not respond to ${sel.name}`); 311 | } 312 | 313 | const argc = method.argumentCount; 314 | if ((args.length + 2) !== argc) { 315 | throw new Error( 316 | `${objclass.name} ${sel.name} expects ${ 317 | argc - 2 318 | } arguments, but got ${args.length}`, 319 | ); 320 | } 321 | 322 | const argDefs: CTypeInfo[] = []; 323 | const retDef = parseCType(method.returnType); 324 | 325 | for (let i = 0; i < argc; i++) { 326 | const arg = method.getArgumentType(i); 327 | argDefs.push(parseCType(arg)); 328 | } 329 | 330 | const argDefsNative = argDefs.map(toNativeType); 331 | const retDefNative = toNativeType(retDef); 332 | 333 | const fn = new Deno.UnsafeFnPointer( 334 | sys.objc_msgSend!, 335 | { 336 | parameters: argDefsNative as Deno.NativeType[], 337 | result: retDefNative, 338 | } as any, 339 | ); 340 | 341 | const cargs = [ 342 | typeof obj === "bigint" ? obj : obj[_handle], 343 | sel[_handle], 344 | ...args.map((e, i) => { 345 | const def = argDefs[i + 2]; 346 | if (typeof e === "string") { 347 | if (def.type === "id") { 348 | e = this.classes.NSString.stringWithUTF8String(e); 349 | } else if (def.type === "class") { 350 | e = this.classes[e]; 351 | } 352 | } 353 | if (typeof e === "object" && e !== null && e instanceof Array) { 354 | const arr = this.classes.NSMutableArray.array(); 355 | for (const ee of e) { 356 | arr.addObject(ee); 357 | } 358 | e = arr; 359 | } 360 | return toNative(def, e); 361 | }), 362 | ]; 363 | 364 | return toJS(fromNative(retDef, (fn.call as any)(...cargs))); 365 | } 366 | 367 | /** 368 | * Load a framework at runtime using `NSBundle` API. 369 | * 370 | * Example: 371 | * ```ts 372 | * objc.import("AppKit"); 373 | * // or 374 | * objc.import("/System/Library/Frameworks/AppKit.framework"); 375 | * ``` 376 | */ 377 | static import(path: string | URL) { 378 | if (path instanceof URL) { 379 | path = fromFileUrl(path); 380 | } else if (!path.startsWith("/")) { 381 | path = `/System/Library/Frameworks/${path}.framework`; 382 | } 383 | 384 | const { NSBundle } = this.classes; 385 | const bundle = NSBundle.bundleWithPath(path); 386 | 387 | if (!bundle) { 388 | throw new Error(`Could not load bundle at ${path}`); 389 | } 390 | if (!bundle.load()) { 391 | throw new Error(`Failed to load bundle at ${path}`); 392 | } 393 | } 394 | 395 | static [Symbol.for("Deno.customInspect")]() { 396 | return `ObjC { ${ObjC.classCount} classes }`; 397 | } 398 | 399 | /** 400 | * Template Tag for sending messages. 401 | * 402 | * Example: 403 | * ```ts 404 | * const { NSString } = objc.classes; 405 | * const str = objc.send`${NSString} stringWithUTF8String:${"Hello"}`; 406 | * ``` 407 | */ 408 | static send(template: TemplateStringsArray, ...args: any[]) { 409 | return ObjC.msgSend( 410 | args[0], 411 | template.map((e) => e.trim()).join(""), 412 | ...args.slice(1), 413 | ); 414 | } 415 | 416 | /** 417 | * Create an Objective C class implementing methods using JS 418 | * callbacks. 419 | */ 420 | static createClass(options: ClassCreateOptions) { 421 | return Class.create(options); 422 | } 423 | } 424 | 425 | export default ObjC; 426 | -------------------------------------------------------------------------------- /src/object.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { Class } from "./class.ts"; 3 | import { _handle } from "./util.ts"; 4 | 5 | export class CObject { 6 | [_handle]: Deno.PointerValue; 7 | 8 | constructor(handle: Deno.PointerValue) { 9 | this[_handle] = handle; 10 | } 11 | 12 | get className() { 13 | const ptr = sys.object_getClassName(this[_handle]); 14 | return Deno.UnsafePointerView.getCString(ptr!); 15 | } 16 | 17 | get class() { 18 | return new Class(sys.object_getClass(this[_handle])); 19 | } 20 | 21 | [Symbol.for("Deno.customInspect")]() { 22 | return `[cobject ${this.className}]`; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/property.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { _handle, toCString } from "./util.ts"; 3 | 4 | export interface PropertyAttribute { 5 | name: string; 6 | value: string; 7 | } 8 | 9 | export class Property { 10 | [_handle]: Deno.PointerValue; 11 | 12 | constructor(handle: Deno.PointerValue) { 13 | this[_handle] = handle; 14 | } 15 | 16 | get name() { 17 | const ptr = sys.property_getName(this[_handle]); 18 | return Deno.UnsafePointerView.getCString(ptr!); 19 | } 20 | 21 | get attributes() { 22 | const ptr = sys.property_getAttributes(this[_handle]); 23 | if (ptr === null) return undefined; 24 | return Deno.UnsafePointerView.getCString(ptr); 25 | } 26 | 27 | getAttributeList() { 28 | const outCount = new Uint32Array(1); 29 | const ptr = sys.property_copyAttributeList(this[_handle], outCount); 30 | if (ptr === null) return undefined; 31 | const ptrView = new Deno.UnsafePointerView(ptr); 32 | const attributes = new Array(outCount[0]); 33 | for (let i = 0; i < outCount[0]; i++) { 34 | const name = ptrView.getPointer(i * 16); 35 | const value = ptrView.getPointer(i * 16 + 8); 36 | attributes[i] = { 37 | name: Deno.UnsafePointerView.getCString(name!), 38 | value: Deno.UnsafePointerView.getCString(value!), 39 | }; 40 | } 41 | return attributes; 42 | } 43 | 44 | getAttributeValue(name: string) { 45 | const ptr = sys.property_copyAttributeValue(this[_handle], toCString(name)); 46 | if (ptr === null) return undefined; 47 | return Deno.UnsafePointerView.getCString(ptr); 48 | } 49 | 50 | [Symbol.for("Deno.customInspect")]() { 51 | return `[property ${this.name}]`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/protocol.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { _handle } from "./util.ts"; 3 | 4 | export class Protocol { 5 | [_handle]: Deno.PointerValue; 6 | 7 | constructor(handle: Deno.PointerValue) { 8 | this[_handle] = handle; 9 | } 10 | 11 | get name() { 12 | const ptr = sys.protocol_getName(this[_handle]); 13 | return Deno.UnsafePointerView.getCString(ptr!); 14 | } 15 | 16 | [Symbol.for("Deno.customInspect")]() { 17 | return `[protocol ${this.name}]`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sel.ts: -------------------------------------------------------------------------------- 1 | import sys from "./bindings.ts"; 2 | import { _handle, toCString } from "./util.ts"; 3 | 4 | export class Sel { 5 | [_handle]: Deno.PointerValue; 6 | 7 | constructor(handle: Deno.PointerValue | string | Sel) { 8 | this[_handle] = 9 | typeof handle === "object" && Object.getPrototypeOf(handle) === null 10 | ? handle as Deno.PointerValue 11 | : handle instanceof Sel 12 | ? handle[_handle] 13 | : Sel.register(handle as string)[_handle]; 14 | } 15 | 16 | static register(name: string) { 17 | const nameCstr = toCString(name); 18 | const handle = sys.sel_registerName(nameCstr); 19 | return new Sel(handle); 20 | } 21 | 22 | static getUid(name: string) { 23 | const nameCstr = toCString(name); 24 | return sys.sel_getUid(nameCstr); 25 | } 26 | 27 | get name() { 28 | const ptr = sys.sel_getName(this[_handle]); 29 | return Deno.UnsafePointerView.getCString(ptr!); 30 | } 31 | 32 | equals(other: Sel) { 33 | return sys.sel_isEqual(this[_handle], other[_handle]); 34 | } 35 | 36 | [Symbol.for("Deno.customInspect")]() { 37 | return `[sel ${this.name}]`; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export const _handle = Symbol("[[objc_handle]]"); 2 | export const _proxied = Symbol("[[objc_proxied]]"); 3 | 4 | export function toCString(str: string) { 5 | const buffer = new Uint8Array(str.length + 1); 6 | new TextEncoder().encodeInto(str, buffer); 7 | return buffer; 8 | } 9 | 10 | export function isArrayBufferView(obj: any): obj is ArrayBufferView { 11 | return obj instanceof Uint8Array || 12 | obj instanceof Uint16Array || 13 | obj instanceof Uint32Array || 14 | obj instanceof Uint8ClampedArray || 15 | obj instanceof Int8Array || 16 | obj instanceof Int16Array || 17 | obj instanceof Int32Array || 18 | obj instanceof Float32Array || 19 | obj instanceof Float64Array || 20 | obj instanceof BigUint64Array || 21 | obj instanceof BigInt64Array; 22 | } 23 | -------------------------------------------------------------------------------- /test/appkit.ts: -------------------------------------------------------------------------------- 1 | import objc, { Block } from "../mod.ts"; 2 | import { _handle } from "../src/util.ts"; 3 | 4 | objc.import("AppKit"); 5 | objc.import("UserNotifications"); 6 | 7 | function NSMakeRect(x: number, y: number, width: number, height: number) { 8 | return new Float64Array([x, y, width, height]); 9 | } 10 | 11 | function NSMakeSize(width: number, height: number) { 12 | return new Float64Array([width, height]); 13 | } 14 | 15 | const { 16 | NSObject, 17 | NSApplication, 18 | NSWindow, 19 | NSButton, 20 | NSTextField, 21 | NSStatusBar, 22 | NSPopover, 23 | NSViewController, 24 | NSView, 25 | UNUserNotificationCenter, 26 | UNMutableNotificationContent, 27 | UNNotificationRequest, 28 | UNTimeIntervalNotificationTrigger, 29 | } = objc.classes; 30 | 31 | const app = NSApplication.sharedApplication(); 32 | app.setActivationPolicy(0); 33 | 34 | const window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer( 35 | NSMakeRect(100, 100, 300, 300), 36 | 1 | 2 | 4, 37 | 2, 38 | 0, 39 | ); 40 | 41 | // try to make it floating window 42 | // window.opaque = false; 43 | // window.level = 3; 44 | 45 | let close = false; 46 | 47 | let btn1 = 0, btn2 = 0; 48 | 49 | const popoverView = NSView.alloc().initWithFrame(NSMakeRect(0, 0, 300, 300)); 50 | 51 | const contentViewController = objc.createClass({ 52 | name: "ContentViewController", 53 | superclass: NSViewController, 54 | protocols: [], 55 | methods: [ 56 | { 57 | name: "loadView", 58 | result: "void", 59 | parameters: [], 60 | fn() { 61 | this.self.view = popoverView; 62 | }, 63 | }, 64 | ], 65 | }); 66 | 67 | const popover = NSPopover.alloc().init(); 68 | popover.behavior = 1; 69 | popover.contentSize = NSMakeSize(200, 200); 70 | const cvc = contentViewController.proxy.alloc().initWithNibName_bundle_( 71 | null, 72 | null, 73 | ); 74 | popover.contentViewController = cvc; 75 | 76 | const bar = NSStatusBar.systemStatusBar().statusItemWithLength_(-1); 77 | bar.button.title = "Deno"; 78 | 79 | const WindowDelegate = objc.createClass({ 80 | name: "WindowDelegate", 81 | superclass: NSObject, 82 | protocols: [objc.protocols.NSWindowDelegate], 83 | methods: [ 84 | { 85 | name: "windowShouldClose:", 86 | parameters: ["id"], 87 | result: "bool", 88 | fn(_sender) { 89 | console.log("windowShouldClose"); 90 | return true; 91 | }, 92 | }, 93 | { 94 | name: "windowWillClose:", 95 | parameters: ["id"], 96 | result: "void", 97 | fn(_notif) { 98 | console.log("windowWillClose"); 99 | close = true; 100 | app.terminate(null); 101 | }, 102 | }, 103 | { 104 | name: "OnButton1Click:", 105 | parameters: ["id"], 106 | result: "void", 107 | fn(_sender) { 108 | btn1++; 109 | label1.setStringValue("Button1 clicked " + btn1 + " times"); 110 | }, 111 | }, 112 | { 113 | name: "OnButton2Click:", 114 | parameters: ["id"], 115 | result: "void", 116 | fn(_sender) { 117 | btn2++; 118 | label2.setStringValue("Button2 Clicked " + btn2 + " times"); 119 | }, 120 | }, 121 | { 122 | name: "applicationDidFinishLaunching:", 123 | parameters: ["id"], 124 | result: "void", 125 | fn(_notif) { 126 | console.log("Launched 🚀"); 127 | const notificationCenter = UNUserNotificationCenter.alloc() 128 | .initWithBundleIdentifier("xyz.helloyunho.appkit"); 129 | 130 | notificationCenter.getNotificationSettingsWithCompletionHandler( 131 | new Block({ 132 | parameters: ["id", "id"], 133 | result: "void", 134 | fn(_, settings) { 135 | console.log(settings); 136 | }, 137 | }), 138 | ); 139 | 140 | const block = new Block({ 141 | parameters: ["id", "bool", "id"], 142 | result: "void", 143 | fn(_, granted: boolean, error: any) { 144 | console.log("granted:", granted); 145 | console.log("error:", error); 146 | 147 | if (granted) { 148 | const content = UNMutableNotificationContent.alloc().init(); 149 | content.title = "Deno"; 150 | content.body = "Deno is awesome"; 151 | 152 | const trigger = UNTimeIntervalNotificationTrigger 153 | .triggerWithTimeInterval_repeats(5, false); 154 | 155 | const request = UNNotificationRequest 156 | .requestWithIdentifier_content_trigger( 157 | "xyz.helloyunho.appkit", 158 | content, 159 | trigger, 160 | ); 161 | 162 | notificationCenter.addNotificationRequest_withCompletionHandler( 163 | request, 164 | new Block({ 165 | parameters: ["id", "id"], 166 | result: "void", 167 | fn(_, error: any) { 168 | console.log( 169 | "req error:", 170 | objc.send`${error} description`.UTF8String(), 171 | ); 172 | }, 173 | }), 174 | ); 175 | } 176 | }, 177 | }); 178 | 179 | notificationCenter.requestAuthorizationWithOptions_completionHandler( 180 | (1 << 0) | (1 << 1) | (1 << 2), 181 | block, 182 | ); 183 | }, 184 | }, 185 | { 186 | name: "OnPopoverClick:", 187 | parameters: ["id"], 188 | result: "void", 189 | fn(_sender) { 190 | if (bar.button !== null) { 191 | if (popover.shown) { 192 | popover.performClose(_sender); 193 | } else { 194 | contentViewController.proxy.view.window?.becomeKey(); 195 | popover.showRelativeToRect_ofView_preferredEdge( 196 | bar.button.bounds, 197 | bar.button, 198 | 1, 199 | ); 200 | } 201 | } 202 | }, 203 | }, 204 | ], 205 | }); 206 | 207 | const delegate = WindowDelegate.proxy.alloc().init(); 208 | app.setDelegate(delegate); 209 | window.setDelegate(delegate); 210 | 211 | window.setTitle("Deno Obj-C"); 212 | 213 | window.makeKeyAndOrderFront(app); 214 | window.setReleasedWhenClosed(false); 215 | window.setAcceptsMouseMovedEvents(true); 216 | 217 | const button1 = NSButton.alloc().initWithFrame(NSMakeRect(50, 225, 90, 25)); 218 | button1.setTitle("Button1"); 219 | button1.setBezelStyle(1); 220 | button1.setAutoresizingMask(4 | 8); 221 | button1.setTarget(delegate); 222 | button1.setAction("OnButton1Click:"); 223 | 224 | const button2 = NSButton.alloc().initWithFrame(NSMakeRect(50, 125, 200, 75)); 225 | button2.setTitle("Button2"); 226 | button2.setBezelStyle(1); 227 | button2.setAutoresizingMask(4 | 8); 228 | button2.setTarget(delegate); 229 | button2.setAction("OnButton2Click:"); 230 | 231 | const label1 = NSTextField.alloc().initWithFrame(NSMakeRect(50, 80, 150, 20)); 232 | label1.setStringValue("Button1 clicked 0 times"); 233 | label1.setBezeled(false); 234 | label1.setDrawsBackground(false); 235 | label1.setEditable(false); 236 | 237 | const label2 = NSTextField.alloc().initWithFrame(NSMakeRect(50, 50, 150, 20)); 238 | label2.setStringValue("Button2 clicked 0 times"); 239 | label2.setBezeled(false); 240 | label2.setDrawsBackground(false); 241 | label2.setEditable(false); 242 | 243 | bar.button.setTarget(delegate); 244 | bar.button.setAction("OnPopoverClick:"); 245 | 246 | window.contentView.addSubview(button1); 247 | window.contentView.addSubview(button2); 248 | window.contentView.addSubview(label1); 249 | window.contentView.addSubview(label2); 250 | 251 | app.activateIgnoringOtherApps(true); 252 | app.finishLaunching(); 253 | 254 | app.run(); 255 | 256 | // function updateEvents() { 257 | // while (true) { 258 | // const event = app.nextEventMatchingMask_untilDate_inMode_dequeue( 259 | // 2n ** 64n - 1n, 260 | // NSDate.distantPast(), 261 | // "kCFRunLoopDefaultMode", 262 | // true, 263 | // ); 264 | // if (event) { 265 | // app.sendEvent(event); 266 | // } else { 267 | // break; 268 | // } 269 | // } 270 | // } 271 | 272 | // while (!close) { 273 | // updateEvents(); 274 | // } 275 | -------------------------------------------------------------------------------- /test/deps.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.130.0/testing/asserts.ts"; 2 | -------------------------------------------------------------------------------- /test/encoding.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "./deps.ts"; 2 | import { CTypeInfo, parseCType } from "../mod.ts"; 3 | 4 | function assert(source: string, type: CTypeInfo) { 5 | assertEquals(parseCType(source), type); 6 | } 7 | 8 | Deno.test("objc encoding parser", async (t) => { 9 | await t.step("char", () => { 10 | assert( 11 | "c", 12 | { type: "char" }, 13 | ); 14 | }); 15 | 16 | await t.step("int", () => { 17 | assert( 18 | "i", 19 | { type: "int" }, 20 | ); 21 | }); 22 | 23 | await t.step("short", () => { 24 | assert( 25 | "s", 26 | { type: "short" }, 27 | ); 28 | }); 29 | 30 | await t.step("long", () => { 31 | assert( 32 | "l", 33 | { type: "long" }, 34 | ); 35 | }); 36 | 37 | await t.step("long long", () => { 38 | assert( 39 | "q", 40 | { type: "long long" }, 41 | ); 42 | }); 43 | 44 | await t.step("unsigned char", () => { 45 | assert( 46 | "C", 47 | { type: "unsigned char" }, 48 | ); 49 | }); 50 | 51 | await t.step("unsigned int", () => { 52 | assert( 53 | "I", 54 | { type: "unsigned int" }, 55 | ); 56 | }); 57 | 58 | await t.step("unsigned short", () => { 59 | assert( 60 | "S", 61 | { type: "unsigned short" }, 62 | ); 63 | }); 64 | 65 | await t.step("unsigned long", () => { 66 | assert( 67 | "L", 68 | { type: "unsigned long" }, 69 | ); 70 | }); 71 | 72 | await t.step("unsigned long long", () => { 73 | assert( 74 | "Q", 75 | { type: "unsigned long long" }, 76 | ); 77 | }); 78 | 79 | await t.step("float", () => { 80 | assert( 81 | "f", 82 | { type: "float" }, 83 | ); 84 | }); 85 | 86 | await t.step("double", () => { 87 | assert( 88 | "d", 89 | { type: "double" }, 90 | ); 91 | }); 92 | 93 | await t.step("bool", () => { 94 | assert( 95 | "B", 96 | { type: "bool" }, 97 | ); 98 | }); 99 | 100 | await t.step("void", () => { 101 | assert( 102 | "v", 103 | { type: "void" }, 104 | ); 105 | }); 106 | 107 | await t.step("string", () => { 108 | assert( 109 | "*", 110 | { type: "string" }, 111 | ); 112 | }); 113 | 114 | await t.step("id", () => { 115 | assert( 116 | "@", 117 | { type: "id" }, 118 | ); 119 | }); 120 | 121 | await t.step("class", () => { 122 | assert( 123 | "#", 124 | { type: "class" }, 125 | ); 126 | }); 127 | 128 | await t.step("sel", () => { 129 | assert( 130 | ":", 131 | { type: "sel" }, 132 | ); 133 | }); 134 | 135 | await t.step("pointer", () => { 136 | assert( 137 | "^c", 138 | { type: "pointer", pointeeType: { type: "char" } }, 139 | ); 140 | }); 141 | 142 | await t.step("array", () => { 143 | assert( 144 | "[24c]", 145 | { type: "array", length: 24, elementType: { type: "char" } }, 146 | ); 147 | }); 148 | 149 | await t.step("struct", () => { 150 | assert( 151 | "{name=cisl}", 152 | { 153 | type: "struct", 154 | name: "name", 155 | fields: [ 156 | { type: "char" }, 157 | { type: "int" }, 158 | { type: "short" }, 159 | { type: "long" }, 160 | ], 161 | }, 162 | ); 163 | }); 164 | 165 | await t.step("unknown", () => { 166 | assert( 167 | "?", 168 | { type: "unknown" }, 169 | ); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/pasteboard.ts: -------------------------------------------------------------------------------- 1 | import objc from "../mod.ts"; 2 | import { assertEquals } from "./deps.ts"; 3 | 4 | objc.import("AppKit"); 5 | 6 | const { 7 | NSPasteboard, 8 | } = objc.classes; 9 | 10 | Deno.test("pasteboard", async (t) => { 11 | const pb = NSPasteboard.generalPasteboard(); 12 | 13 | await t.step("clear contents", () => { 14 | pb.clearContents(); 15 | }); 16 | 17 | await t.step("write string", () => { 18 | objc 19 | .send`${pb} setString:${"hello world"} forType:${"public.utf8-plain-text"}`; 20 | // or 21 | // pb.setString_forType("hello world", "public.utf8-plain-text"); 22 | }); 23 | 24 | await t.step("read string", () => { 25 | const str = pb.stringForType("public.utf8-plain-text").UTF8String(); 26 | assertEquals(str, "hello world"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import objc from "../mod.ts"; 2 | 3 | const { 4 | NSDate, 5 | NSDateFormatter, 6 | NSObject, 7 | } = objc.classes; 8 | 9 | objc.autoreleasepool(() => { 10 | const date = NSDate.date(); 11 | const dateFormatter = NSDateFormatter 12 | .localizedStringFromDate_dateStyle_timeStyle(date, 2, 2); 13 | console.log(dateFormatter.UTF8String()); 14 | 15 | const MyClass = objc.createClass({ 16 | name: "MyClass", 17 | superclass: NSObject, 18 | ivars: [ 19 | { 20 | name: "iv", 21 | type: "int", 22 | }, 23 | ], 24 | properties: ["iv"], 25 | methods: [ 26 | { 27 | name: "iv", 28 | parameters: [], 29 | result: "int", 30 | fn() { 31 | return 1; 32 | }, 33 | }, 34 | { 35 | name: "setIv:", 36 | parameters: ["int"], 37 | result: "void", 38 | fn(iv: number) { 39 | console.log("set iv", iv); 40 | }, 41 | }, 42 | { 43 | name: "test:", 44 | parameters: ["id"], 45 | result: "void", 46 | fn(obj: any) { 47 | console.log("test", obj.iv); 48 | }, 49 | }, 50 | ], 51 | }).proxy; 52 | 53 | const cls = MyClass.alloc().init(); 54 | console.log(cls.iv); 55 | cls.iv = 2; 56 | cls.test(cls); 57 | }); 58 | -------------------------------------------------------------------------------- /test_appkit/jsx.ts: -------------------------------------------------------------------------------- 1 | import objc from "./sys.ts"; 2 | import { NSApp, NSMakeRect } from "./util.ts"; 3 | 4 | const { 5 | NSView, 6 | NSButton, 7 | NSObject, 8 | NSWindow, 9 | NSTextField, 10 | } = objc.classes; 11 | 12 | export type Rect = [number, number, number, number]; 13 | 14 | export interface BaseProps { 15 | bind?: (value: any) => void; 16 | } 17 | 18 | export interface ViewProps extends BaseProps { 19 | frame: Rect; 20 | } 21 | 22 | export function View(props: ViewProps, components: any[]) { 23 | const view = NSView.alloc().initWithFrame(NSMakeRect(...props.frame)); 24 | for (const component of components) { 25 | view.addSubview(component); 26 | } 27 | props.bind?.(view); 28 | return view; 29 | } 30 | 31 | export interface ButtonProps extends BaseProps { 32 | frame: Rect; 33 | title: string; 34 | bezelStyle?: number; 35 | autoresizingMask?: number; 36 | target?: any; 37 | action?: string; 38 | onClick?: () => void; 39 | } 40 | 41 | let ctr = 0; 42 | 43 | export function Button(props: ButtonProps) { 44 | const button = NSButton.alloc().initWithFrame(NSMakeRect(...props.frame)); 45 | button.setTitle(props.title); 46 | button.setBezelStyle(props.bezelStyle || 1); 47 | button.setAutoresizingMask(props.autoresizingMask || (4 | 8)); 48 | if (props.target && props.action) { 49 | button.setTarget(props.target); 50 | button.setAction(props.action); 51 | } 52 | if (props.onClick) { 53 | const delegate = objc.createClass({ 54 | name: "_JSXButtonDelegate" + ctr++, 55 | superclass: NSObject, 56 | methods: [ 57 | { 58 | name: "action:", 59 | parameters: ["id"], 60 | result: "void", 61 | fn(_sender) { 62 | props.onClick?.(); 63 | }, 64 | }, 65 | ], 66 | }).proxy.alloc().init(); 67 | button.setTarget(delegate); 68 | button.setAction("action:"); 69 | } 70 | props.bind?.(button); 71 | return button; 72 | } 73 | 74 | export interface WindowProps extends BaseProps { 75 | title: string; 76 | contentRect: Rect; 77 | styleMask?: number; 78 | } 79 | 80 | let windowsOpen = 0; 81 | 82 | export function Window(props: WindowProps, components: any[]) { 83 | const window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer( 84 | NSMakeRect(...props.contentRect), 85 | props.styleMask || (1 | 2 | 4), 86 | 2, 87 | 0, 88 | ); 89 | window.setTitle(props.title); 90 | window.setReleasedWhenClosed(false); 91 | window.setAcceptsMouseMovedEvents(true); 92 | const WindowDelegate = objc.createClass({ 93 | name: "_JSXWindowDelegate" + ctr++, 94 | superclass: NSObject, 95 | methods: [ 96 | { 97 | name: "windowShouldClose:", 98 | parameters: ["id"], 99 | result: "bool", 100 | fn(_sender) { 101 | return true; 102 | }, 103 | }, 104 | { 105 | name: "windowWillClose:", 106 | parameters: ["id"], 107 | result: "void", 108 | fn(_notif) { 109 | windowsOpen--; 110 | if (windowsOpen === 0) { 111 | NSApp.terminate(window); 112 | } 113 | }, 114 | }, 115 | ], 116 | }).proxy.alloc().init(); 117 | window.setDelegate(WindowDelegate); 118 | for (const component of components) { 119 | window.contentView.addSubview(component); 120 | } 121 | windowsOpen++; 122 | props.bind?.(window); 123 | return window; 124 | } 125 | 126 | export interface TextFieldProps extends BaseProps { 127 | frame: Rect; 128 | value?: string; 129 | bezeled?: boolean; 130 | editable?: boolean; 131 | drawsBackground?: boolean; 132 | } 133 | 134 | export function TextField(props: TextFieldProps) { 135 | const tf = NSTextField.alloc().initWithFrame(NSMakeRect(...props.frame)); 136 | if (props.value !== undefined) tf.setStringValue(props.value); 137 | if (props.bezeled !== undefined) tf.setBezeled(props.bezeled); 138 | if (props.drawsBackground !== undefined) { 139 | tf.setDrawsBackground(props.drawsBackground); 140 | } 141 | if (props.editable !== undefined) tf.setEditable(props.editable); 142 | props.bind?.(tf); 143 | return tf; 144 | } 145 | 146 | export function useState(init: any) { 147 | return [ 148 | () => init, 149 | (value: any) => { 150 | init = value; 151 | }, 152 | ] as const; 153 | } 154 | 155 | export class AppKit { 156 | static createElement( 157 | element: string | CallableFunction, 158 | props: Record, 159 | ...children: any[] 160 | ) { 161 | if (typeof element === "string") { 162 | throw new Error(`No intrinsic element named "${element}"`); 163 | } else { 164 | return element(props, children); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /test_appkit/main.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx AppKit.createElement */ 2 | 3 | import { 4 | AppKit, 5 | Button, 6 | mainloop, 7 | NSApp, 8 | TextField, 9 | useState, 10 | Window, 11 | } from "./mod.ts"; 12 | 13 | function App() { 14 | const [label1, setLabel1] = useState(null); 15 | const [label2, setLabel2] = useState(null); 16 | 17 | let btn1 = 0, 18 | btn2 = 0; 19 | 20 | return ( 21 | 22 | 30 |