diff --git a/.gitignore b/.gitignore index cc8fd7f..253173c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ yarn-error.log .nyc_output/ coverage/ +~* diff --git a/changelog.md b/changelog.md deleted file mode 100644 index a8453dd..0000000 --- a/changelog.md +++ /dev/null @@ -1,93 +0,0 @@ -### v0.23.0 (2022-01-21) -- Added a cache for parsed type encodings, which should result in somewhat better performance -- `objc.import` now checks whether a bundle actually exists, and throws an error if it doesn't -- `objc.import` now returns whether the bundle import was successful or not -- Changed how `BOOL` and `char` return values are handled. On x86, `BOOL`s are now always returned as numbers (simple `if` checks should be unaffected by this), and on ARM they are returned as JS `Boolean`s - - -### v0.22.0 (2021-11-17) -- Improve support for struct parameters in blocks -- Improve support for inout (e.g. `BOOL*`) parameters in blocks -- Fix a crash when printing a (proxied) instance of a class implementing neither `-description` nor `-debugDescription` - -### v0.21.0 (2021-08-27) -- Switch to node-ffi-napi, ref-napi, and ref-struct-di -- Remove exception support -- Update node version requirements - -### v0.20.0 (2019-07-16) -- Added a struct compound initializer (see block.js for a usage example) -- Updated a bunch of outdated dependencies, removed some dependencies - -### v0.19.0 (2019-01-01) -- Updated the ffi module -- Updated supported node versions to >=8 -- Updated the framework loading code to use `NSBundle` instead of `dlopen` - -### v0.18.0 (2018-10-15) -- Added struct support -- Improved type encoding handling - -### v0.17.0 (2018-07-03) -- Added `Instance.isNull` (also exposed as `objc.isNull`) to check whether a `Instance` is holding a null pointer -- Renamed `ptr` to `__ptr` to indicate that it's an internal field - -### v0.16.0 (2018-06-28) -- fixed a bug where passing a `objc.InstanceProxy` to `objc.ns` would return an empty JS object - -### v0.15.0 (2018-06-07) -- Added `objc.createClass` -- The `objc.Selector` constructor can now also be called w/ a `SEL` pointer - -### v0.14.0 (2018-04-15) -- Implemented inout parameters -- Dropped support for node 7.x - -### v0.13.0 (2018-04-06) -- Added a `objc.swizzle` function - -### v0.12.0 (2018-02-23) -- Added support for loading `NSString*` constants -- Cache imported frameworks to avoid loading the same framework multiple times - -### v0.11.0 (2018-02-20) -- Better handling of null return values -- Iterate over `NSSet` instances - -### v0.10.0 (2018-01-30) -- Rewrote everything in JavaScript (no more c++ bindings) -- This removes support for accessing objc constants (will be re-implemented in the future) - -### v0.6.0 (2017-06-25) -- Added Block support - -### v0.5.0 (2017-06-19) -- Exception handling -- Allow omitting the last underscore in method calls - -### v0.4.0 (2017-05-20) -**New** -- JavaScript Arrays passed to ObjC methods will now automatically be converted to `NSArray *` instances -- Added `objc.ns` and `objc.js` functions to convert compatible objects (`String` <-> `NSString`, `Date` <-> `SDate`, etc.) -- Added support for iterating over enumerable Objective-C objects (using `objectEnumerator`) -- Implemented Selector resolving, meaning that you now can call objc methods that contain an underscore -- Added a bunch of documentation -- Added a bunch of tests - -**Changed** -- Explicitly require C++14 -- Removed NodeJS v6.x support (incompatible w/ C++14) - -**Fixed** -- Fixed the random "misaligned pointer" bug -- Logging an ObjCProxy holding a `nil` object no longer crashes the node process - -### v0.3.0 (2017-05-10) -- Added basic support for inout parameters (like `NSError **`) - -### v0.2.0 (2017-04-22) -- Added the `constant(name, [bundle])` function to load an ObjC string constant -- Constants can also be accessed directly from the module, the same way as classes are loaded - -### v0.1.0 (2017-04-19) -- Initial release diff --git a/examples/applescript.js b/examples/applescript.js index 96c6d13..a9594c5 100644 --- a/examples/applescript.js +++ b/examples/applescript.js @@ -1,9 +1,27 @@ +#!/usr/bin/env node + const objc = require('../src/index'); -const {NSAppleScript} = objc; +const {NSAppleScript, Ref} = objc; + +{ + const script = NSAppleScript.alloc().initWithSource_(` + tell application "System Events" to get name of current user + `); + + const err = new Ref(); + const result = script.executeAndReturnError_(err); + console.log('result:', result); // [objc: ] + console.log('error:', err.deref()); // null +} -const script = NSAppleScript.alloc().initWithSource_(`tell application "System Events" to get name of current user`); +{ + const script = NSAppleScript.alloc().initWithSource_(` + this is not valid code + `); -const err = objc.allocRef(); -const result = script.executeAndReturnError_(err); -console.log(result, err); + const err = new Ref(); + const result = script.executeAndReturnError_(err); + console.log('result:', result); // null + console.log('error:', err.deref()); // [objc { NSAppleScriptErrorBriefMessage = "Expected end of line, etc. but found identifier."; NSAppleScriptErrorMessage = "Expected end of line, etc. but found identifier."; NSAppleScriptErrorNumber = "-2741"; NSAppleScriptErrorRange = "NSRange: {23, 4}"; }] +} diff --git a/examples/blocks.js b/examples/blocks.js index 33bcda0..4cd092a 100644 --- a/examples/blocks.js +++ b/examples/blocks.js @@ -1,16 +1,35 @@ -const objc = require('../'); +#!/usr/bin/env node -const {wrap} = objc; -const {id, NSInteger} = objc.types; +const objc = require('../src/index'); -const array = objc.ns(['i', 'am', 'the', 'doctor']); -const block = new objc.Block((arg0, arg1) => { - arg0 = wrap(arg0); - arg1 = wrap(arg1); +// an NSComparator block takes 3 ObjC objects ('@') as arguments: the block itself plus 2 values to compare), and returns an NSComparisonResult (an Int64-based enum, 'q') - return arg0.length() > arg1.length() ? -1 : 1; -}, NSInteger, [id, id]); +// define the NSComparator type, using its ObjC type encoding string (naming the type is optional, but recommended) +// objc.defineBlock('q@@@', 'NSComparator'); // this is already defined in objc + +// once the type is defined, it can be referred to by its name... +// console.log(objc.NSComparator); // [class NSComparator extends Block] + + +// ...and new Block objects of that type created for use as arguments to ObjC methods: + +const array = objc.ns(['i','can','hold','up','TWO','books']); + + +const longestToShortest = new objc.NSComparator( + (thing1, thing2) => { + return thing1.length() > thing2.length() ? -1 : +1; + }); + +console.log(array.sortedArrayUsingComparator_(longestToShortest)); // [objc ( books, hold, TWO, can, up, i )] + + + +const shortestToLongest = new objc.NSComparator( + (thing1, thing2) => { + return thing1.length() < thing2.length() ? -1 : +1; + }); + +console.log(array.sortedArrayUsingComparator_(shortestToLongest)); // [objc ( i, up, TWO, can, hold, books )] -const sorted = array.sortedArrayUsingComparator_(block); -console.log(sorted); diff --git a/examples/classes.js b/examples/classes.js index b5cec73..7c49452 100644 --- a/examples/classes.js +++ b/examples/classes.js @@ -1,30 +1,47 @@ +#!/usr/bin/env node + const objc = require('../src/index'); -const {runtime, NSObject} = objc; -const LKGreeter_instanceMethods = { - 'greet:': (self, cmd, name) => { - name = objc.wrap(name); - return objc.ns(`hello, ${name}!`); - }, - _encodings: { - 'greet:': ['@', ['@', ':', '@']] - } -}; -const LKGreeter_classMethods = { - foo: (self, cmd) => { - console.log('foo'); +// the newly created class will be globally available as `objc.LKGreeter` +objc.defineClass('LKGreeter', 'NSObject', { + // ObjC type encodings for the following methods + $foo: 'v@:', + greet_: '@@:@', + description: '@@:', +}, { + // class methods + + $foo: (self) => { + console.log(`${self}.foo() class method was called`); + }, + + // instance methods + + greet_: (self, name) => { + return `greeter instance says: "hello, ${name}!"`; }, + + description: (self) => { + return ``; // ObjC descriptions are typically written as '<...>' + }, + +}); + + +console.log('LKGreeter class:', objc.LKGreeter); // LKGreeter class: [objc: LKGreeter] + + +objc.LKGreeter.foo(); // "[ObjCClass: LKGreeter].foo() class method was called" + +const greeter = objc.LKGreeter.new(); // instantiate by calling ObjC class method `new` - _encodings: { - 'foo': ['v', ['@', ':']] - } -}; +console.log('LKGreeter instance:', greeter); // LKGreeter instance: [objc: ] +console.log(objc.js(greeter.greet_('lukas'))); // greeter instance says: "hello, lukas!" -const LKGreeter = objc.createClass('LKGreeter', 'NSObject', LKGreeter_instanceMethods, LKGreeter_classMethods); -LKGreeter.foo(); +console.log(objc.js(greeter.description())); // '' -console.log(LKGreeter.new().greet('lukas')); +console.log(`LKGreeter instance: ${greeter}`); // LKGreeter instance: [] diff --git a/examples/clipboard.js b/examples/clipboard.js index 57c6518..0ea82fc 100644 --- a/examples/clipboard.js +++ b/examples/clipboard.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + 'use strict'; const objc = require('../src/index'); @@ -15,21 +17,21 @@ const { let pasteboard = NSPasteboard.generalPasteboard(); pasteboard.declareTypes_owner_([NSPasteboardTypeString], null); -const get = () => { +const getClipboard = () => { return pasteboard.stringForType_(NSPasteboardTypeString); }; -const set = text => { +const setClipboard = text => { text = NSString.stringWithUTF8String_(text); console.log(`new text: ${text}`); - let oldValue = get(); + let oldValue = getClipboard(); pasteboard.setString_forType_(text, NSPasteboardTypeString); return oldValue; }; -set('Hello World!'); +setClipboard('Hello World!'); -let contents = get(); // This is now still an `NSString` +let contents = getClipboard(); // This is now still an `NSString` -console.log(String(contents)); // The `String(...)` call converts the `NSString to a native JavaScript string object` +console.log(String(contents)); // The `String(...)` call converts the `NSString` to a native JavaScript string object diff --git a/examples/date.js b/examples/date.js index 47b1b53..e571a80 100644 --- a/examples/date.js +++ b/examples/date.js @@ -1,7 +1,8 @@ +#!/usr/bin/env node + 'use strict'; -const objc = require('../src/'); -const util = require('util'); +const objc = require('../src/index'); const { NSDate, @@ -11,15 +12,15 @@ const { let now = NSDate.date(); console.log('LOG DATE') -console.log('now', now); +console.log('now:', now); // e.g. now: [objc: 2022-02-04 12:15:44 +0000] // Convert the NSDate object to a JavaScript date -let asJSDate = objc.js(now); -console.log('asJSDate', asJSDate); +let jsdate = objc.js(now); +console.log('jsdate:', jsdate); // jsdate: 2022-02-04T12:15:44.862Z -let asNSDate = objc.ns(asJSDate); -console.log('asNSDate', asNSDate); +let nsdate = objc.ns(jsdate); +console.log('nsdate:', nsdate); // nsdate: [objc: 2022-02-04 12:15:44 +0000] let localizedDate = NSDateFormatter.localizedStringFromDate_dateStyle_timeStyle_(now, 2, 2); -console.log('loc', String(localizedDate)); // -> "19. Apr 2017, 22:41:13" +console.log('localized:', String(localizedDate)); // localized: 4 Feb 2022 at 12:15:44 diff --git a/examples/delegate.js b/examples/delegate.js index a1bb74c..cd825fe 100644 --- a/examples/delegate.js +++ b/examples/delegate.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + const objc = require('../src/index'); const { @@ -8,30 +10,33 @@ const { -const FileManagerDelegate = objc.createClass('FileManagerDelegate', 'NSObject', { - 'fileManager:shouldMoveItemAtPath:toPath:': (self, cmd, fileManager, srcPath, dstPath) => { - console.log('-[NSFileManagerDelegate fileManager:shouldMoveItemAtPath:toPath:]'); +// the newly created ObjC class is available as `objc.FileManagerDelegate` +objc.defineClass('FileManagerDelegate', 'NSObject', { + // type encodings + fileManager_shouldMoveItemAtPath_toPath_: 'c40@0:8@16@24@32', +}, { + // methods + + fileManager_shouldMoveItemAtPath_toPath_: (self, cmd, fileManager, srcPath, dstPath) => { + console.log('calling: -[NSFileManagerDelegate fileManager:shouldMoveItemAtPath:toPath:]'); return 1; }, - - _encodings: { - 'fileManager:shouldMoveItemAtPath:toPath:': ['c40', ['@0', ':8', '@16', '@24', '@32']] - } + }); const fm = NSFileManager.new(); -const delegate = FileManagerDelegate.new(); +const delegate = objc.FileManagerDelegate.new(); fm.setDelegate_(delegate); const pathA = 'x.txt'; const pathB = 'y.txt'; -// create file +// create file const data = objc.ns('hello world').dataUsingEncoding_(4); fm.createFileAtPath_contents_attributes_(pathA, data, null); diff --git a/examples/nsarray.js b/examples/nsarray.js index dd2b903..9150f4e 100644 --- a/examples/nsarray.js +++ b/examples/nsarray.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + 'use strict'; const objc = require('../src/index'); @@ -7,27 +9,25 @@ const { NSMutableArray } = objc; -let array = NSArray.arrayWithArray_(["Hey", "missy", "you", "so", "fine"]); -//console.log("array:", array); +let nsarray = NSArray.arrayWithArray_(["Hey", "missy", "you", "so", "fine"]); +console.log("nsarray:", nsarray); -//console.log("count:", array.count()); +console.log("count:", nsarray.count()); -//console.log("array[1]:", array.objectAtIndex_(1)); -//console.log("first object:", array.firstObject()); +console.log("nsarray[1]:", nsarray.objectAtIndex_(1)); +console.log("first object:", nsarray.firstObject()); -// Sort -let sortedUsingSelector = array.sortedArrayUsingSelector_("caseInsensitiveCompare:") -console.log("sorted", sortedUsingSelector); +// sorted alphabetically: [objc ( fine, Hey, missy, so, you )] +console.log("sorted alphabetically:", nsarray.sortedArrayUsingSelector_("caseInsensitiveCompare:")); -let asJSArray = objc.js(array); -//console.log('js:', asJSArray); +console.log('js array:', objc.js(nsarray)); -let nsarrayFromJs = objc.ns(['missy', 'you', 'so', 'fine']); -//console.log('ns:', nsarrayFromJs); +nsarray = objc.ns(['Hey', 'missy', 'you', 'so', 'fine']); +console.log('nsarray:', nsarray); // Iterate over an array -for (let str of array) { - //console.log(String(str)); +for (let item of nsarray) { + console.log(item); } @@ -39,20 +39,8 @@ for (let str of array) { // Sort using block +const shortestToLongest = new objc.NSComparator((arg1, arg2) => (arg1.length() < arg2.length()) ? -1 : +1); -var block = new objc.Block((arg1, arg2) => { - arg1 = objc.wrap(arg1); - arg2 = objc.wrap(arg2); - return arg1.length() < arg2.length() ? -1 : 1; -}, 'q', ['@', '@']); - - -let sortedUsingBlock = array.sortedArrayUsingComparator_(block); -console.log(sortedUsingBlock); - - - - - +// sorted by length: [objc ( so, you, Hey, fine, missy )] +console.log("sorted by length:", nsarray.sortedArrayUsingComparator_(shortestToLongest)); -//s diff --git a/examples/nsdictionary.js b/examples/nsdictionary.js index 4dcc529..9e7e011 100644 --- a/examples/nsdictionary.js +++ b/examples/nsdictionary.js @@ -1,8 +1,8 @@ +#!/usr/bin/env node + const objc = require('../src/index'); -const { - NSMutableDictionary -} = objc; +// const { NSMutableDictionary } = objc; const me_js = { @@ -13,12 +13,12 @@ const me_js = { age: 19 }; -console.log(me_js); +console.log(me_js); // { name: { first: 'Lukas', last: 'Kollmer' }, age: 19 } const me_ns = objc.ns(me_js); -console.log(me_ns); +console.log(me_ns); // [objc: { age = 19; name = { first = Lukas; last = Kollmer; }; }] const me_js_2 = objc.js(me_ns); -console.log(me_js_2); +console.log(me_js_2); // { name: { first: 'Lukas', last: 'Kollmer' }, age: 19 } -console.log(objc.ns('hey').isKindOfClass_('NSString')); +console.log(objc.ns('hey').isKindOfClass_(objc.NSString)); // true diff --git a/examples/range.js b/examples/range.js index 2d84ed8..fa1a083 100644 --- a/examples/range.js +++ b/examples/range.js @@ -1,4 +1,6 @@ -const objc = require('../'); +#!/usr/bin/env node + +const objc = require('../src/index'); const ref = require('ref-napi'); objc.import('AppKit'); @@ -8,38 +10,61 @@ const { NSMutableString, NSAttributedString, NSMutableAttributedString, - NSLinkAttributeName + NSLinkAttributeName, + NSRange, } = objc; -const { id, NSRange } = objc.types; let attrString = NSMutableAttributedString.new(); -attrString.appendAttributedString_(NSAttributedString.alloc().initWithString_attributes_( - 'abc', { - [NSLinkAttributeName]: 'https://example.com' - } -)); -attrString.appendAttributedString_(NSAttributedString.alloc().initWithString_attributes_( - 'def', { - [NSLinkAttributeName]: 'https://example.de' - } -)); -console.log(attrString); +attrString.appendAttributedString_( + NSAttributedString.alloc().initWithString_attributes_('ABC', { + [NSLinkAttributeName]: 'https://example.com', + }) +); + +attrString.appendAttributedString_( + NSAttributedString.alloc().initWithString_attributes_('DEFG', { + [NSLinkAttributeName]: 'https://example.de', + }) +); + +console.log(attrString); // [objc: ABC{ NSLink = "https://example.com"; }DEFG{ NSLink = "https://example.de"; }] + + +let range = new NSRange({location: 0, length: attrString.length()}); +//console.log(range); -let range = NSRange.new(0, attrString.length()); -console.log(range); -let block = new objc.Block((arg0, arg1, arg2) => { - const attrs = objc.wrap(arg0); - const range = arg1; - console.log(`block called w/ args attrs: ${attrs}, range: ${[arg1.location, arg1.length]}, stop: ${arg2.deref()}`); - //ref.set(arg2, 0, 1); // uncomment this to have it stop iterating after the first range. +// (void (^)(NSDictionary * attrs, NSRange range, BOOL * stop)) +const enumeratorBlock = objc.defineBlock('v@@{_NSRange}^B'); // TO DO: there is an 'already existss' error if {_NSRange=QQ} is used, which shouldn't happen as long as type encodings are the same/equivalent in both + + +let block = new enumeratorBlock( + (attrs, range, stop) => { // TO DO: stop should be Ref containing boolean; on return, its original ptr needs updated + console.log(`block called w/ args attrs: ${attrs}, range: ${[range.location, range.length]}, stop: ${stop.value}`); + //ref.set(arg2, 0, 1); // uncomment this to have it stop iterating after the first range. // TO DO: update return; -}, objc.types.void, [id, NSRange, ref.refType(objc.types.char)]); + } +); + +/* TO DO: FIX + +Error: NSConcreteMutableAttributedString.enumerateAttributesInRange_options_usingBlock_ expected 'v@:{_NSRange=QQ}Q@?', got ([StructType], [number], [AnonymousBlock]): RangeError: Maximum call stack size exceeded} + at Object.proxy [as msgSend] (/Users/has/node_modules/ffi-napi/lib/_foreign_function.js:61:14) + at Proxy.callObjCMethod (/Users/has/dev/javascript/objc/src/instance.js:118:37) + at Object. (/Users/has/dev/javascript/objc/examples/range.js:68:12) + at Module._compile (node:internal/modules/cjs/loader:1101:14) + at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10) + at Module.load (node:internal/modules/cjs/loader:981:32) + at Function.Module._load (node:internal/modules/cjs/loader:822:12) + at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) + at node:internal/main/run_main_module:17:47 + +*/ attrString.enumerateAttributesInRange_options_usingBlock_(range, 0, block); diff --git a/examples/something.js b/examples/something.js deleted file mode 100644 index 1e65c05..0000000 --- a/examples/something.js +++ /dev/null @@ -1,42 +0,0 @@ - -/* -runtime.import('AppKit'); - - -const main = new Proxy({}, { - get: (target, name) => { - return new InstanceProxy(new Instance(name)) - } -}) - - -const { - NSString, - NSDate, - NSDateFormatter -} = main; - - -let now = NSDate.date() -let localizedDate = NSDateFormatter.localizedStringFromDate_dateStyle_timeStyle_(now, 2, 2); - -console.log(localizedDate); // -> "19. Apr 2017, 22:41:13" - -console.log(NSDate.date()); -*/ - -/* -const NSDate = new Instance('NSDate') -console.log(NSDate); - -const date = NSDate.call('date'); -console.log(date.description()); - - -const newDate = date.call('dateByAddingTimeInterval:', 5); -console.log(newDate.description()); - - -const NSProcessInfo = new Instance('NSProcessInfo'); -console.log(`pi: ${NSProcessInfo.call("processInfo").description()}`); -*/ diff --git a/examples/struct.js b/examples/struct.js index 935cd0a..7ad4f02 100644 --- a/examples/struct.js +++ b/examples/struct.js @@ -1,45 +1,49 @@ -const objc = require('../'); +#!/usr/bin/env node + const ffi = require('ffi-napi'); -const CGFloat = objc.types.double; -const CGPoint = objc.defineStruct('CGPoint', { - x: CGFloat, - y: CGFloat -}); +const objc = require('../src/index'); -const CGSize = objc.defineStruct('CGSize', { - width: CGFloat, - height: CGFloat -}); +const NSObjectType = objc.__internal__.types.objc_instance_t; -const CGRect = objc.defineStruct('CGRect', { - origin: CGPoint, - size: CGSize + +// note: NSStringFromRect and NSRectFromString are C functions, so for now must be manually bridged // TO DO: add `objc.defineFunction` to simplify this +const foundation = new ffi.Library(null, { + NSStringFromRect: [NSObjectType, [objc.NSRect]], + NSRectFromString: [objc.NSRect, [NSObjectType]], }); -const libFoundation = new ffi.Library(null, { - NSStringFromRect: ['pointer', [CGRect]], - NSRectFromString: [CGRect, ['pointer']] + +// create a new NSRect value: + +const rect = new objc.NSRect({ + origin: new objc.NSPoint({x: 5, y: 10}), + size: new objc.NSSize({width: 100, height: 250}), }); -const rect = CGRect.new( - CGPoint.new(5, 10), - CGSize.new(100, 250) -); -const string = objc.wrap(libFoundation.NSStringFromRect(rect)); -console.log(string); + + +//console.log(rect) // TO DO: ref-struct-di's StructType works, but it has an awful unreadable inspection string; we should provide our own implementation for usability, troubleshooting, etc + + +const string = foundation.NSStringFromRect(rect); + +console.log(string); // [objc: {{5, 10}, {100, 250}}] + +console.log(objc.js(string)); // '{{5, 10}, {100, 250}}' + + const string2 = objc.ns('{{1, 2}, {3, 4}}'); -const rect2 = libFoundation.NSRectFromString(string2.__ptr); -console.log(rect2); + +const rect2 = foundation.NSRectFromString(string2); + +//console.log(rect2); + +// TO DO: again, Structs should generate their own human-readable representations, but for now we must format manually console.log(` -Rect { - origin: { - x: ${rect2.origin.x} - y: ${rect2.origin.y} - }, - size: { - width: ${rect2.size.width} - height: ${rect2.size.height} - } +NSRect { + origin: {x: ${rect2.origin.x}, y: ${rect2.origin.y}}, + size: {width: ${rect2.size.width}, height: ${rect2.size.height}} } `); + diff --git a/examples/struct2.js b/examples/struct2.js deleted file mode 100644 index 3d98c83..0000000 --- a/examples/struct2.js +++ /dev/null @@ -1,11 +0,0 @@ -const objc = require('../'); -const path = '/Users/lukas/Library/Developer/Xcode/DerivedData/test_LKFramework-cegmgngjjlcdtkewahzbxzzgxchq/Build/Products/Debug/LKFramework.framework'; - -objc.import(path); - -const {NSBundle} = objc; -//const b2 = NSBundle.bundleWithPath_(path); -//b2.load(); -console.log(NSBundle); - -//console.log(objc.runtime.classExists('LKClassThatDoesShit')); diff --git a/examples/swizzle.js b/examples/swizzle.js index 7b92d3c..01c3647 100644 --- a/examples/swizzle.js +++ b/examples/swizzle.js @@ -1,37 +1,43 @@ -const ref = require('ref-napi'); +#!/usr/bin/env node + const objc = require('../src/index'); -const { - NSDate, - NSProcessInfo, - wrap -} = objc; +const { NSDate, NSProcessInfo } = objc; + -objc.swizzle(NSDate, 'date', () => { +const swapper = objc.__internal__.swizzle(NSDate, 'date', (self) => { return NSDate.distantPast(); -}, 'class'); +}, true); +// +[NSDate date ]: Fri Dec 29 0000 23:58:45 GMT-0001 (Greenwich Mean Time) +// +[NSDate date ]: Fri Dec 29 0000 23:58:45 GMT-0001 (Greenwich Mean Time) +// +[NSDate distantPast ]: Fri Dec 29 0000 23:58:45 GMT-0001 (Greenwich Mean Time) +// +[NSDate distantFuture]: Mon Jan 01 4001 00:00:00 GMT+0000 (Greenwich Mean Time) +// TO DO: FIX: TypeError: No method named objc.NSDate.xxx__date + console.log(`+[NSDate date ]: ${NSDate.date()}`); console.log(`+[NSDate date ]: ${NSDate.date()}`); console.log(`+[NSDate distantPast ]: ${NSDate.distantPast()}`); console.log(`+[NSDate distantFuture]: ${NSDate.distantFuture()}`); -console.log(`+[NSDate xxx__date ]: ${NSDate.xxx__date()}`); +//console.log(`+[NSDate xxx__date ]: ${NSDate.xxx__date()}`); + -objc.swizzle('NSProcessInfo', 'processorCount', () => { - return 12; +objc.__internal__.swizzle(NSProcessInfo, 'processorCount', (self) => { // TO DO: FIX: bus error + return 71; }); -const pi = NSProcessInfo.processInfo(); -console.log(pi.processorCount()); +const pi = NSProcessInfo.processInfo(); +console.log('fake processor count:', pi.processorCount()); // 71 -objc.swizzle(NSDate, 'dateByAddingTimeInterval:', (self, _cmd, timeInterval) => { - self = wrap(self); +objc.__internal__.swizzle(NSDate, 'dateByAddingTimeInterval:', (self, timeInterval) => { return self.xxx__dateByAddingTimeInterval_(timeInterval * 2); }); -const now = NSDate.xxx__date(); -console.log(now); -console.log(now.dateByAddingTimeInterval_(2)); + +// +//const now = NSDate.xxx__date(); // TO DO: FIX: this still throws error: No method named objc.NSDate.xxx__date +//console.log(now); +//console.log(now.dateByAddingTimeInterval_(2)); diff --git a/examples/touch-id.js b/examples/touch-id.js index 85933b6..c989f95 100644 --- a/examples/touch-id.js +++ b/examples/touch-id.js @@ -24,13 +24,15 @@ console.log(context.canEvaluatePolicy_error_(LAPolicyDeviceOwnerAuthenticationWi let sema = c.dispatch_semaphore_create(0); -const handler = new objc.Block((success, err) => { +const HandlerType = objc.defineBlock('v@c@'); + +const handler = new HandlerType((success, err) => { console.log('handler'); c.dispatch_semaphore_signal(sema); -}, 'v', ['c', '@']); +}); console.log(objc.NSThread.currentThread()); context.evaluatePolicy_localizedReason_reply_(LAPolicyDeviceOwnerAuthenticationWithBiometrics, 'HEY', handler); -c.dispatch_semaphore_wait(sema, Number.MAX_VALUE); \ No newline at end of file +c.dispatch_semaphore_wait(sema, Number.MAX_VALUE); diff --git a/examples/username.js b/examples/username.js index f666b72..f1eda2f 100644 --- a/examples/username.js +++ b/examples/username.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + 'use strict'; const objc = require('../src/index'); @@ -8,4 +10,4 @@ const { let username = NSProcessInfo.processInfo().userName(); -console.log(`Your username is: ${username}`); +console.log(`Your username is: ${username}`); // "Your username is: ..." diff --git a/package.json b/package.json index 0926a1f..22bda13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "objc", - "version": "0.23.0", + "version": "0.24.1", "description": "NodeJS ↔ Objective-C bridge", "license": "MIT", "repository": "lukaskollmer/objc", @@ -20,6 +20,10 @@ "bridge", "runtime" ], + "engines": { + "npm": ">=6.14.4", + "node": ">=14.0.0" + }, "dependencies": { "ffi-napi": "^4.0.3", "ref-napi": "^3.0.3", diff --git a/readme.md b/readme.md index 1819020..f8e2edf 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,8 @@ ## Install +For now, install from: https://github.com/hhas/objc + ``` $ npm install --save objc ``` @@ -24,48 +26,68 @@ const { let now = NSDate.date() let localizedDate = NSDateFormatter.localizedStringFromDate_dateStyle_timeStyle_(now, 2, 2); -console.log(localizedDate); // -> "19. Apr 2017, 22:41:13" +console.log(localizedDate); // -> "19. Apr 2022, 22:41:13" ``` ### Topics - [API](#api) - [Calling Methods](#calling-methods) -- [Blocks](#blocks) +- [Inout Parameters](#inout-parameters) - [Constants](#constants) +- [Blocks](#blocks) +- [Functions](#functions) - [Structs](#structs) -- [Inout Parameters](#inout-parameters) -- [Method Swizzling](#method-swizzling) - [Custom Classes](#custom-classes) ### API ##### `objc.import(bundleName)` -Import an Objective-C framework. Foundation is always imported by default -##### `objc.ns(object, [hint = '@'])` -Convert a JavaScript object to its objc equivalent. Returns `null` if the object doesn't have an objc counterpart. -Takes an optional second parameter to specify whether strings should be converted to `NSString` objects (default), `SEL` or `Class` +Import an Objective-C framework. For example: -##### `objc.js(object, [returnInputIfUnableToConvert = false])` -Convert an objc object to its JavaScript equivalent. -Takes an optional second parameter to specify whether it should return `null` or the input if the object doesn't have a JS counterpart +```js +const objc = require('objc'); +objc.import('AppKit'); +``` -### Calling methods -When calling Objective-C methods, all you need to do is replace the colons in the selector with underscores. +Foundation is always imported by default. -For example, this Objective-C code: +##### `objc.ns(object, [resultIfUnconverted])` -```objc -#import +Convert a JavaScript object to its objc equivalent. +Takes an optional second parameter that determines the result if object is not converted. This may be a function that takes the object as its argument and returns its objc equivalent, an objc value, or `null`. If omitted, throws `TypeError`. -NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; -[pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil]; +##### `objc.js(object, [resultIfUnconverted])` -[pasteboard setString:@"44 > 45" forType:NSPasteboardTypeString]; +Convert an objc object to its JavaScript equivalent. +Takes an optional second parameter that determines the result if object is not converted. This may be a function that takes the object as its argument and returns its JS equivalent, a JS value, or `null`. If omitted, returns the object as-is. + +##### `objc.NAME` + +Get an ObjC class. For example: + +```js +objc.NSString +objc.NSMutableArray ``` -is equivalent to the following JavaScript code: +The framework for that class must be imported first or an `Error` will be thrown. + +#### `objc.auto(fn, ...args)` + +Create an autorelease pool before calling a function, `fn`, automatically draining the pool after the function returns or errors. Any additional arguments are passed to the function. + +### Calling methods + +_TO DO: array, union, bitwise types_ + +When calling Objective-C methods: + +- replace any underscores in the selector with double underscores, e.g. `"foo_barBaz"` becomes `"foo__barBaz"` +- replace the colons in the selector with underscores, e.g. `"bar:fubZim:"` becomes `"bar_fubZim_"` + +For example, this JavaScript code: ```js const objc = require('objc'); @@ -79,180 +101,166 @@ pasteboard.declareTypes_owner_([NSPasteboardTypeString], null); pasteboard.setString_forType_("44 > 45", NSPasteboardTypeString); ``` -### Blocks -You can create a block with the `objc.Block` helper class: -```js -const block = new objc.Block(() => { - console.log('In the block!'); -}, 'v', []); -``` +is equivalent to the following Objective-C code: -When creating a block, you need to explicitly declare the type encoding of the block's return value and all its parameters. +```objc +#import + +NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; +[pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil]; + +[pasteboard setString:@"44 > 45" forType:NSPasteboardTypeString]; +``` -**Note** -If a block takes an Objective-C object as its parameter, you'll need to manually wrap that object in an `objc.Proxy` (via the `objc.wrap` helper function). +### Inout arguments -
+If a method expects an `inout`/`out` argument (e.g. `NSError**`), use an `objc.Ref` instance: -**Example:** Sort an array by word length, longest to shortest ```js -const {NSArray, Block, wrap} = objc; -const {id, NSInteger} = objc.types; -const array = NSArray.arrayWithArray_(['I', 'Am', 'The', 'Doctor']); +const {NSAppleScript, Ref} = objc; -const block = new Block((arg1, arg2) => { - arg1 = wrap(arg1); - arg2 = wrap(arg2); - return arg1.length() > arg2.length() ? -1 : 1; -}, NSInteger, [id, id]); // Match the NSComparator signature +const script = NSAppleScript.alloc().initWithSource_('get foobar'); -const sorted = array.sortedArrayUsingComparator_(block); -// => ['Doctor', 'The', 'Am', 'I'] +const error = new Ref(); +script.executeAndReturnError_(error); // `executeAndReturnError:` takes a `NSDictionary**` + +console.log(error.deref()); // `error` is now a `NSDictionary*` +``` + +Output: + +``` +[objc { + NSAppleScriptErrorBriefMessage = "The variable foobar is not defined."; + NSAppleScriptErrorMessage = "The variable foobar is not defined."; + NSAppleScriptErrorNumber = "-2753"; + NSAppleScriptErrorRange = "NSRange: {0, 6}"; +}] ``` +The `Ref` constructor optionally takes an "in" value as argument. This can be an objc object, JS value, or null (the default). On return, call its `deref` method to obtain the out value. + + ### Constants -You can load `NSString*` constants just like you'd access a class: + +You can load ObjC constants (typically `NSString*`) just like you'd access a class: ```js -const {NSFontAttributeName} = objc; -console.log(NSFontAttributeName); // => 'NSFont' +const objc = require('objc'); + +console.log(objc.NSFontAttributeName); // => 'NSFont' ``` -`NSString*` constants are returned as native JavaScript `String` objects. +ObjC constants are returned as objc objects. -### Structs -Use the `objc.defineStruct` function to define a struct by its name and layout. The returned type can be used to create instances of the struct, and when specifying type encodings in the `objc` module. It is also compatible with the `ffi-napi`, `ref-napi`, `ref-struct-di` modules. +### Blocks -You can use the `StructType.new` function to create an instance of the struct. Optionally, you can pass +_TO DO: finalize API_ -The `objc` module already provides a definition for `NSRange`, accessible via `objc.types`. +Use `objc.defineBlock(encoding[,...names])` to define a block's [type encoding](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html), optionally followed by any human-readable names for that type: -**Example 1** Using structs with objc methods ```js -const {NSRange} = objc.types; - -const string = objc.ns('Hello World'); -const substring = string.substringWithRange_(NSRange.new(0, 5)); -// -> 'Hello' +objc.defineBlock('q@@@', 'NSComparator'); ``` -
-Example 2** Using structs with the ffi module +When creating a block, you need to explicitly declare the type encoding of the block's return value and all its parameters. + +**Example:** Sort an array by word length, longest to shortest ```js -const ffi = require('ffi-napi'); -const CGFloat = objc.types.double; +const objc = require('objc'); -const CGPoint = objc.defineStruct('CGPoint', { - x: CGFloat, - y: CGFloat -}); +objc.defineBlock('q@@@', 'NSComparator'); -const CGSize = objc.defineStruct('CGSize', { - width: CGFloat, - height: CGFloat -}); -const CGRect = objc.defineStruct('CGRect', { - origin: CGPoint, - size: CGSize -}); +const array = NSArray.arrayWithArray_(['I', 'Am', 'The', 'Doctor']); -const libFoundation = new ffi.Library(null, { - NSStringFromRect: ['pointer', [CGRect]] -}); -const rect = CGRect.new( - CGPoint.new(5, 10), - CGSize.new(100, 250) -); -const string = objc.wrap(libFoundation.NSStringFromRect(rect)) -// -> '{{5, 10}, {100, 250}}' +const longestToShortest = new objc.NSComparator( + (thing1, thing2) => { + return thing1.length() < thing2.length() ? -1 : +1; + }); + +const sorted = array.sortedArrayUsingComparator_(longestToShortest); +// => ['Doctor', 'The', 'Am', 'I'] ``` -
-### Inout parameters -If a method expects an inout parameter (like `NSError**`), you can use the `objc.allocRef` function to get a pointer to a `nil` objc object that can be passed to a method expecting an `id*`: -```js -const {NSAppleScript} = objc; +### Functions -const script = NSAppleScript.alloc().initWithSource_('foobar'); +_TO DO: implement `wrapFunction(name,encoding)`_ -const error = objc.allocRef(); -script.executeAndReturnError_(error); // `executeAndReturnError:` takes a `NSDictionary**` +e.g. `NSStringFromRect` -console.log(error); // `error` is now a `NSDictionary*` -``` -Output: -``` -[objc.InstanceProxy { - NSAppleScriptErrorBriefMessage = "The variable foobar is not defined."; - NSAppleScriptErrorMessage = "The variable foobar is not defined."; - NSAppleScriptErrorNumber = "-2753"; - NSAppleScriptErrorRange = "NSRange: {0, 6}"; -}] -``` -If you need more advanced inout functionality (using primitive types, etc), simply use the [`ref`](https://github.com/TooTallNate/ref) module. +### Structs +_TO DO: finalize StructType implementation_ -### Method swizzling -Method swizzling allows you to replace a method's implementation: -```js -const {NSProcessInfo} = objc; -objc.swizzle(NSProcessInfo, 'processorCount', (self, _cmd) => { - return 12; -}); +Use `obj.defineStruct(encoding[,...names])` function to define a struct by its name and layout. The resulting `StructType` is available as `objc.NAME`. It is also compatible with the `ffi-napi`, `ref-napi`, `ref-struct-di` modules. -NSProcessInfo.processInfo().processorCount(); // => 12 -``` +The `objc` module already provides definitions for the following: -The method's original implementation is still available, with the `xxx__` prefix: +* `NSPoint` +* `NSSize` +* `NSRect` +* `NSRange` -```js -const {NSDate, wrap} = objc; -objc.swizzle(NSDate, 'dateByAddingTimeInterval:', (self, _cmd, timeInterval) => { - self = wrap(self); - return self.xxx__dateByAddingTimeInterval_(timeInterval * 2); -}); +Use `new StructType(object)` to create an instance of the struct, passing an object to populate the struct. (Note: missing fields are set to `0`/`null`.) -const now = NSDate.date(); -const a = now.dateByAddingTimeInterval_(2); -const b = now.xxx__dateByAddingTimeInterval_(4); +**Example:** Using structs with objc methods + +```js +const objc = require('objc'); -a.isEqualToDate_(b); // => true +const string = objc.ns('Hello World'); +const substring = string.substringWithRange_(new objc.NSRange({location: 0, length: 5})); +// => 'Hello' ``` -**Note** -- Just like with blocks, you have to `wrap` all non-primitive parameters -- If you want to swizzle a class method, pass `'class'` as the `swizzle` function's last parameter -- `objc.swizzle` returns a function that - if called - restores the original implementation of the swizzled method + ### Custom Classes -Use the `objc.createClass` function to register custom classes with the Objective-C runtime: + +_TO DO: API is not finalized_ + +Use the `objc.defineClass` function to register a custom class with the Objective-C runtime: + ```js const objc = require('objc'); -const LKGreeter = objc.createClass('LKGreeter', 'NSObject', { - 'greet:': (self, cmd, name) => { - name = objc.wrap(name); - return objc.ns(`Hello, ${name}!`); +const LKGreeter = objc.defineClass('LKGreeter', 'NSObject', { + // define the ObjC type encodings + + greet_: '@@:@', // -(id)greet:(id) +}, { + // define the method implementations + + greet_: (self, name) => { + return `Hello, ${name}!`; }, - - _encodings: { - 'greet:': ['@', ['@', ':', '@']] - } }); -LKGreeter.new().greet('Lukas'); // => 'Hello, Lukas!' +LKGreeter.new().greet_('Lukas'); // => 'Hello, Lukas!' ``` -**Note**: You might have to specify individual offsets in the type encoding, see [this example](/examples/delegate.js). + +The method's type encoding consists of the return type (in this example, `@`), followed by the target (always `@`) and selector (always `:`) arguments, followed by any additional arguments (in this example, `@`) to be passed to the method. + +The method function should take the target object (`self`) as its first argument, followed by any additional arguments. The selector argument is omitted. + +To define class methods, prefix the method name with `$`, e.g.: + +* `$foo_bar_` => class method `foo:bar:` + +* `foo_bar_` => instance method `foo:bar:` + +**Note:** You might have to specify individual offsets in the type encoding, see [this example](/examples/delegate.js). ## Roadmap In the future, I'd like to add support for: -- improved support for inout parameters (`id*`) +- varargs - c-style arrays, unions as method parameter/return type - runtime introspection (accessing an object's properties, ivars, methods, etc) - improved class creation api +- thread-safe ## License MIT © [Lukas Kollmer](https://lukaskollmer.me) diff --git a/src/block.js b/src/block.js index f892388..3cbe14e 100644 --- a/src/block.js +++ b/src/block.js @@ -1,72 +1,234 @@ /* eslint-disable camelcase, key-spacing */ + +// wrap a JS function as an ObjC block so it can be called from ObjC + +// BlockType is a ref-napi compatible type object representing the ObjC block's signature + +// Block wraps a BlockType with a JS callback function that will be called when the block is invoked in ObjC + + +// TO DO: Block only supports wrapping JS functions as ObjC blocks for passing into ObjC; what about passing an ObjC block into JS so that the JS code can call it? (e.g. in a JS-based subclass); opaque_block_t should allow ObjC blocks to pass through JS but doesn't allow calling them within JS (which, amongst other things, requires knowing the block's exact ObjC type) + +// TO DO: inout arguments (these will need to pass a Ref object into the JS callback; the JS callback can then replace the Ref’s value as appropriate so that the ObjC pointer will be updated immediately/when the callback returns) + +// TO DO: what about ObjC properties/methods that return an ObjC block? for now, the block will appear as a pointer of type `@?`, which is how the ObjC introspection APIs represent all blocks, but without knowing its true type encoding (which is only defined in ObjC header and bridgesupport files), any block passed from ObjC *to* JS must remain opaque and uncallabel within JS; therefore, methods need to pack and unpack block arguments and return values using objc_opaqueblock_t (note: will need to find an ObjC method that returns a block against which to test) + + +/* +Important: the Block constructor uses fn.length to determine the number of arguments it should pass to the callback function; if the function takes 1 less argument than the number of arguments in the ObjC encoding string, it omits the block as first argument to callback. + +This means that the encoding string *must* represent the block's canonical type; i.e. its first argument is always '@', which is the ObjC block itself. The alternative would be to pass an explicit flag: + + objc.defineBlock(FUNCTION[,SKIP_BLOCK_ARGUMENT=true/false]),) + +which would mean that the number of arguments to pass to the callback is always known, and comparing argc can be used to check that the callback takes the correct number of [formal] arguments. + +Caution: fn.length doesn't count named parameters that have a default value, nor ...rest; however, this'd only be an issue if user was recycling an existing function written for non-ObjC related code. The expectation is that a function passed to a Block's constructor is written specifically for this purpose so will explicitly define the correct number of arguments with no default values. + +*/ + +/* + +Example block type declaration from Foundation.bridgesupport: + + + + + + + + + + + + +note that bridgesupport defines each argument and return type separately; thus this block's type encoding is "v@@Qo^B" +*/ + + + const ffi = require('ffi-napi'); -const structs = require('./structs'); -const {pointer, int32, ulonglong} = require('./types'); +const ref = require('ref-napi'); +const StructType = require('ref-struct-di')(ref); + +const constants = require('./constants'); const runtime = require('./runtime'); -const {coerceType} = require('./type-encodings'); + +const objctypes = require('./objctypes'); + +const pointerType = ref.refType(ref.types.void); + const _NSConcreteGlobalBlock = runtime.getSymbol('_NSConcreteGlobalBlock'); -const block_t = structs.defineStruct(null, { - isa: pointer, - flags: int32, - reserved: int32, - invoke: pointer, - descriptor: pointer + +/******************************************************************************/ +// an ObjC block + +// an ObjC Block has the following in-memory structure: + +const block_t = StructType({ + isa: pointerType, + flags: ref.types.int32, + reserved: ref.types.int32, + invoke: pointerType, + descriptor: pointerType }); -const descriptor = structs.defineStruct(null, { - reserved: ulonglong, - block_size: ulonglong -}).new(0, block_t.size); +const descriptor_t = StructType({ + reserved: ref.types.ulonglong, + block_size: ref.types.ulonglong +}); + +const descriptor = new descriptor_t(0, block_t.size); -class Block { - constructor(fn, returnType, argumentTypes, skipBlockArgument = true) { - if (typeof fn !== 'function' || returnType === undefined || argumentTypes === undefined) { - throw new TypeError('The objc.Block constructor expects a function'); - } - this.fn = fn; - this.returnType = coerceType(returnType); - this.argumentTypes = argumentTypes; +// base class for Blocks (this wraps a BlockType and a JS function, allowing that function to be used as an ObjC block) - this.skipBlockArgument = skipBlockArgument; - if (skipBlockArgument) { - this.argumentTypes.splice(0, 0, '@'); // 1st argument is the block itself +class Block { + #type; + #fn; + #callback; + #ptr; + + constructor(type, fn) { // external code should not call this constructor directly + this.#type = type; + if (typeof fn === 'function') { + let skipBlockArgument; + if (fn.length === type.argumentTypes.length) { + skipBlockArgument = false; + } else if (fn.length === type.argumentTypes.length - 1) { + skipBlockArgument = true; + } else { + throw new TypeError(`new objc.BLOCK_TYPE(...) expected a function with ${type.argumentTypes.length} parameters, got ${fn.length}`); + } + const callback = ffi.Callback(type.returnType, type.argumentTypes, function(blockPtr, ...args) { // eslint-disable-line new-cap + // Call the block implementation, by default skipping the 1st parameter (the block itself) + + // TO DO: as with ObjC methods, any inout args need additional processing, although this time it works in reverse, updating Ref.__ptr with the new (packed) Ref.value + + const retval = fn.apply(null, skipBlockArgument ? args : [blockPtr, ...args]); + + return retval; + }); + this.#fn = fn; + this.#callback = callback; + this.#ptr = new block_t({ + isa: _NSConcreteGlobalBlock, + flags: 1 << 29, + reserved: 0, + invoke: callback, + descriptor: descriptor.ref() + }).ref(); + + } else if (fn instanceof Buffer) { // assume it is a pointer to an ObjC block + this.#fn = () => { throw new Error(`Opaque Block cannot yet be called directly.`); } + this.#callback = null; + this.#ptr = fn; + + } else { + throw new TypeError(`new objc.blocks.TYPE(...) expected a function, got ${typeof fn}: ${fn}`); } - this.argumentTypes = this.argumentTypes.map(type => coerceType(type)); } + + get __callback__() { return this.#callback; } // kludge: see subclass.addMethods + + get type() { return this.#type; } // TO DO: ensure naming conventions are consistent across objc APIs + get fn() { return this.#fn; } // we could make also make Block directly callable, but it is probably best for it to appear to JS code as an explicit wrapper around a JS value (in this case, a function) + get ptr() { return this.#ptr; } +} + +/******************************************************************************/ +// base class for ObjC Block types - makeBlock() { - return block_t.new(structs.CompoundInit, { - isa: _NSConcreteGlobalBlock, - flags: 1 << 29, - reserved: 0, - invoke: this.getFunctionPointer(), - descriptor: descriptor.ref() - }).ref(); +class BlockType { // caution: as with ObjCClass, ObjCStructType, etc, users should not instantiate BlockType directly; call defineBlockType instead + + constructor(encoding) { + // encoding : string -- ObjC encoding string + // important: the encoding must include argument 0, which is the block itself, although the Block function can omit it if not needed (which it usually isn't) + this.objcEncoding = encoding; + this.argumentTypes = objctypes.coerceObjCType(encoding); + this.returnType = this.argumentTypes.shift(); // as with methods, first type is the return type; for ObjC blocks this is followed by '@', which is the block being passed to itself as its own first argument, then zero or more additional arguments passed by caller + this.argumentTypes[0] = pointerType; // in general we don't want to autobox the block ptr + this.size = ref.sizeof.pointer; + this.alignment = ref.alignof.pointer; + this.ffi_type = ffi.FFI_TYPES.pointer; + this.indirection = 1; } + + get(buffer, offset, type) { + throw new Error('TO DO: BlockType.get'); + } + + set(buffer, offset, value, type) { // pack an Block (e.g. passed as argument to an ObjC method) + let ptr; // get ObjC pointer to this block... + if (value instanceof Block) { + ptr = value.ptr; + } else if (typeof value === 'function') { + ptr = new Block(this, value).ptr; + } else if (value instanceof Buffer) { + ptr = value; + } else { + throw new TypeError(`Expected an Block, got ${typeof value}: ${value}`); + } + ref.writePointer(buffer, offset || 0, ptr); // ...and pack it into the buffer provided + } +} - getFunctionPointer() { - const self = this; - const callback = ffi.Callback(this.returnType, this.argumentTypes, function () { // eslint-disable-line new-cap - // Call the block implementation, skipping the 1st parameter (the block itself) - const retval = self.fn.apply(null, Array.from(arguments).slice(self.skipBlockArgument ? 1 : 0)); - if (retval === undefined) { - return null; - } +const _blockTypes = Object.create(null); // cache (type encoding strings must be kept in their own caches; for now, we also add human-readable names) - // Return the return value, unwrapping potential instance proxies - if (retval !== null && retval.___is_instance_proxy === true) { - return retval.__ptr; - } - return retval; - }); - return callback; +const defineBlockType = (encoding, ...names) => { + // define a new BlockType by encoding string (or return the existing BlockType if already defined) + // encoding : string -- ObjC type encoding string; important: this must include the block (@) as its first argument, although the callback function can omit it if not needed + // names : [string] -- zero or more convenience names under which to store the new BlockType + // Result: BlockType + if (!constants.isString(encoding)) { + throw new Error(`defineBlock expected an encoding string, got ${typeof encoding}: ${encoding}`); + } + let blockType = _blockTypes[encoding]; // look for an existing Block class, if already created + if (!blockType) { + const t = new BlockType(encoding); // create a ref-napi compatible type description + blockType = class extends Block { // create a subclass from which Block instances of this type can be instantiated + constructor(fn) { + super(t, fn); + } + }; + Object.defineProperty(blockType, 'name', {value: names[0] || 'AnonymousBlock', writable: false}); + _blockTypes[encoding] = blockType; // cache the class under its encoding string } + for (let name of names) { // alias it under any human-readable names + if (blockType !== _blockTypes[name]) { + if (_blockTypes[name] !== undefined) { + throw new Error(`Can't add BlockType alias named '${name}' as it already exists.`); + } + _blockTypes[name] = blockType; + } + } + return blockType; // return it so caller can start using it immediately +}; + + +function isBlockType() { + return value instanceof BlockType; } -module.exports = Block; +function isBlock(value) { + return value instanceof Block; +} + + +/******************************************************************************/ +// caution: there are circular dependencies between this module and objctypes, so do not write `module.exports = {...}` + +module.exports.Block = Block; // for internal use only + +module.exports.getBlockClassByName = (name) => _blockTypes[name]; // used by `objc` Proxy + +// public API; these are re-exported on `objc` +module.exports.defineBlockType = defineBlockType; +module.exports.isBlockType = isBlockType; +module.exports.isBlock = isBlock; + diff --git a/src/codecs.js b/src/codecs.js new file mode 100644 index 0000000..8368930 --- /dev/null +++ b/src/codecs.js @@ -0,0 +1,235 @@ +/* eslint-disable no-multi-assign */ + +// ns, js functions for converting the standard JS types to their NS equivalents and back + +// TO DO: bridge NSSet<->Set and NSDictionary<->Map + +// TO DO: should js() unpack NSDictionary to Map by default, with an optional flag to unpack as Object instead? + +'use strict'; + +const ffi = require('ffi-napi'); +const ref = require('ref-napi'); + +const constants = require('./constants'); +const runtime = require('./runtime'); +const Selector = require('./selector'); + +const pointerType = ref.refType(ref.types.void); + + +/******************************************************************************/ +// cache + + +let ObjCInstance, createMethodProxy; +let NSString, cptr_NSString, sel_stringWithUTF8String_, m_stringWithUTF8String_; +let NSNumber, cptr_NSNumber, sel_numberWithDouble_, m_numberWithDouble_; +let NSCFBoolean, nsTrue, nsFalse, iptr_true, iptr_false; +let NSDate, cptr_NSDate, sel_dateWithTimeIntervalSince1970_, m_dateWithTimeIntervalSince1970_; +let NSArray, NSMutableArray, cptr_NSMutableArray, sel_array, m_array, sel_addObject_, m_addObject_; +let NSDictionary, NSMutableDictionary, cptr_NSMutableDictionary, sel_dictionary, m_dictionary, + sel_setObject_forKey_, m_setObject_forKey_; + + +function initialize() { + const instance = require('./instance'); + ObjCInstance = instance.ObjCInstance; + createMethodProxy = instance.createMethodProxy; + + NSString = instance.getClassByName('NSString'); + cptr_NSString = NSString[constants.__objcClassPtr]; + sel_stringWithUTF8String_ = runtime.sel_getUid('stringWithUTF8String:'); + m_stringWithUTF8String_ = ffi.ForeignFunction(runtime.objc_msgSend, + pointerType, [pointerType, pointerType, ref.types.CString]); + + NSNumber = instance.getClassByName('NSNumber'); + cptr_NSNumber = NSNumber[constants.__objcClassPtr]; + sel_numberWithDouble_ = runtime.sel_getUid('numberWithDouble:'); + m_numberWithDouble_ = ffi.ForeignFunction(runtime.objc_msgSend, + pointerType, [pointerType, pointerType, ref.types.double]); + + // caution: because ObjCInstance overrides ObjC's own -class method, these will report nsTrue/nsFalse.class() as `NSNumber`, as opposed to its true class, which is __NSCFBoolean + NSCFBoolean = instance.getClassByName('__NSCFBoolean'); + nsTrue = NSCFBoolean.numberWithBool_(true); + nsFalse = NSCFBoolean.numberWithBool_(false); + iptr_true = nsTrue[constants.__objcInstancePtr]; + iptr_false = nsFalse[constants.__objcInstancePtr]; + + NSDate = instance.getClassByName('NSDate'); + cptr_NSDate = NSDate[constants.__objcClassPtr]; + sel_dateWithTimeIntervalSince1970_ = runtime.sel_getUid('dateWithTimeIntervalSince1970:'); + m_dateWithTimeIntervalSince1970_ = ffi.ForeignFunction(runtime.objc_msgSend, + pointerType, [pointerType, pointerType, ref.types.double]); + + NSArray = instance.getClassByName('NSArray'); + NSMutableArray = instance.getClassByName('NSMutableArray'); + cptr_NSMutableArray = NSMutableArray[constants.__objcClassPtr]; + sel_array = runtime.sel_getUid('array'); + m_array = ffi.ForeignFunction(runtime.objc_msgSend, pointerType, [pointerType, pointerType]); + sel_addObject_ = runtime.sel_getUid('addObject:'); + m_addObject_ = ffi.ForeignFunction(runtime.objc_msgSend, ref.types.void, [pointerType, pointerType, pointerType]); + + NSDictionary = instance.getClassByName('NSDictionary'); + NSMutableDictionary = instance.getClassByName('NSMutableDictionary'); + cptr_NSMutableDictionary = NSMutableDictionary[constants.__objcClassPtr]; + sel_dictionary = runtime.sel_getUid('dictionary'); + m_dictionary = ffi.ForeignFunction(runtime.objc_msgSend, pointerType, [pointerType, pointerType]); + sel_setObject_forKey_ = runtime.sel_getUid('setObject:forKey:'); + m_setObject_forKey_ = ffi.ForeignFunction(runtime.objc_msgSend, ref.types.void, [pointerType, pointerType, + pointerType, pointerType]); + + return module.exports; +} + + +/******************************************************************************/ +// handler functions for unconverted values + + +const jsReturnIfUnconverted = object => object; + + +const nsThrowIfUnconverted = (object) => { + const typename = typeof object === 'object' ? object.constructor.name : typeof object; + throw new TypeError(`objc.ns() cannot convert value: ${typename}`); +} + + +/******************************************************************************/ +// unpack + +const js = (object, resultIfUnconverted = jsReturnIfUnconverted) => { + // object : ObjCObject -- JS values are returned as is + // resultIfUnconverted : function | value -- if a function, call it and return its result; otherwise return it (default behavior is to return the ObjCObject unchanged) + let retvalue; + if (object === undefined) { + throw new TypeError('objc.js() expected a value but received: undefined'); + + } else if (object === null || object[constants.__objcObject] === undefined) { // return JS values as-is + retvalue = object; + + } else if (object.isKindOfClass_(NSString)) { + retvalue = object.UTF8String(); // eslint-disable-line new-cap + + } else if (object.isKindOfClass_(NSCFBoolean)) { // TO DO: this doesn't seem to be matching + retvalue = object.boolValue(); + + } else if (object.isKindOfClass_(NSNumber)) { // TO DO: check how this behaves + retvalue = object.doubleValue(); + + } else if (object.isKindOfClass_(NSDate)) { + retvalue = new Date(object.timeIntervalSince1970() * 1000); + + } else if (object.isKindOfClass_(NSArray)) { + retvalue = []; // TO DO: we could return a Proxy'd Array that encapsulates the ObjC NSArray and an initially lazy JS Array and lazily converts its items from NS to JS on first access (being a Proxy, it should still appear as instanceof Array). This might be quicker than immediately converting all items to JS, assuming the JS code only needs some of the values. Challenges: + // 1. ensuring the Proxy correctly implements all Array properties and methods; and + // 2. handling Array mutations when the NSArray is immutable (either by copying everything to the JS Array, or by converting the NSArray to NSMutableArray) + for (const obj of object) { + retvalue.push(js(obj, true)); // note that the JS Array conversion may be shallow (i.e. items within the NSArray will be converted to JS types if possible but any non-bridged items will remain as ObjCObjects) + } + + } else if (object.isKindOfClass_(NSDictionary)) { + retvalue = {}; + for (const key of object) { + retvalue[String(key)] = js(object.objectForKey_(key), true); // caution: mapping NSDictionary to Object is problematic, as NS-to-JS key conversions for numbers and other non-string values will be lossy and/or ambiguous, e.g. @"3" and @3 will be the same key in an Object (although will be different in a Map); it might be better to unpack as Map by default + } + + } else { + retvalue = typeof resultIfUnconverted === 'function' ? resultIfUnconverted(object) : resultIfUnconverted; + } + return retvalue; +}; + + +/******************************************************************************/ +// pack + +const ns = (object, resultIfUnconverted = nsThrowIfUnconverted, returnPtr = false) => { + // object : any + // nsThrowInUnconverted : function -- callback invoked if the given object is not bridged to ObjC + // returnPtr : boolean -- if false, result is a wrapped ObjCInstance or null; if true, result is a raw Buffer pointer (i.e. the wrapping step is skipped); this is used internally when recursively packing Array/Object items into an NSArray/NSDictionary + // Result: ObjCInstance | null -- the wrapped object; or raw Buffer if returnPtr is true + let classObject, ptr; + + // TO DO: can any of the following ObjC instantiations fail (i.e. return nil)? if so, add NULL ptr checks and throw a descriptive Error if the JS value failed to pack as an NS object + + if (object === undefined) { + throw new TypeError('objc.ns() expected a value but received: undefined'); + + } else if (object === null) { + return returnPtr ? ref.NULL : null; // returns a NULL pointer buffer if returnPtr is true, else null + + } else if (object[constants.__objcObject]) { + return returnPtr ? object[constants.__objcObject].ptr : object; + + } else if (constants.isString(object)) { // String -> NSString + classObject = NSString; + ptr = m_stringWithUTF8String_(cptr_NSString, sel_stringWithUTF8String_, object); + + } else if (constants.isNumber(object)) { // Number -> NSNumber + classObject = NSNumber; + ptr = m_numberWithDouble_(cptr_NSNumber, sel_numberWithDouble_, object); + + } else if (object === true) { + return returnPtr ? iptr_true : nsTrue; + + } else if (object === false) { + return returnPtr ? iptr_false : nsFalse; + + } else if (object instanceof Date) { // Date -> NSDate + const seconds = Number(object) / 1000; + classObject = NSDate; + ptr = m_dateWithTimeIntervalSince1970_(cptr_NSDate, sel_dateWithTimeIntervalSince1970_, seconds); + + } else if (Array.isArray(object)) { // Array -> NSArray + classObject = NSMutableArray; + ptr = m_array(cptr_NSMutableArray, sel_array); + try { + for (let item of object) { + m_addObject_(ptr, sel_addObject_, ns(item, nsThrowIfUnconverted, true)); + } + } catch (e) { + console.log('objc.ns had problem packing Array into NSArray: ', e) + if (e instanceof TypeError) { + return resultIfUnconverted === 'function' ? resultIfUnconverted(object) : resultIfUnconverted; + } else { + throw e; + } + } + + } else if (typeof object === 'object' && object.constructor.name === 'Object') { // Object -> NSDictionary + // note: the problem with accepting *any* JS object is anything more complex than simple key-value data is liable to cause data loss and/or packing errors (since, e.g. its methods won't pack), preventing it passing through the ObjC runtime and back to JS in working order; e.g. an object like `{a:1, b:true}` will roundtrip fine (and is obviously the intention here), but `new Foo(…)` won't; therefore 1. check that the object's constructor is "Object", 2. be specific in iterating property names, and 3. treat the entire object as unconverted if any of its values fail to pack + // + classObject = NSMutableDictionary; + ptr = m_array(cptr_NSMutableDictionary, sel_dictionary); + try { + // note: keys are not 100% roundtripped, as JS objects keys are limited to string/symbol whereas NSDictionary keys can be any hashable object, e.g. NSString, NSNumber + for (let key of Object.getOwnPropertyNames(object)) { + if (!constants.isString(key)) { throw new TypeError(`Non-string keys are not supported: '${String(key)}'`); } + m_setObject_forKey_(ptr, sel_setObject_forKey_, ns(object[key], nsThrowIfUnconverted, true), + m_stringWithUTF8String_(cptr_NSString, sel_stringWithUTF8String_, key)); + } + } catch (e) { + console.log('objc.ns had problem packing Object into NSDictionary: ', e) + if (e instanceof TypeError) { + return resultIfUnconverted === 'function' ? resultIfUnconverted(object) : resultIfUnconverted; + } else { + throw e; + } + } + + } else { + return typeof resultIfUnconverted === 'function' ? resultIfUnconverted(object) : resultIfUnconverted; + } + return returnPtr ? ptr : createMethodProxy(new ObjCInstance(classObject, ptr)); +}; + + +/******************************************************************************/ + + +module.exports.initialize = initialize; +module.exports.ns = ns; +module.exports.js = js; + diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..66a3b19 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,18 @@ + +// TO DO: is it worth defining one or more Error subclasses for objc-specific errors? + + +module.exports = { + + // use these functions to typecheck JS values + isBoolean: object => (typeof object === 'boolean' || object instanceof Boolean), + isNumber: object => (typeof object === 'number' || object instanceof Number), // note: this does not check isNaN(); this is intentional (NaN's type is still 'number', and it's part of the IEEE spec for FP numbers, so will still map to and from NSNumber which is all we're interested in) + isString: object => (typeof object === 'string' || object instanceof String), + + // keys for externally accessing method Proxy's internal objects; used throughout objc and may occasionally be needed by client code when working with C functions (which objc does not currently generate wrappers for) + __objcObject: Symbol('__objcObject'), // extract the ObjCObject from its method Proxy + __objcClassPtr: Symbol('__objcClassPtr'), // extract an ObjC Class pointer from its method Proxy + __objcInstancePtr: Symbol('__objcInstancePtr'), // extract an ObjC instance pointer from its method Proxy + + __objcType: Symbol('__objcType'), // ObjCStructType +}; diff --git a/src/create-class.js b/src/create-class.js deleted file mode 100644 index 86b81d5..0000000 --- a/src/create-class.js +++ /dev/null @@ -1,39 +0,0 @@ -const runtime = require('./runtime'); -const Block = require('./block'); -const {InstanceProxy} = require('./proxies'); -const Instance = require('./instance'); -const {_retainGlobal} = require('./util'); - -// TODO check whether `name` already exists, either throw an error or append a counter? -module.exports = (name, superclass, instanceMethods = {}, classMethods = {}) => { - // Assuming superclass is a string - superclass = runtime.objc_getClass(superclass); - - const classPtr = runtime.objc_allocateClassPair(superclass, name, 0); // TODO add ivar support? - - const addMethods = (dest, methods) => { - for (const name of Object.getOwnPropertyNames(methods).filter(n => n !== '_encodings')) { - const selector = runtime.sel_getUid(name); - const encoding = methods._encodings[name]; - - const [returnType, argumentTypes] = encoding; - const block = new Block(methods[name], returnType, argumentTypes, false); - - const imp = block.getFunctionPointer(); - _retainGlobal(imp); - - runtime.class_addMethod(dest, selector, imp, [].concat.apply([], encoding).join('')); - } - }; - - // Add instance methods - addMethods(classPtr, instanceMethods); - - runtime.objc_registerClassPair(classPtr); - - // Add class methods - addMethods(runtime.object_getClass(classPtr), classMethods); - - // Return a proxy wrapping the newly created class - return new InstanceProxy(new Instance(name)); -}; diff --git a/src/index.js b/src/index.js index 3077aee..ea28733 100644 --- a/src/index.js +++ b/src/index.js @@ -1,47 +1,155 @@ +// objc + +// TO DO: `console.log(objc)` returns recursive inspection of the Proxy's target object, which is probably not what we (or users) want; however, [util.inspect.custom] doesn't seem to work regardless of where we put it (I think, ideally, util.inspect should, by default, list the names (and maybe types) of builtins, followed by names of any already-bound ObjC classes and constants); with the option to pass a flag in options object to get a full (default) list +// +// for now, we limit inspection string for objc.__internal__ (which most users shouldn’t need) to "[object objc.__internal__]", keeping `console.log(objc)` representation reasonably clean + +const util = require('util'); + +const constants = require('./constants'); + const runtime = require('./runtime'); -const Instance = require('./instance'); -const Block = require('./block'); +const instance = require('./instance'); +const objctypes = require('./objctypes'); + +const Ref = require('./reference'); const Selector = require('./selector'); -const swizzle = require('./swizzle'); -const createClass = require('./create-class'); -const {InstanceProxy, MethodProxy} = require('./proxies'); -const {js, ns, importFramework} = require('./util'); -const {defineStruct} = require('./structs'); -const types = require('./types'); - -const builtins = { - types, - runtime, - Instance, - InstanceProxy, - MethodProxy, - Block, - Selector, - swizzle, - createClass, - import: importFramework, - js, - ns, - defineStruct, - wrap: obj => new InstanceProxy(new Instance(obj)), - allocRef: Instance.alloc, - isNull: Instance.isNull + +const struct = require('./struct'); +const block = require('./block'); + +const codecs = require('./codecs').initialize(); // note: './instance' MUST be fully imported before initializing codecs + +const subclass = require('./subclass'); // for subclassing ObjC classes in JS (this needs reworking) + +// instance is exported as objc.__internal__ so add any 'advanced users only' functionality to that +instance.swizzle = require('./swizzle').swizzle; +instance.types = objctypes; + + +/******************************************************************************/ +// import [Obj-]C frameworks + +const importFramework = name => { + // name : string -- framework's name (e.g. "AppKit") or absolute path ("/PATH/TO/NAME.framework") + // Result: boolean -- was the framework successfully loaded? + if (['Foundation', 'CoreFoundation'].includes(name)) { return true; } // TO DO: this is a kludge (since these frameworks should already be loaded); however, if client tries to [re]import Foundation/CF it seems to break subsequent imports of AppKit and other frameworks (the bundle.load() call still returns true, but AppKit etc classes, e.g. `objc.NSWorkspace`, can't be found); why? + const path = name.includes('/') ? name : `/System/Library/Frameworks/${name}.framework`; + const bundle = instance.getClassByName('NSBundle').bundleWithPath_(path); + if (!bundle) { + throw new Error(`Unable to find bundle named '${name}'`); + } + if (!bundle.load()) { // TO DO: it would be better to use loadAndReturnError:(NSError**), to provide better error messages + throw new Error(`Unable to load bundle named '${name}'`); + } + return bundle; // think we should return the bundle itself, in case callers want to load resource files from the bundle as well }; -module.exports = new Proxy({}, { - get: (_, key) => { - if (Object.prototype.hasOwnProperty.call(builtins, key)) { - return builtins[key]; - } - if (runtime.classExists(key)) { - return builtins.wrap(key); - } +/******************************************************************************/ +// predefined ObjC types + +block.defineBlockType('q@@@', 'NSComparator'); + +struct.defineStructType('{CGPoint="x"d"y"d}', 'NSPoint'); +struct.defineStructType('{CGSize="width"d"height"d}', 'NSSize'); +struct.defineStructType('{CGRect="origin"{CGPoint}"size"{CGSize}}', 'NSRect'); +struct.defineStructType('{_NSRange="location"Q"length"Q}', 'NSRange'); + + +/******************************************************************************/ +// built-in `objc` objects and functions - // Not a class, see if we can find a constant with that name - const symbol = runtime.getSymbolAsId(key); - if (symbol !== null) return builtins.wrap(symbol).UTF8String(); // eslint-disable-line curly, new-cap +const NSAutoreleasePool = instance.getClassByName('NSAutoreleasePool'); - throw new Error(`Unable to find symbol '${key}'`); +const _builtins = Object.create(null); + +// import frameworks +_builtins.import = importFramework; + +// explicitly convert values between ObjC and JS types +_builtins.js = codecs.js; +_builtins.ns = codecs.ns; + +// type checking (note: users should use these functions to identify ObjC objects, not `instanceof` which won't check for method Proxy wrapper) +_builtins.isObject = instance.isWrappedObjCObject; +_builtins.isClass = instance.isWrappedObjCClass; +_builtins.isInstance = instance.isWrappedObjCInstance; + +// creating ObjC-specific types + +_builtins.Ref = Ref; +_builtins.Selector = Selector; + +_builtins.defineBlock = block.defineBlockType; +_builtins.isBlockType = block.isBlockType; +_builtins.isBlock = block.isBlock; + +_builtins.defineStruct = struct.defineStructType; +_builtins.isStructType = struct.isStructType; +_builtins.isStruct = struct.isStruct; + +_builtins.defineClass = subclass.defineClass; + +_builtins.auto = (fn, ...args) => { + const pool = NSAutoreleasePool.alloc().init(); + try { + return fn(...args); + } finally { + pool.drain(); } +}; + +// TO DO: defineFunction(ENCODING,NAME) for wrapping C functions, e.g. NSStringFromRect + + +_builtins[util.inspect.custom] = (depth, inspectOptions, inspect) => { // called by console.log(objc) + return '[object objc]'; // TO DO: opaque/shallow/deep inspection options (bear in mind that inspecting the `objc` object will only show hardcoded members, and maybe already-imported objects if we make it smart enough to list those too) +}; + +// allow access to internal APIs if users need to work directly with ObjC pointers (e.g. when passing NS objects to/from CF APIs and vice-versa) +_builtins.__internal__ = instance; + + +// top-level `objc` object is a Proxy that performs lazy instantiation and lookup of ObjC classes in this namespace + +module.exports = new Proxy(_builtins, { + get: (builtins, key) => { + let retval = builtins[key]; + if (retval === undefined) { + if (constants.isString(key)) { + + retval = instance.getClassByName(key) + ?? struct.getStructTypeByName(key) + ?? block.getBlockClassByName(key); // caution: this doesn't check for or prevent name masking, e.g. given a class named 'Foo' and a struct also named 'Foo', the class definition is returned first and the struct definition is inaccessible; TO DO: ideally there should be a single cache shared by all wrapper types (ObjC class, ObjC block, C struct, C function), so that any conflicting names can be detected + + if (retval === undefined) { // not a class or struct/block type, so see if we can find a constant with that name + // note: this only works for [e.g.] NSString constants; primitive constants are typically defined in header, not object file, so aren't runtime-accessible (at least, not without bridgesupport XML), e.g.: + // + // console.log(objc.NSAppleScriptErrorAppName) // this works (the constant is an NSString) + // console.log(objc.NSUTF8StringEncoding) // this doesn't (the constant is a C int defined in .h file) + // + retval = instance.getObjCSymbolByName(key); + if (!retval) { throw new Error(`Not found: 'objc.${key}'`); } + builtins[key] = retval; + } + } else { // key is Symbol + switch (key) { + case Symbol.toPrimitive: + retval = (hint) => { + return hint === 'number' ? Number.NaN : '[object objc]'; + } + break; + default: + throw new Error(`Not found: 'objc[${String(key)}]'`); + } + } + } + return retval; + }, + + set: (_, key, value) => { + throw new Error(`Can't set 'objc.${key}'`); + }, + }); diff --git a/src/instance.js b/src/instance.js index c90741a..b6c659f 100644 --- a/src/instance.js +++ b/src/instance.js @@ -1,191 +1,491 @@ +// this module defines: +// +// - ObjCClass and ObjCInstance, which wrap raw ObjC class and instance pointers +// +// - wrapMethod, which wraps an ObjC method's pointer in a closure that invokes the method via objc_msgSend(), converting argument and return values to/from ObjC types according to the method's introspected type encodings +// +// - method Proxy, which wraps ObjCClass and ObjCInstance so JS can access their methods by name + + +// TO DO: wrap NSArray (and NSDictionary?) instances in custom Proxy objects that implement standard JS Array/Object APIs in addition to ObjC instance methods? this would allow a degree of polymorphism, reducing need to explicitly unpack (with added caveat that proxies around mutable NS objects probably can't cache unpacked items as the NS object's contents may change at any time) + +// TO DO: better error messages in callObjCMethod when arguments fail to pack + +// TO DO: how to ensure correct retain/release of ObjC instance ptrs? (up to now we've not bothered with autorelease pools, leaving ObjC objects to leak memory, but when AR pools are used to clean up we need to ensure the ObjC runtime doesn't dealloc objects while we still hold Buffer pointers to them); see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry; another option (if we move guts into C++ will be to implement ObjCObject in C++ as V8's API presumably provides GC hooks for deallocing [C/C++/ObjC] memory) + + +const util = require('util'); + +const ffi = require('ffi-napi'); const ref = require('ref-napi'); + +const constants = require('./constants'); const runtime = require('./runtime'); -const Selector = require('./selector'); -const {InstanceProxy, _getUnderlyingObject} = require('./proxies'); -const Block = require('./block'); -const {coerceType} = require('./type-encodings'); -const util = require('util'); +const codecs = require('./codecs'); // used in toPrimitive +const Ref = require('./reference'); +const objctypes = require('./objctypes'); -let ns; -const inoutType = ref.refType(ref.refType(ref.types.void)); - -class Instance { - constructor(args) { - if (args === undefined || args === null || !['string', 'object'].includes(typeof args)) { - throw new TypeError('Invalid arguments passed to the constructor'); - } else if (typeof args === 'string') { - this.type = 'class'; - this.__ptr = runtime.objc_getClass(args); - } else { - // TODO check whether the object is a class or an instance. object_isClass or something like that - this.type = 'instance'; - this.__ptr = args; - } - } - respondsToSelector(selector) { - return !this.methodForSelector(selector).isNull(); - } +/******************************************************************************/ +// ObjCClass cache - methodForSelector(selector) { - if (typeof selector === 'string') { - selector = new Selector(selector); - } - if (this.type === 'class') { - return runtime.class_getClassMethod(this.__ptr, selector.__ptr); +// an ObjCClass object is created for each ObjC class that's used, wrapped in a Proxy object that provides access to the ObjC class's methods, and cached for reuse (i.e. always use the accessors below); always use these functions to create class wrappers + +// the main reason for separating ObjCClass vs ObjCInstance is so that ObjCClasses can cache all of the (class+instance) method wrappers for that class, as each of those method wrappers is crazy expensive to create; this in turn means that ObjCInstances must keep a reference to their ObjCClass (while ObjCInstance really only needs the instance method cache, it's simplest just to grab the whole thing) + +// caution: only ObjCClass objects are cached; ObjCInstance wrappers are always created anew, thus it's possible to have multiple ObjCInstances which share the same pointer; i.e. whereas in ObjC a simple `a == b` pointer comparison is sufficient to determine object identity, JS's `===` can't be used to determine if two ObjCInstances are the same ObjC object; // TO DO: provide dedicated `objc.isSameObject(a,b)` that compares the underlying pointers + + +// same values, just different lookup keys (`objc.NAME` vs a method wrapper when processing returned ptrs) +const _cachedClassesByName = {}, _cachedClassesByPtr = {}; + + +const getClassByName = name => { + // used by `objc` proxy to look up classes, e.g. `objc.NSString` + // name : str -- the name of an ObjC class (this assumes the framework which defines the class is already imported) + // Result: object | undefined -- an ObjCClass wrapped in its method proxy, or undefined if not found + let obj = _cachedClassesByName[name]; + if (obj === undefined) { + const ptr = runtime.objc_getClass(name); + if (!ptr.isNull()) { + obj = wrapClass(ptr); + _cachedClassesByName[name] = obj; + _cachedClassesByPtr[ptr] = obj; } - return runtime.class_getInstanceMethod(runtime.object_getClass(this.__ptr), selector.__ptr); } + return obj; +} - call(selector, ...argv) { - if (typeof selector === 'string') { - selector = new Selector(selector); +const getClassByPtr = ptr => { + // used by method proxies to look up the ObjCClass when wrapping a return value as an ObjCInstance + // ptr : pointer -- an ObjC class + // Result: object | undefined -- an ObjCClass wrapped in its method proxy, or undefined if not found + let obj = _cachedClassesByPtr[ptr]; + if (obj === undefined) { + if (!ptr.isNull()) { + let name = runtime.class_getName(ptr); + obj = wrapClass(ptr); + _cachedClassesByName[name] = obj; + _cachedClassesByPtr[ptr] = obj; } + } + return obj; +} - if (selector.name.includes('_')) { - for (const permutation of selector.permutations()) { - if (this.respondsToSelector(permutation)) { - selector = permutation; - break; - } - } - } - const method = this.methodForSelector(selector); +/******************************************************************************/ +// ObjC class, instance, and method wrappers - if (typeof method === 'undefined' || method.isNull()) { - throw new Error(`Unable to find method ${selector.name} on object ${this.description()}`); - } - const expectedNumberOfArguments = runtime.method_getNumberOfArguments(method); +const wrapClass = ptr => { + // used by getClassByName, getClassByPtr; given an ObjC Class (pointer), wrap it in ObjCClass and method Proxy + // ptr : pointer -- the underlying ObjC class to be wrapped + // Result: Proxy(ObjCClass) + // caution: this only creates the JS wrapper for the ObjC class and does not add it to objc's cache; external callers should use getClassByPtr(ptr), which performs both + return createMethodProxy(new ObjCClass(ptr)); +} - const argumentTypes = [...Array(expectedNumberOfArguments).keys()].map(i => { - return coerceType(runtime.method_copyArgumentType(method, i)); - }); +const wrapInstance = ptr => { + // ptr : pointer -- the underlying ObjC instance to be wrapped + // Result: Proxy(ObjCInstance) + return createMethodProxy(new ObjCInstance(getClassByPtr(runtime.object_getClass(ptr)), ptr)); +} - const returnTypeEncoding = runtime.method_copyReturnType(method); - const inoutArgs = []; // Indices of inout args (ie `NSError **`) +// DEBUG: performance test +let _totaltime = process.hrtime.bigint(); +let _zero = _totaltime - _totaltime; +_totaltime = _zero; - const args = argv.map((arg, idx) => { - idx += 2; - if (arg instanceof Block) { - return arg.makeBlock(); - } - if (arg === null) { - return arg; +const wrapMethod = (objcObject, methodDefinition) => { + // create a method wrapper bound to a ObjCClass/ObjCInstance + // objcObject : ObjCObject -- the ObjCClass/ObjCInstance to which this method belongs + // methodDefinition : object -- see introspectMethod + // Result: function -- the "method" function that the client code will call, e.g. `NSString.stringWithString_` + // + // for efficiency, returned NSString, NSArray, etc are not automatically converted back to JS values, as we have know way of knowing if caller will pass them to another ObjC method, or wants to use them in JS now (if caller specifically wants a JS value back, they need to wrap the method call in a js() function, and likewise apply js() to any out arguments) + // + // TO DO: this doesn't support varargs + // + // TO DO: somewhat annoyingly, the callObjCMethod function shows as "Proxy.callObjCMethod" in Error call stacks; ideally we want it to show as "NSFoo.bar_baz_" (note also that the Proxy appears to become the function's `this` upon its return from Proxy's `get` handler, which is just typical JS) + // + return function callObjCMethod(...args) { // ideally closure would be named after methodDefinition.methodName, but that's tricky; TO DO: we probably could attach a custom inspect that will display method name and argument types + //console.log('>>callObjCMethod: '+methodDefinition.selector.name) + try { + let t = process.hrtime.bigint(); // DEBUG: performance test + let retval = methodDefinition.msgSend(objcObject.ptr, methodDefinition.sel, ...args); + _totaltime += process.hrtime.bigint() - t; // DEBUG: performance test + + // update any inout/out arguments (ObjCRefType.set attached before and after pointers and type info) + if (methodDefinition.inoutIndexes) { + for (let i of methodDefinition.inoutIndexes) { + const box = args[i]; + if (box instanceof Ref && !box.__outptr.equals(box.__inptr)) { + // box.__outptr.readPointer().isNull() // TO DO: do we need a NULL check here? + box.value = box.ffi_type.reftype.get(box.__outptr, 0, box.ffi_type.reftype); + } + } + } + //console.log('pack res: '+methodName+': '+(Number(process.hrtime.bigint() - t)/1e6)) + //console.log('...callObjCMethod returning: '+ isWrappedObjCInstance(retval)); + return retval; + } catch (e) { + + // TO DO: should this clear any __inptr, __outptr, __reftype values on Ref arguments? + + if (args.length != methodDefinition.argc - 2) { // TO DO: this doesn't support varargs + e = new Error(`Expected ${methodDefinition.argc - 2} arguments, got ${args.length}`); } + // TO DO: catch known errors and throw a new 'ObjCMethodError' error so the stack trace ends here (bugs should still throw stack all the way to source, so should modify existing Error.message and rethrow) + if (!(e instanceof Error)) { e = new Error(String(e)); } + // kludge: massage 'bad argument' error message to make it easier to understand + let msg = e.message.replace(/^error setting argument (\d+)/i, (m, n) => `argument ${n-2}`); // adjust arg no. + const argTypes = args.map((o) => `[${typeof o === 'object' ? o.constructor.name : typeof o}]`).join(', '); + const enc = methodDefinition.encoding.replace(/[0-9]/g, ''); + e.message = `${objcObject.name}.${methodDefinition.methodName} expected '${enc}', got (${argTypes}): ${msg}}`; + throw e; + } + }; +}; - const expectedArgumentType = runtime.method_copyArgumentType(method, idx); - if (arg !== null && typeof arg === 'object' && typeof arg.__ptr !== 'undefined') { - if (expectedArgumentType === '^@') { - inoutArgs.push(idx); - } - return arg.__ptr; - } +/******************************************************************************/ +// ObjC class and instance wrappers +// these wrap ObjC class/instance objects and are, in turn, wrapped by Proxies that provide access to their ObjC methods - // If the method expects id, SEL or Class, we convert `arg` to the expected type and return the pointer - if (['@', ':', '#'].includes(expectedArgumentType)) { - // We have to delay requiring ./util until here to work around the circular dependency (util also requires Instance) - ns = ns || require('./util').ns; - const _obj = ns(arg, expectedArgumentType); - return _obj === null ? null : _obj.__ptr; - } - return arg; - }); +// TO DO: should ObjCInstance.tojs() include ptr's hex address, to distinguish one instance from another? - const returnType = coerceType(returnTypeEncoding); - const msgSend = runtime.msgSend(returnType, argumentTypes); - let retval; +// abstract base class; wraps a pointer to an ObjC class or instance - // Doing the exception handling here is somewhat useless, since ffi-napi removed objc support - // (see https://github.com/node-ffi-napi/node-ffi-napi/issues/4 and https://github.com/node-ffi-napi/node-ffi-napi/commit/ee782d8510003fef67b181836bd089aae1e41f84) - // Keeping it in though in case they ever bring that back - try { - retval = msgSend(this.__ptr, selector.__ptr, ...args); - } catch (err) { - if (err instanceof Error) { - throw err; - } - const exc = new InstanceProxy(new Instance(err)); - throw new Error(`${exc.name()} ${exc.reason()}`); - } +class ObjCObject { + #__ptr; - inoutArgs.forEach(idx => { - idx -= 2; // Skip `self` and `_cmd` - argv[idx].__ptr = argv[idx].__ptr.deref(); - }); + constructor(ptr) { + this.#__ptr = ptr; + } + + get ptr() { // read-only ptr property is nominally public (being used within objc, and needed when e.g. using NSObjects in CoreFoundation APIs via toll-free bridging) + return this.#__ptr; + } + + get [Symbol.toStringTag]() { + return `${this.constructor.name}=${this.name}`; + } + + [Symbol.toString]() { // TO DO: this returns ObjC's description string, which might not be what we want, but let's see how it goes for now + return this.__callObjCMethod__('description').UTF8String(); + } + + // TO DO: what else needs [re]implemented on ObjCObjects and/or their method Proxy wrapper? + + // note that there is no need to reimplement the old `Instance.respondsToSelector()` as the user can test for any method's existence just by asking for it by name and trapping the resulting 'method not found' error (which should really be its own MethodNotFoundError subclass of Error, along with other objc errors); plus, of course, the NSObject protocol already guarantees native ObjC `respondsToSelector:` methods on all classes and instances + + [util.inspect.custom]() { + // console.log('inspecting ObjCObject (constructor='+this.constructor.name+')'); // caution: you'd think the constructor here would be ObjCClass/ObjCInstance, but no: it's Object, the Proxy wrapper which is now bound to `this` + // TO DO: annoyingly, when util.inspect() is passed the method Proxy, the Proxy's 'get' handler does not intercept the util.inspect.custom key lookup but instead passes it directly to the ObjCObject; therefore, to return a custom inspection string, we have to implement the util.inspect.custom behavior here. But wait, there's more! Just to further confuse, when this inspection method is invoked, its `this` is actually the Proxy, not the ObjCObject, so we have to go back through the Proxy again in order to call the method we actually want, which is `ObjCObject.__callObjCMethod__`, which calls `[NSObject description]` to get the object's ObjC description string. + // + // ...and then sometimes it doesn't, as `this` appears to change identity between the Proxy wrapper and the ObjCObject depending on who is calling this method from where (calling through the method wrapper, the Proxy becomes the proxied object's `this` [counterituitive, but probably a special Proxy behavior that's intended]; whereas displaying the returned object in the REPL, `this` is the ObjCObject itself [what would normally be expected]). + // + // note: since ObjC descriptions for NSArray, NSDescription, etc traditionally extend over multiple lines, this collapses all whitespace into single spaces to improve density and maintain consistency with JS's traditional bracket notation (toString still returns the original representation) + return `[objc ${this.__callObjCMethod__('description').UTF8String().replace(/\s+/g, ' ')}]`; + } + + __callObjCMethod__(name, ...args) { + // an unwrapped ObjCObject can use this to call an ObjC method on itself (caution: external code should always use the method Proxy wrapper, not call __callObjCMethod__ directly) + // name : string -- the method’s JS name, e.g. 'foo_bar_' + // args : any -- any arguments to pass to the method + // Result: ObjCClass | ObjCInstance | null -- a method Proxy-wrapped ObjC object, or null (nil) (note: ObjC objects are not automatically converted back to JS types) + return (this.cachedMethods[name] ?? this.bindMethod(name))(...args); + } +} - if (retval instanceof Buffer && retval.isNull()) { - return null; - } - if (returnTypeEncoding === '@') { - return InstanceProxy(new Instance(retval)); // eslint-disable-line new-cap - } else if (returnTypeEncoding === 'B') { - // This won't catch all booleans, since on x86_64 bools are encoded as 'c' - // (which we can't just flat-out all convert to Booleans), - // but C/C++ bools and ObjC BOOLs on ARM are encoded as 'B', so we should handle at least these properly... - return Boolean(retval); +// concrete classes representing ObjC objects of types `Class` and `id` + +class ObjCClass extends ObjCObject { + + constructor(ptr) { + // ptr : pointer -- the ObjC class; caution: this pointer must be non-NULL but this is not checked + super(ptr); + // cache the wrapped ObjC class methods (also includes internal keys for extracting the instance's C pointer) + this.cachedMethods = { + [constants.__objcClassPtr]: ptr, + [constants.__objcInstancePtr]: null, // not sure about this: on one hand, Class is a subtype of id (so there's an argument for returning ptr here); on other, we use these properties to determine if a given ObjCObject is a class or an instance (so there's also an argument for returning null) + }; + this.instanceMethodDefinitions = {}; // cache the parsed type encodings for this class's instance methods + this.__name = runtime.class_getName(ptr); + } + + get name() { + // Result: string -- display name (class name) + return this.__name; + } + + tojs(hint = 'default') { + // called by Proxy wrapper's toPrimitive function + // hint : 'number' | 'string' | 'default' + // Result: number | string + return hint === 'number' ? Number.NaN : `[ObjCClass: ${this.name}]`; // TO DO: how to represent a class? a parenthesized literal expression might be best, e.g. `(objc.NSString)`, as it is both self-descriptive and can-be copy+pasted+evaluated to recreate it; however, that doesn't work so well with instances, where we'd need to replicate the constructor and arguments as literals as well (we might eventually do that for the bridged Foundation types, but anything else is probably best using the ObjC description string) + } + + objcMethodPtr(sel) { // looks up and returns the C pointer to the specified class method; used by introspectMethod + return runtime.class_getClassMethod(this.ptr, sel); + } + + bindMethod(methodName) { + // create a wrapper for the named ObjC class method that's bound to this ObjClass object, caching it for reuse + // methodName : string -- JS-style method name + // Result: function + // note: this is called by method Proxy wrapper, once for each class method used + let methodDefinition = objctypes.introspectMethod(this, methodName); + let method = wrapMethod(this, methodDefinition); + this.cachedMethods[methodName] = method; + return method; + } + + bindInstanceMethod(instanceObject, methodName) { // this is called once for each instance method + // instanceObject : ObjCObject -- unwrapped + // methodName : string + let methodDefinition = this.instanceMethodDefinitions[methodName]; + if (!methodDefinition) { + methodDefinition = objctypes.introspectMethod(instanceObject, methodName); + this.instanceMethodDefinitions[methodName] = methodDefinition; } - return retval; + return wrapMethod(instanceObject, methodDefinition); } +} - description() { - if (this.__ptr === null || this.__ptr.isNull()) { - return '(null)'; - } - if (this.respondsToSelector('description')) { - return this.call('description').UTF8String(); // eslint-disable-line new-cap - } else if (this.respondsToSelector('debugDescription')) { - return this.call('debugDescription').UTF8String(); // eslint-disable-line new-cap - } else if (this.respondsToSelector('class')) { - let classname = runtime.class_getName(this.call('class')); - return `<${classname} ${this.__ptr}>`; - } else if (this.type === 'instance') { - let classname; - if (this.type === 'instance') { - classname = runtime.class_getName(runtime.object_getClass(this.__ptr)); - } else { - classname = runtime.class_getName(this.__ptr); - } - return `<${classname} ${this.__ptr}>`; - } else { - return `objc.Instance(type: ${this.type}, __ptr: ${this.__ptr})`; + +class ObjCInstance extends ObjCObject { + + constructor(classObject, ptr) { + // class:Object : Proxy(ObjCClass) -- the wrapped ObjC class of which this is an instance + // ptr : pointer -- the ObjC instance; caution: this pointer must be non-NULL but this is not checked + super(ptr); + this.classObject = classObject; + // cache the wrapped ObjC instance methods (also includes internal keys for extracting the instance's C pointer) + this.cachedMethods = { + 'class': () => this.classObject, // TO DO: is it good/safe/wise to override the ObjC object's own -class method like this? yes, the caller will want the proxied ObjCClass, but what if ObjC runtime does swizzling (and what about class clusters)? e.g. if true class is __NSCFBoolean, what does -[NSNumber class] return? + [constants.__objcClassPtr]: null, + [constants.__objcInstancePtr]: ptr, + }; + } + + get name() { + // Result: string -- display name (for now this is class's name) + return runtime.object_getClassName(this.ptr); + } + + tojs(hint = 'default') { + // called by Proxy wrapper's toPrimitive function + // hint : 'number' | 'string' | 'default' + // Result: number | string + switch (hint) { + case 'number': + return Number.NaN; // TO DO: should handle NSNumber + case 'string': + return this[Symbol.toString](); // ObjC description string // for now, this invokes [NSObject description] and returns that ObjC-style string, though we might want to adjust that later to bring it more in line with JS's toPrimitive + default: + return this[Symbol.toString](); // TO DO: need exception for NSNumber (not __NSCFBoolean) and NSArray containing a single NSNumber (since JS seems to like reducing `[3]` to `3`); also, should we allow this to return undefined/null, so Proxy can use it for valueOf too? (on returning undefined, the Proxy would return the proxyObject unchanged, which I believe is ther expected behavior for valueOf when it encounters JS objects that can't be reduced to primitives without data loss, whereas toPrimitive always forces objects to string or number and damn all the data loss) } } - - toString() { - return this.description(); + + objcMethodPtr(sel) { + // looks up and returns the C pointer to the specified instance method; used by bindMethod() + // sel : Buffer -- pointer to ObjC selector + // Result : Buffer -- pointer to ObjC method; may be NULL + return runtime.class_getInstanceMethod(runtime.object_getClass(this.ptr), sel); } - - [util.inspect.custom](depth, options) { - const self = util.types.isProxy(this) ? this[_getUnderlyingObject] : this; - //return `[objc.Instance<${self.class()}> ${self.description()}]`; - return `[objc.Instance ${self.description()}]`; + + bindMethod(methodName) { + // create a wrapper for the named ObjC class method that's bound to this ObjInstance object, caching it for reuse + // methodName : string -- JS-style method name + // Result: function + let method = this.classObject[constants.__objcObject].bindInstanceMethod(this, methodName); + this.cachedMethods[methodName] = method; + return method; } +} - get [Symbol.toStringTag]() { - return 'objc.Instance'; - } - class() { - return runtime.class_getName(this.type === 'class' ? this.__ptr : this.call('class')); - } +/******************************************************************************/ +// wraps ObjCObjects in a Proxy object that gives JS access to the underlying ObjC methods +// this lazily generates function wrappers for ObjC methods the first time they’re called by client code +// (creating ObjC method wrappers is expensive as libobjc's introspection functions are sloooow) +// note that this wrapper is largely transparent to JS, so e.g. `obj instanceof ObjCInstance` will still return true if obj is a method proxy wrapper around an ObjCInstance (which is what we want) + +// TO DO: is there a way to rename Proxy to MethodProxy? (we don't really want to call the function MethodProxy as it's not a `new`-based constructor, even though it constructs an object, and there's good reason for es-lint wanting to associate CapNames with `new` constructors for visual consistency [ignoring entrenched warts such as `Date()`]) + +// TO DO: to what extent can NSNumber, NSString, NSDate, NSArray, NSDictionary ObjCInstances be made interchangeable with corresponding JS types (boolean/Number, String, Date, Array, object)? e.g. can `nsString + toDescription`, `toDescription + nsString`, and `nsString + nsString` all be made to work the same as `toDescription + toDescription`, or are we going to hit a wall with JS's cranky type system (i.e. while JS's string primitives are non-objects, it also provides String objects, so if we can masquerade an NSString as one of those - and likewise for Date, Array, and object (NSNumbers are not as common so we could live with the discrepancy there, although toPrimitive should reduce it) - then this reduces the need for user to call obj.js()) - static alloc() { - // TODO is this zero-initialised? (probably will otherwise try to fuck up rhe refcount, right?) - return new InstanceProxy(new Instance(ref.alloc(inoutType))); + + +const createMethodProxy = obj => { + // wrap an ObjCObject in a Proxy that converts property lookups to ObjC method lookups, e.g. `foo.some_method_(1,2)` will return an ObjC method wrapped in a callObjCMethod function so that when that function is called the underlying ObjC method is invoked, `[foo some:1 method:2]` + // obj : ObjCObject -- the ObjC class/instance wrapper + // Result : Proxy(ObjCObject) + // + // important: to access the underlying ObjCObject from outside the Proxy, use `foo[constants.__objcObject]` (where `foo` is a Proxy-wrapped ObjCObject, e.g. the `objc.NSString` class, or an instance of that class) + // + // important: to access the underlying ObjC pointer, use `foo[constants.__objcObject].ptr`, `foo[constants.__objcClassPtr]`, or `foo[constants.__objcInstancePtr]` + // + // As a rule, client code should not need to access the underlying ObjCObject or ObjC pointer, except when working with ffi directly, e.g. when calling a C function that takes a `Class` or `id` argument (i.e. the underlying ObjC pointer); it is also possible to call CoreFoundation functions passing a toll-free-bridged ObjC pointer where a CFTYPERef is expected, caveat ownership remains with ObjC's ARC (transferring ObjC object ownership to/from CF's manual refcounting is left as an exercise to brave/expert/foolhardy readers) + // + if (!(obj instanceof ObjCObject)) { + throw new Error(`createMethodProxy expected an ObjCObject but received ${typeof obj}: ${String(obj)}`); } + return new Proxy(obj, { // note: no way to customize the Proxy's name, unfortunately, or we'd name it MethodProxy so that it's easy to identify in stack traces + get: function(objcObject, methodName, proxyObject) { + + // console.log(`method Proxy is looking up ${typeof methodName}: ${String(methodName)}…`); + + // 1. see if there's already a method wrapper by this name (this also handles __objcClassPtr/__objcInstancePtr) + let method = objcObject.cachedMethods[methodName]; + + if (method === undefined) { + // 2. next, check if it's a builtin (mostly Symbol-based keys) + switch (methodName) { + case constants.__objcObject: + return objcObject; + + case '__callObjCMethod__': // TO DO: make this a Symbol? or can we live with one "magic method" exposed as string? (fairly sure ObjC doesn't use double underscores in any method names; and if there was an ObjC method named `_call_` then it’d have zero extra arguments whereas this has at least one, so any collision will soon be resolved by something barfing an error) + return (name, ...args) => objcObject.__callObjCMethod__(name, ...args); + + // inspection/coercion + + case util.inspect.custom: + return (depth, options) => { + /* + TO DO: return a formatted string representation of object's structure; see: + + https://nodejs.org/dist./v8.14.1/docs/api/util.html#util_util_inspect_object_options + + may want to call util.inspect(objcObject), allowing it to show more details (e.g. its ObjC description) + */ + return `[${proxyObject[Symbol.toStringTag]}]`; + } + + // case Symbol.valueOf: // TO DO: IIRC, valueOf should return objcObject as a JS primitive (e.g. string, number, array) if a lossless conversion is available, otherwise it should return objcObject as-is + + case Symbol.toString: + return () => { + objcObject.toPrimitive('string'); // quick-n-dirty + } + case Symbol.toStringTag: // describes the ObjC object's construction, including presence of method Proxy wrapper; JS will call this when generating its '[object TAG]' representation + return `Wrapped-${objcObject[Symbol.toStringTag]}`; // TO DO: how best to indicate wrapper's addition? parens/brackets/braces/'wrapped-'? + case Symbol.toPrimitive: + return (hint => { + // hint : 'number' | 'string' | 'default' + // Result: number | string + if (objcObject instanceof ObjCClass) { + return objcObject.tojs(hint); + } + let obj = codecs.js(proxyObject); // return as-is if not converted to JS primitive + // first, deal with any types that must/can be reduced to numbers... + if (hint === 'number' || (hint === 'default' && (constants.isBoolean(obj) || constants.isNumber(obj)))) { + return Number(obj); // returns NaN for objects that can't be converted to numbers; this is expected behavior (thus, any ObjCObjects that get to here, will return NaN) + // ...now deal with string representations; first, ObjCInstance... + } else if (obj?.[constants.__objcInstancePtr]) { + // we need to handle ObjCInstances here, so we can call their -[NSObject description] to get a nice descriptive Cocoa-style string, which is typically of the format '', e.g. "[]"; we then wrap this string in square brackets as is the JS tradition, and hope that users don't mistake the result for a single-item Array (we might want to work on ObjC instance representations some more later on) + return `[${proxyObject.description().UTF8String()}]`; // + // ...and finally, ObjCClasses (which can stringify themselves) and JS values are left for JS to stringify + } else { + return String(obj); // let JS do its own formatting of native JS values; this will also format (just to be JS-awkward, Array doesn't appear to have a Symbol.toPrimitive method, so I’m guessing JS runtime calls its toString method which it does have) + } + }); + + case Symbol.iterator: + let enumerator; + if (proxyObject.isKindOfClass_(getClassByName('NSArray')) + || proxyObject.isKindOfClass_(getClassByName('NSSet'))) { + enumerator = proxyObject.objectEnumerator(); + } else if (proxyObject.isKindOfClass_(getClassByName('NSDictionary'))) { + enumerator = proxyObject.keyEnumerator(); + } else { + throw new Error(`Can't iterate over non-enumerable type ${objcObject.class()}`); + } + return function * objcEnumerator() { + let nextObject; + while ((nextObject = enumerator.nextObject()) && nextObject !== null) { + yield nextObject; + } + }; + // default: best to fall-thru here, in case an above case needs to break instead of return + } + + // 3. anything else is either an unsupported Symbol key or a string, which is assumed to be the name of an ObjC method which has not previously been used so will be looked up (and also cached for resuse) now + if (!constants.isString(methodName)) { // must be a Symbol so it can't be a method name and it isn't a builtin + return undefined; + } + method = objcObject.bindMethod(methodName); // note: this throws if name not found + } + +//console.log('lookup '+methodName+': '+(Number(process.hrtime.bigint() - t)/1e6)) + + // 4. return the result + return method; + }, + + set: function(objcObject, methodName, _) { + throw new Error(`method Proxy cannot set ${objcObject}.${key}`); + } + }); +} + + +const isWrappedObjCObject = (object) => object?.[constants.__objcObject] !== undefined; +const isWrappedObjCClass = (object) => object?.[constants.__objcClassPtr] !== undefined; +const isWrappedObjCInstance = (object) => object?.[constants.__objcInstancePtr] !== undefined; + + - static isNull(instance) { - return instance.__ptr.isNull(); +const getObjCSymbolByName = (name) => { // get a symbol which caller knows to be an ObjC object (`id`, typically a NSString* constant); caution: passing a name for something that is not an ObjC instance will crash + try { // TO DO: what throws here? + const symbol = runtime.getSymbol(name); + symbol.type = ref.refType(ref.refType(ref.types.void)); // void** + const ptr = symbol.deref(); + return (ptr === null || ptr.isNull()) ? null : wrapInstance(ptr); + } catch (err) { + return null; } } -module.exports = Instance; +/******************************************************************************/ + + +// DEBUG: performance test +module.exports.reset = () => _totaltime = _zero; +module.exports.totaltime = () => _totaltime; + +// important: external callers must always use `wrapClass`/`wrapInstance`/`wrap` to convert an ObjC object ptr to a correctly wrapped ObjCClass/ObjCInstance, and ensure its class is correctly registed in the internal cache + +module.exports.getClassByName = getClassByName; +module.exports.getClassByPtr = getClassByPtr; +module.exports.wrap = (ptr) => (runtime.object_isClass(ptr) ? getClassByPtr(ptr) : wrapInstance(ptr)); +module.exports.wrapClass = getClassByPtr; +module.exports.wrapInstance = wrapInstance; + +module.exports.getObjCSymbolByName = getObjCSymbolByName; + +module.exports.createMethodProxy = createMethodProxy; // used by ./codecs + +// these classes are exported for type-checking in objctypes; external code should not instantiate them directly, e.g. to check for an unwrapped ObjCInstance: `(object instanceof ObjCInstance && !isWrappedObjCInstance(object))` +module.exports.ObjCObject = ObjCObject; +module.exports.ObjCClass = ObjCClass; +module.exports.ObjCInstance = ObjCInstance; + +// note: these type-checking functions check for an ObjCObject that is wrapped in its method Proxy, which is what user code should normally interact with (users should not interact with the unwrapped objects, C ptrs, etc unless they really know what they're doing and why they need to do it) +module.exports.isWrappedObjCObject = isWrappedObjCObject; +module.exports.isWrappedObjCClass = isWrappedObjCClass; +module.exports.isWrappedObjCInstance = isWrappedObjCInstance; +module.exports.keyObjCObject = constants.__objcObject; // key for extracting an ObjCObject from its method Proxy wrapper + +// objc exports instance module as objc.__internal__, so export low-level runtime and types modules attached to that +module.exports.runtime = runtime; +module.exports[util.inspect.custom] = (depth, inspectOptions, inspect) => '[object objc.__internal__]'; + + diff --git a/src/objctypes.js b/src/objctypes.js new file mode 100644 index 0000000..37c285d --- /dev/null +++ b/src/objctypes.js @@ -0,0 +1,646 @@ +#!/usr/bin/env node + +'use strict'; + +// +// - extends ref.types with ObjC-specific type definitions +// +// - parse ObjC type encoding strings to ref-napi compatible type definitions +// +// - create ffi.ForeignFunction wrappers for calling ObjC methods +// + +// TO DO: Struct support +// +// NSPoint '{CGPoint="x"d"y"d}' +// NSRectPointer '{CGRect="origin"{CGPoint}"size"{CGSize}}' +// + + +// TO DO: FIX: how to represent opaque pointers? e.g. -[NSData bytes] currently returns Ref object, but deref returns null and there's no Buffer bound to it (ref-napi's logic is tricky to follow, but it allows Buffers to be used as pointer objects); might want to rename Ref to Pointer and have it encapsulate either an object (inc. JS primitive) or a Buffer, depending on whether user instantiates to pass in or objc instantiates it to pass out + + +// TO DO: ref-array-napi, ref-union-napi + +// TO DO: ref-bitfield doesn't seem to have napi version + +// TO DO: check behavior is correct (this is type as defined in AE.bridgesupport): +// objc.defineStruct('{OpaqueAEDataStorageType=}'); + +const util = require('util'); + +const ffi = require('ffi-napi'); +const ref = require('ref-napi'); + +const constants = require('./constants'); + +const Ref = require('./reference'); +const Selector = require('./selector'); +const objcblock = require('./block'); +const objcstruct = require('./struct'); + +const instance = require('./instance'); +const runtime = require('./runtime'); +const codecs = require('./codecs'); + + +const pointerType = ref.refType(ref.types.void); + + +/******************************************************************************/ +// ref-napi compatible type definitions for wrapping and unwrapping ObjC objects +// +// (these should be fully interchangeable with ref-napi's ref.types.TYPE objects) +// +// Important: `Class` and `id` types are themselves *always* pointers to an ObjC structure, e.g. `NSString*` is a sub-type of `id`. There is no way to allocate an ObjC class instance directly on the stack, as can be done with C++. Therefore a `ref.refType(TYPE)` call should only be applied when creating a pointer to that pointer, e.g. `NSError**`, and even then it is normally more convenient (and robust) to use objc's `Ref` instead. +// +// +// note: when TYPE.indirection===1, ffi.ForeignFunction calls TYPE.set/get() to convert the given data to/from C (this is counterintuitive, but ffi treats 0 as an error along with other falsy values) +// + + +// TO DO: check these work correctly with ref-napi + + +const objc_class_t = { + name: 'objc_class_t', + size: ref.sizeof.pointer, + alignment: ref.alignof.pointer, + ffi_type: ffi.FFI_TYPES.pointer, + indirection: 1, + get: (buffer, offset) => { // e.g. ffi.ForeignFunction calls to wrap result + const ptr = ref.readPointer(buffer, offset || 0); + return ptr.isNull() ? null : instance.wrapClass(ptr); + }, + set: (buffer, offset, value) => { // e.g. ffi.ForeignFunction calls to unwrap argument + if (!instance.isWrappedObjCClass(value)) { + throw new TypeError(`Expected an ObjC class but received ${typeof value}: ${value}`); + } + ref.writePointer(buffer, offset || 0, value[instance.keyObjCObject].ptr); + }, +}; + + +const objc_instance_t = { + name: 'objc_instance_t', + size: ref.sizeof.pointer, + alignment: ref.alignof.pointer, + ffi_type: ffi.FFI_TYPES.pointer, + indirection: 1, + get: (buffer, offset) => { + const ptr = ref.readPointer(buffer, offset || 0); + return ptr.isNull() ? null : instance.wrap(ptr); + }, + set: (buffer, offset, value) => { + let ptr; + // TO DO: what value types should this accept? + if (instance.isWrappedObjCObject(value)) { + ptr = value[instance.keyObjCObject].ptr; + } else if (value === null) { + ptr = ref.NULL; + } else if (value instanceof instance.ObjCObject) { + console.log('WARN: objc_instance_t.set received an unwrapped ObjCObject'); + ptr = value.ptr; + } else if (value instanceof Buffer) { + + throw new Error(`raw buffer`); + + console.log('WARN: objc_instance_t.set received a raw Buffer'); + ptr = value; + } else { + ptr = codecs.ns(value, (value) => { + throw new TypeError(`Expected an ObjC instance or null but received ${typeof value}: ${util.inspect(value)}`); + }, true); + } + ref.writePointer(buffer, offset || 0, ptr); + }, +}; + + +const objc_selector_t = { + name: 'objc_selector_t', + size: ref.sizeof.pointer, + alignment: ref.alignof.pointer, + ffi_type: ffi.FFI_TYPES.pointer, + indirection: 1, + get: (buffer, offset) => { + const ptr = ref.readPointer(buffer, offset || 0); + return ptr.isNull() ? null : new Selector(ptr); + }, + set: (buffer, offset, value) => { + let ptr; + // TO DO: what value types should this accept? + if (value instanceof Selector) { + ptr = value.ptr; + } else if (constants.isString(value)) { // TO DO: also accept string, e.g. "foo:barBaz:"? + ptr = runtime.sel_getUid(value); // TO DO: use JS or NS selector syntax? + } else if (value instanceof Buffer) { + console.log('WARN: objc_selector_t.set received a raw Buffer'); + ptr = value; + } else { + throw new TypeError(`Expected an ObjC Selector but received ${typeof value}: ${value}`); + } + ref.writePointer(buffer, offset || 0, ptr); + }, +}; + + +const objc_opaqueblock_t = { // TO DO: move this to ./block.js? + name: 'objc_opaqueblock_t', + size: ref.sizeof.pointer, + alignment: ref.alignof.pointer, + ffi_type: ffi.FFI_TYPES.pointer, + indirection: 1, + get: (buffer, offset) => { + const ptr = ref.readPointer(buffer, offset || 0); + return ptr.isNull() ? null : new objcblock.Block(objc_opaqueblock_t, ptr); // opaque Block; not currently callable from within JS, but at least it can pass through JS back to ObjC + }, + set: (buffer, offset, value) => { + if (!(value instanceof objcblock.Block)) { + throw new TypeError(`Expected ObjC Block but received ${typeof value}: ${value}`); + } + ref.writePointer(buffer, offset || 0, value.ptr); + }, +}; + + +class ObjCPtrType { + // a ref-napi compatible type definition for an objc.Ref pointer object + // + // Note: whereas ref-napi constructs a pointer value using: + // + // const ptr = ref.alloc(TYPE[,VALUE]).ref() + // + // objc uses: + // + // const ptr = new objc.Ref([VALUE]) + // + // This does not require a ref-napi type definition to construct it; instead, the Ref's type - an instance of ObjCPtrType - is created and attached to it when it is passed as an argument to/returned as a result from an ObjC method. + // + // (While a ref-napi pointer value can also be created and passed to an ObjC method, it is up to the user to ensure it is of the correct type; the objc bridge cannot check it and the ObjC runtime will crash if it is not.) + + + // TO DO: get/set methods' fragile (dynamic) `this` bindings below assumes that ffi/ref will never bind a type object's get/set functions to variables to be called later, but will always call them directly on body of type object, i.e. `TYPE.get(...)`, never `const f = TYPE.get; f(...)`; however, most ref-napi types are plain old objects containing unbound functions, which does allow such usage; therefore, it'd be safest to avoid using `this` in these methods + + constructor(type, indirection = 1) { + // type : object -- a ref-napi compatible type definition, most often `objc_instance_t` (e.g. when constructing `NSError**` out arguments) though can be any ref-napi compatible type object + this.reftype = type; // TO DO: make this private and add `get reftype()` for safety? or just apply Object.freeze()? (Q. does ref-napi bother to freeze its own type objects, or are they left mutable and user is entrusted not to bork them?) // ref-napi would copy type and increment its indirection + this.size = ref.sizeof.pointer; + this.alignment = ref.alignof.pointer; + this.ffi_type = ffi.FFI_TYPES.pointer; // Q. is this appropriate? + this.indirection = indirection; + } + + get name() { return `${this.reftype.objcName || this.reftype.name || 'void'}*`; } // TO DO: is this appropriate syntax? (it is not the syntax used by ObjC encoding strings, but ref.types.TYPE.name); TO DO: should we standardize on `objcName` for our own preferred names (e.g. 'CGPoint') + + [util.inspect.custom]() { + return `[objc Ref(${this.reftype.objcName || this.reftype.name || 'void'})]`; + } + + get(buffer, offset) { // e.g. -[NSAppleEventDescriptor aeDesc] returns `^{AEDesc}`, i.e. a pointer to an AEDesc struct + //console.log('PtrType.get: ', buffer) + // buffer's content is a pointer (void*) + const ptr = Buffer.from(buffer, offset, ref.sizeof.pointer); // copy the address + return new Ref(ptr, this, true); + } + + set(buffer, offset, value) { + //console.log('PACK REF',value) + let ptr; + if (value instanceof Ref) { + ptr = ref.alloc(pointerType); + this.reftype.set(ptr, offset, value.value, this.reftype); + // attach [copy of] ptr to Ref, so that method wrapper can check if it has changed and rewrap if it has // TO DO: think this is wrong: it's the pointee that changes + value.__outptr = ptr; + value.__inptr = Buffer.from(ptr); + value.ffi_type = this; + } else if (value === null) { + ptr = ref.NULL; // some methods allow inout args to be nil, in which case nothing is returned + } else if (value instanceof Buffer) { // assume user knows what they're doing // TO DO: we should probably check this for ffi_type (Q. does ref.alloc attach type to buffer?) and confirm indirection level is correct + ptr = value; + } else { + throw new TypeError(`Expected a Ref or null but received ${typeof value}: ${value}`); + } + ref.writePointer(buffer, offset || 0, ptr); + } +} + + +const objc_inout_t = new ObjCPtrType(objc_instance_t); // most commonly used ObjC pointer type, `id*` (e.g. `NSError**`) + + +/******************************************************************************/ +// ObjC type encodings +// +// from: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide + +// Table 6-1 Objective-C type encodings +const _objcTypeEncodings = { + 'c': ref.types.char, + 'i': ref.types.int32, + 's': ref.types.int16, + 'l': ref.types.int32, // Apple docs say: "l is treated as a 32-bit quantity on 64-bit programs." + 'q': ref.types.int64, + 'C': ref.types.uchar, + 'I': ref.types.uint32, + 'S': ref.types.uint16, + 'L': ref.types.uint32, + 'Q': ref.types.uint64, + 'f': ref.types.float, + 'd': ref.types.double, + 'B': ref.types.bool, // A C++ bool or a C99 _Bool + 'v': ref.types.void, + '*': ref.types.CString, + + '#': objc_class_t, // A class object (Class) + ':': objc_selector_t, // A method selector (SEL) + + // these are handled in parseType: + // @ // An object (whether statically typed or typed id) + // [array type] // An array + // {name=type...} // A structure + // (name=type...) // A union + // bnum // A bit field of num bits + // ^type // A pointer to type + + // TO DO: how does ObjC encoding describe varargs? + + '?': ref.refType(ref.types.void), // An unknown type (among other things, this code is used for function pointers) +}; + + +// Table 6-2 Objective-C method encodings +const _objcTypeQualifiers = { + 'r': 'const', + 'n': 'in', + 'N': 'inout', + 'o': 'out', + 'O': 'bycopy', + 'R': 'byref', + 'V': 'oneway', +}; + + +const DIGITS = '1234567890'; +const ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'; + +const IDENTIFIER_FIRST = ALPHA_UPPER + ALPHA_LOWER + '_'; +const IDENTIFIER_REST = IDENTIFIER_FIRST + DIGITS; + + + +class ObjCTypeEncodingParser { + encoding; + cursor; + + constructor(encoding) { + // encoding : string -- an ObjC type encoding string, e.g. "o^@", "@:@io^@" + this.encoding = encoding; + this.cursor = 0; + } + + get currentToken() { return this.encoding[this.cursor]; } + get nextToken() { return this.encoding[this.cursor + 1]; } + + // read... methods consume a portion of the encoding string starting at current token + + readSize() { + let size = ''; + while (DIGITS.includes(this.currentToken)) { + size += this.currentToken; + this.cursor++; + } + if (!size) { throw new Error(`Missing bit size in: '${this.encoding}'`); } + return parseInt(size); + } + + readName() { + let token, name = this.currentToken; + if (!IDENTIFIER_FIRST.includes(name)) { throw new Error(`Bad identifier in: '${this.encoding}'`); } + this.cursor++; + while ((token = this.currentToken) && IDENTIFIER_REST.includes(token)) { + name += token; + this.cursor++; + } + return name; + } + + readQuotedName() { + this.cursor++; // step over opening '"' + const name = this.readName(); + if (this.currentToken !== '"') { + throw new Error(`Bad quoted name; expected '"', got '${this.currentToken}' at character ${this.cursor} in: '${this.encoding}'`); + } + this.cursor++; // step over closing '"' + return name; + } + + readArrayType() { // e.g. [12^f] = an array of 12 pointers to float + const size = this.readSize(); + const type = this.readType(); + if (this.currentToken !== ']') { throw new Error(`Bad array encoding in: '${this.encoding}'`); } + this.cursor++; // step over closing ']' + // TO DO: use ref-napi-array to create new [ObjC]ArrayType + return pointerType; // TO DO: temporary + } + + /* + Structures are specified within braces, and unions within parentheses. The structure tag is listed first, followed by an equal sign and the codes for the fields of the structure listed in sequence. For example, the structure: + + typedef struct example { + id anObject; + char *aString; + int anInt; + } Example; + + would be encoded like this: + + {example=@*i} + + The same encoding results whether the defined type name (Example) or the structure tag (example) is passed to @encode(). The encoding for a structure pointer carries the same amount of information about the structure’s fields: + + ^{example=@*i} + + However, another level of indirection removes the internal type specification: + + ^^{example} + */ + readStructType() { + // parses an ObjC struct encoding, e.g. '{CGRect="origin"{CGPoint}"size"{CGSize}}' + + // IMPORTANT: this method not only parses the encoding string, it also gets and sets the StructType cache in objcstruct; this allows it to handle recursive definitions and to look up name-only encodings, e.g. parsing `{CGSize}` will return the type definition for `CGSize` + + let type; + const startIndex = this.cursor - 1; // caller already stepped over '{' + const name = this.readName(); // type name is required (without it, the encoding would be ambiguous, e.g. `{Bi}`) + if (this.currentToken === '=') { + this.cursor++; // step over '=' + // TO DO: is this correct, or should it use `new`: + type = objcstruct.ObjCStructType(); // eslint-disable-line new-cap + type.objcName = name; // note: StructType's name is read-only + let count = 0; + while (this.currentToken && this.currentToken !== '}') { + let propertyName = null; + if (this.currentToken === '"') { // member name + propertyName = this.readQuotedName(); + } + //console.log(`${name}: read quoted name: '${propertyName}'`) + const propertyType = this.readType(); + type.defineProperty(propertyName ?? `\$${count}`, propertyType); // if no name given, use `$0`, `$1`, etc; TO DO: problem with this is it doesn't match correct fields in object + count++; + } + if (this.currentToken !== '}') { + throw new Error(`Bad struct encoding: expected '}', got '${this.currentToken}' in '${this.encoding}'`); + } + this.cursor++; // step over closing '}'/')' + + const structEncoding = this.encoding.slice(startIndex, this.cursor); + const existingType = objcstruct.getStructTypeByName(name); + + + if (!existingType || existingType.objcEncoding.length < structEncoding) { // kludge; we want the most detailed definition to be kept; TO DO: e.g. 'substringWithRange:' has encoding '@32@0:8{_NSRange=QQ}16', which is not full _NSRange encoding as it lacks property names + + type.objcEncoding = structEncoding; + objcstruct.addStructType(type); // add this new StructType to objcstruct's cache // TO DO: might be safer to merge this type object into the cached object instead of replacing it + + } else { // there's already a full encoding + objcstruct.aliasStructType(existingType, structEncoding); + type = existingType; + } + + } else if (this.currentToken === '}') { // name-only struct `{NAME}`; note: while name-only encodings are useless for creating a new ObjCStructType, they can appear in other types' encoding strings; for now, a struct type must be fully defined before it can be referenced by another encoding // TO DO: need to support out-of-order definitions as, unlike C header files, bridgesupport files are not guaranteed to define types in order of dependency + this.cursor++; // step over closing '}'/')' + type = objcstruct.getStructTypeByName(name); + if (!type) { throw new Error(`Struct encoding referenced undefined '${name}' struct type: '${this.encoding}'`); } + } else { + throw new Error(`Bad struct encoding: expected '}', got '${this.currentToken}' in '${this.encoding}'`); + } + // console.log(`parsed: '${type.objcEncoding}'`) + return type; + } + + /* + + */ + readUnionType() { + throw new Error('TO DO: readUnionType'); + } + + // + + readType() { + let token, type, indirection = 0; + while (_objcTypeQualifiers[(token = this.currentToken)] !== undefined) { this.cursor++; } // skip qualifiers + while (token === '^') { + indirection++; + this.cursor++; + token = this.currentToken; + } + type = _objcTypeEncodings[token]; + this.cursor++; // step over token + if (!type) { + switch (token) { + case '@': + if (this.currentToken === '?') { // '@?' is the encoding used for blocks + this.cursor++; + type = objc_opaqueblock_t; + } else if (indirection === 1) { // commonly 'o^@' argument, e.g. NSError** + indirection--; + type = objc_inout_t; + } else { + type = objc_instance_t; + } + if (this.currentToken === '"') { // step over class/protocol name, e.g. `@"NSString"`, `@""` + do { this.cursor++; } while (this.currentToken !== '"'); + this.cursor++; + } + break; + case '[': + type = this.readArrayType(); + break; + case '{': + type = this.readStructType(); + break; + case '(': + type = this.readUnionType(); + break; + case 'b': + const size = this.readSize(); + throw new Error("TO DO: return bitfield type"); + break; + default: + token = (token === undefined) ? 'end of string' : `'${token}'`; + throw new Error(`Bad type encoding: unexpected ${token} at character ${this.cursor} in '${this.encoding}'`); + } + } + while ('1234567890'.includes(this.currentToken)) { this.cursor++; } // skip #bits offset + if (indirection > 0) { type = new ObjCPtrType(type, indirection); } + return type; // cursor is positioned on first character of next type (or undefined if end of encoding) + } + + // public API: parse methods consume a complete encoding string and return a type object or array of type objects + + parseType() { + // parse a single ObjC type encoding, e.g. "o^@" + // Result: object -- a ref-napi compatible type definition, suitable for use in ffi-napi + // e.g. .bridgesupport files normally define each argument/member separately so use parseType() for those + let type = this.readType(); + if (this.cursor !== this.encoding.length) { throw new Error(`Bad type encoding '${encoding}'`); } + return type; + } + + parseTypeArray() { + // parse a sequence of one or more ObjC type encodings; typically a method, struct, or block’s type, e.g. "@:@io^@" + // Result: [object,...] -- one or more ref-napi compatible type definitions, suitable for use in ffi-napi // TO DO: should result be [returnType,[argType,...]]? this is what ffi APIs expect and it'd save caller having to shift the return type off the Array + // e.g. method signatures obtained via ObjC's introspection APIs + const types = []; + do { + types.push(this.readType()); + } while (this.currentToken); + return types; + } +} + + +/******************************************************************************/ + + +const coerceObjCType = (encoding) => { + const typeParser = new ObjCTypeEncodingParser(encoding); + return typeParser.parseTypeArray(); +} + + +const introspectMethod = (object, methodName) => { + // object : ObjCObject -- an ObjCClass/ObjCInstance, without Proxy wrapper + // methodName : string -- the JS-style name of an ObjC method on this object, e.g. "foo_barBaz_" + // Result: object -- description of this method, plus an ffi.ForeignFunction object to call it + /* + Examples: + + +[NSBundle bundleWithPath:] = "@24@0:8@16" + returnType @ + argument 0 @ + argument 1 : + argument 2 @ + + -[NSURL getResourceValue:forKey:error:] = "c40@0:8o^@16@24o^@32" + returnType c + argument 0 @ + argument 1 : + argument 2 o^@ + + First 2 arguments are always target and selector + + optional characters before type = qualifiers + digits after type = byte offsets, which we ignore + */ + // look up the method so we can introspect its argument and return types + const selectorName = Selector.selectorNameFromJS(methodName); + const sel = runtime.sel_getUid(selectorName); + const method = object.objcMethodPtr(sel); // do not keep method pointer; objc_msgSend will look up methods itself + if (!method || method.isNull()) { // TO DO: do class_getClassMethod/class_getInstanceMethod always return a pointer Buffer, or can they also return null/undefined? (added DEBUG below to see) + let msg = ''; // TO DO: smarter autosuggest (for now it only checks for a missing trailing underscore, which is the most common mistake) + if (!selectorName.endsWith(':')) { + const method2 = object.objcMethodPtr(runtime.sel_getUid(`${selectorName}:`)); + if (method2 && !method2.isNull()) { msg = ` (did you mean ${methodName}_?)`; } + } + if (!method) { console.log(`introspectMethod warning: objcMethodPtr for "${selectorName}" returned: ${method}`); } // DEBUG + throw new TypeError(`No method named objc.${object.name}.${methodName}${msg}`); + } + const encoding = runtime.method_getTypeEncoding(method); + const typeParser = new ObjCTypeEncodingParser(encoding); + const argTypes = typeParser.parseTypeArray(); // [result, target, selector,...arguments] + const returnType = argTypes.shift(); + // first 2 args are always target and selector, which method wrapper already passes as pointers + argTypes[0] = pointerType; + argTypes[1] = pointerType; + let inoutIndexes = []; + for (let [i, arg] of Object.entries(argTypes.slice(2))) { + if (arg instanceof ObjCPtrType) { inoutIndexes.push(i); } + } + return { // used in callObjCMethod + methodName, // used in error messages + sel, // ptr -- arg 1 to msgSend + encoding, // string // currently used in error messages, although it's not particularly user-friendly + argc: argTypes.length, // method wrapper does its own argument count check + inoutIndexes: (inoutIndexes.length > 0 ? inoutIndexes : null), // used to set values in objc.Ref `[in]out` arguments + msgSend: ffi.ForeignFunction(runtime.objc_msgSend, returnType, argTypes), // eslint-disable-line new-cap + }; +} + + +/******************************************************************************/ + +// DEBUG: performance test +let _totaltime = process.hrtime.bigint(); +let _zero = _totaltime - _totaltime; +_totaltime = _zero; + + + +Object.assign(module.exports, ref.types, { + + ObjCTypeEncodingParser, + + // DEBUG: performance test + reset: () => _totaltime = _zero, + totaltime: () => _totaltime, + + // aliases + NSInteger: ref.types.int64, + NSUInteger: ref.types.uint64, + id: objc_instance_t, + Class: objc_class_t, + SEL: objc_selector_t, + + // ObjC types + objc_instance_t, + objc_class_t, + objc_selector_t, + objc_opaqueblock_t, + ObjCPtrType, // constructor for ref-napi compatible type that packs and unpacks an objc.Ref + + // introspection + coerceObjCType, // unlike ref.coerceType, which takes a single C type name and returns a single ref type object, this takes a complete ObjC encoding string describing 1 or more types and returns an Array of ref-compatible type objects + introspectMethod, // returns an object describing the method, plus a msgSend ForeignFunction for calling it + + // shallow inspect this module + [util.inspect.custom]: (depth, inspectOptions, inspect) => `{\n\t${Object.keys(module.exports).join(',\n\t')}\n}`, // TO DO: allow a custom 'detail' flag ('low'/'medium'/'high') in inspectOptions, that switches between returning '[object objctypes]', '{void,int8,uint8,etc}', or the full object representation (the standard util.inspect behavior); with 'medium' or 'low' as the default setting; see also ./runtime.js; this should cut down on unhelpful noise being displayed to users (most of whom will rarely need to use types or runtime modules), while still allowing full inspection for troubleshooting purposes + +}); + + + +// DEBUG: performance test (this should log how much time is spent inside ref.types' get/set functions; with caveat that any calls back into the plumbing will be included in the total, which is not ideal as we're trying to distinguish time taken by code within get/set from time spent in the infrastructure underneath) +var _depth = 0; + +function timer(fn) { + return (...args) => { + let res; + if (!_depth) { + _totaltime -= process.hrtime.bigint(); + } + _depth++; + try { + res = fn(...args); + } finally { + _depth--; + } + if (!_depth) { + _totaltime += process.hrtime.bigint(); + } + return res; + } +} + +for (let k in module.exports) { + const v = module.exports[k]; + if (v && v.indirection !== undefined) { + module.exports[k].get = timer(v.get); + module.exports[k].set = timer(v.set); + } +} + + diff --git a/src/proxies.js b/src/proxies.js deleted file mode 100644 index 4063011..0000000 --- a/src/proxies.js +++ /dev/null @@ -1,98 +0,0 @@ -const util = require('util'); -const Selector = require('./selector'); - -const _getUnderlyingObject = Symbol('getUnderlyingObject'); - -const MethodProxy = (object, selector) => { - const self = object; - const pseudoTarget = () => {}; - pseudoTarget[util.inspect.custom] = (depth, options) => { - return `[objc.MethodProxy '${self.type === 'class' ? '+' : '-'}[${object.class()} ${selector}]']` - } - return new Proxy(pseudoTarget, { - get: (_, key) => { - if (key === util.inspect.custom) { - return () => `[objc.MethodProxy '${self.type === 'class' ? '+' : '-'}[${object.class()} ${selector}]']`; - } - }, - - apply: (target, _this, args) => { - // Add a trailing underscore to the selector if: - // 1. There are more arguments than underscores - // 2. The method doesn't already end w/ an underscore - if (!selector.endsWith('_') && (selector.split('_').length - 1) < args.length) { - selector += '_'; - } - - return self.call(new Selector(selector), ...args); - } - }); -}; - -function InstanceProxy(object) { - const self = object; - - return new Proxy(self, { - get: (target, key) => { - if (key === _getUnderlyingObject) { - return object; - } - if (key === util.inspect.custom) { - return () => `[objc.InstanceProxy ${self.description()}]`; - } else if (key === Symbol.toPrimitive) { - return hint => { - if (hint === 'number') { - return MethodProxy(self, 'doubleValue')(); // eslint-disable-line new-cap - } - // Hint is either 'string' or 'default' - return self.description(); - }; - } else if (key === Symbol.iterator) { - const isKindOfClass = MethodProxy(self, 'isKindOfClass_'); // eslint-disable-line new-cap - - // PLOT TWIST: what if self is already an enumerator? // TODO - let enumerator; - - if (isKindOfClass('NSArray') || isKindOfClass('NSSet')) { - enumerator = MethodProxy(self, 'objectEnumerator')(); // eslint-disable-line new-cap - } else if (isKindOfClass('NSDictionary')) { - // TODO should we enumerate over the keys or values, or should we return tuples??? - enumerator = MethodProxy(self, 'keyEnumerator')(); // eslint-disable-line new-cap - } else { - throw new Error(`Can't iterate over non-enumerable type ${self.class()}`); - } - - return function * () { - let nextObject; - while ((nextObject = enumerator.nextObject()) && !nextObject.__ptr.isNull()) { - yield nextObject; - } - }; - } - - // Might be a Symbol - key = String(key); - - if (key === '__ptr') { - return self.__ptr; - } else if (key === '___is_instance_proxy') { - return true; - } - - return MethodProxy(self, key); // eslint-disable-line new-cap - }, - set: (_, key, value) => { - if (key === '__ptr') { - self.__ptr = value; - return true; - } - return false; - } - }); -} - -module.exports = { - InstanceProxy, - MethodProxy, - _getUnderlyingObject -}; diff --git a/src/reference.js b/src/reference.js new file mode 100644 index 0000000..508a8bd --- /dev/null +++ b/src/reference.js @@ -0,0 +1,67 @@ +// Ref -- use with ObjC class and instance methods that return-by-argument + +// notes: +// +// - unlike ref-napi's `ref.alloc(TYPE[,VALUE])`, which requires inout types be explicitly defined by caller, `new objc.Ref([VALUE])` determines the VALUE's type from the ObjC type encoding when it is passed as argument to the ObjC method +// +// caution: Ref objects are not thread-safe and should not be shared across multiple threads +// + +const util = require('util'); + +/******************************************************************************/ +// inout argument + + +class Ref { + #value; #isPtr; + __inptr; __outptr; ffi_type; // used internally by objctypes to update out arguments + + constructor(value = null, type = null, valueIsPtr = false) { + this.#value = value; + this.ffi_type = type; // null if not [yet] known + this.#isPtr = valueIsPtr; + } + + ref() { + throw new Error('TO DO: Ref.ref'); + } + // TO DO: compatibility with ref-napi 'pointer' codec + + deref() { // call this after the ObjC method returns to get the returned value + if (this.#isPtr) { + if (!this.ffi_type) { throw new Error(`Can't deref: unknown reftype.`); } + + // TO DO: what about opaque pointers? (ffi_type = `void*`); really depends on how we want to implement Ref wrt ref-napi, which uses shallow copy plus incrementing/decrementing indirection to represent the ffi_type, attaching that type to a new pointer-sized Buffer each time it's incremented or decremented to 2, and unpacking the buffer when decremented to 1; the problem with that is decrementing an opaque void* is that the last decrement returns null, which probably isn't what's intended (i.e. C wouldn't let you do that, as `void t; *t = ptr;` isn't valid); with ref-napi, deref() on a `void*` returns a NULL Buffer, which in turn deref()s to null + + const data = this.#value.readPointer(0, this.ffi_type.reftype.size); + + //console.log(data) + + const value = this.ffi_type.reftype.get(data, 0, this.reftype); + + return value; + } else { + return this.#value; + } + } + + set value(newValue) { // use this within a Block function to return a new value by argument + // TO DO: when called within a Block function, this needs to pack the value and assign it to the __outptr + this.#value = newValue; + this.#isPtr = false; + } + + get value() { // use this after an ObjC method returns to get the returned value + return this.deref(); + } + + [util.inspect.custom]() { + return `Ref(${util.inspect(this.deref())})`; + } +} + + + +module.exports = Ref; + diff --git a/src/runtime.js b/src/runtime.js index da83805..8d8fafb 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -1,8 +1,11 @@ /* eslint-disable camelcase, key-spacing, no-multi-spaces, array-bracket-spacing */ +const util = require('util'); + const ffi = require('ffi-napi'); const ref = require('ref-napi'); + const dlfcn = new ffi.Library(null, { dlopen: ['pointer', ['string', 'int']] }); @@ -16,6 +19,7 @@ const libobjc = new ffi.Library('libobjc', { objc_getClass : ['pointer', ['string' ]], object_getClass : ['pointer', ['pointer']], object_isClass : ['bool', ['pointer']], + object_getClassName : ['string', ['pointer']], class_getName : ['string', ['pointer']], class_getClassMethod : ['pointer', ['pointer', 'pointer']], class_getInstanceMethod: ['pointer', ['pointer', 'pointer']], @@ -25,11 +29,11 @@ const libobjc = new ffi.Library('libobjc', { objc_registerClassPair : ['void', ['pointer']], // Methods - method_getImplementation : ['pointer', ['pointer']], method_getTypeEncoding : ['string', ['pointer']], + method_getNumberOfArguments : ['int', ['pointer']], method_copyReturnType : ['string', ['pointer']], method_copyArgumentType : ['string', ['pointer', 'int']], - method_getNumberOfArguments : ['int', ['pointer']], + method_getImplementation : ['pointer', ['pointer']], method_exchangeImplementations: ['void', ['pointer', 'pointer']], method_setImplementation : ['pointer', ['pointer', 'pointer']], @@ -39,28 +43,20 @@ const libobjc = new ffi.Library('libobjc', { libobjc.objc_msgSend = ffi.DynamicLibrary().get('objc_msgSend'); // eslint-disable-line new-cap -const msgSend = (returnType, argumentTypes) => { - return ffi.ForeignFunction(libobjc.objc_msgSend, returnType, argumentTypes); // eslint-disable-line new-cap -}; -const classExists = classname => !libobjc.objc_getClass(classname).isNull(); +// TO DO: this is probably redundant from users' POV as they can just ask for objc.CLASSNAME and catch 'not found' error +const classExists = (classname) => !libobjc.objc_getClass(classname).isNull(); -const getSymbol = name => new ffi.DynamicLibrary().get(name); +const getSymbol = (name) => new ffi.DynamicLibrary().get(name); // TO DO: why does this create new DynamicLibrary object every time it's called? -const getSymbolAsId = name => { - try { - const symbol = getSymbol(name); - symbol.type = ref.refType(ref.refType(ref.types.void)); - return symbol.deref(); - } catch (err) { - return null; - } -}; dlfcn.dlopen('/System/Library/Frameworks/Foundation.framework/Foundation', ffi.DynamicLibrary.FLAGS.RTLD_LAZY); + module.exports = libobjc; -module.exports.msgSend = msgSend; -module.exports.classExists = classExists; -module.exports.getSymbol = getSymbol; -module.exports.getSymbolAsId = getSymbolAsId; + +module.exports.classExists = classExists; +module.exports.getSymbol = getSymbol; + +module.exports[util.inspect.custom] = () => '[object objc.__internal__.runtime]'; // noise reduction when inspecting `objc` object + diff --git a/src/selector.js b/src/selector.js index d5f74e0..f313ad0 100644 --- a/src/selector.js +++ b/src/selector.js @@ -1,64 +1,76 @@ // eslint-disable-next-line camelcase -const {sel_getUid, sel_getName} = require('./runtime'); -const cache = {}; +// selector -- JS wrapper for ObjC SEL (ObjC method name) +// +// includes methods for converting ObjC-style (colon-delimited) selector names to JS-style method names and back +// +// also used in ObjC methods that take selectors as arguments/return selectors as result -const joinSelectorWithPossiblePermutations = (selector, permutations) => { - const split = selector.split('_'); - const methodName = split.shift(); - const selectors = []; - permutations.forEach(permutation => { - // eslint-disable-next-line brace-style, max-statements-per-line - selectors.push(permutation.reduce((acc, val, index) => { return acc + val + split[index]; }, methodName)); - }); - return selectors; -}; +// TO DO: JS<->NS name conversions are almost but not quite 100% stable; the one case that'll trip up method name conversion is 2 or more contiguous colons, e.g. `foo_bar::` (which afaik is legal, if not common nor recommended), as that will, in the current implementation, map to `foo__bar__`, which then maps back to `foo_bar_`, not `foo_bar::` (since double underscores in JS map to single underscores in SEL), making any such ObjC methods inaccessible from JS -const getPossibleSelectorNames = selector => { - if (!selector.includes('_')) { - return [selector]; - } - - const n = selector.split('_').length - 1; - - let permutations = cache[n]; +// BTW, once the mapping is finalized and implemented as Selector methods, it is trivial to provide a CLI utility script that reads an NS-style selector name string from line input and prints its JS representation to stdout (and vice-versa), and tell users who need assistance translating a method’s syntax to go use that - if (typeof permutations !== 'undefined') { - return joinSelectorWithPossiblePermutations(selector, permutations); - } - permutations = []; +const constants = require('./constants'); +const runtime = require('./runtime'); - const numberOfPermutations = Math.pow(2, n); - for (let i = 0; i < numberOfPermutations; i++) { - permutations.push([]); - for (let j = 0; j < n; j++) { - permutations[i][j] = i & (1 << j) ? '_' : ':'; - } - } - cache[n] = permutations; - return joinSelectorWithPossiblePermutations(selector, permutations); -}; class Selector { - constructor(input) { - if (typeof input === 'string') { - this.__ptr = sel_getUid(input); + #__ptr; + + // utility functions for converting between NS- and JS-style method names, e.g. `foo:barBaz:` <-> `foo_barBaz_` + // (these are attached to Selector 'class' simply to avoid further pollution of top-level `objc` namespace) + + static selectorNameFromJS(name) { + return name.replace(/__?/g, s => s.length === 1 ? ':' : '_'); // TO DO: see TODO above re. resolving remaining ambiguity + } + + static selectorNameToJS(name) { + return name.replace(/[:_]/g, s => { // TO DO: see TODO above re. resolving remaining ambiguity + switch (s) { + case ':': + return '_'; + case '_': + return '__'; + } + }); + } + + // constructors + + constructor(input) { // create a Selector from string (e.g. 'foo:bar:') or existing SEL ptr + // caution: this (public) constructor does not type check to ensure the given value is a string/SEL + if (constants.isString(input)) { + this.#__ptr = runtime.sel_getUid(input); } else { - this.__ptr = input; + this.#__ptr = input; } } - - get name() { - return sel_getName(this.__ptr); + + static fromjs(selectorName) { // create a Selector from JS-style name, e.g. "foo_bar_" + return new Selector(Selector.selectorNameFromJS(selectorName)); + } + + // accessors + + _toPrimitive_() { // TO DO: see above TODO re. ambiguity + // Result: string -- the method's JS (underscore-delimited) name + return Selector.selectorNameToJS(this.name); } - permutations() { - return getPossibleSelectorNames(this.name).map(p => new Selector(p)); + get name() { + // Result: string -- the method's ObjC (colon-delimited) name + return runtime.sel_getName(this.#__ptr); } + + get ptr() { return this.#__ptr; } + + get [String.toStringTag]() { `Selector=${this.name}` } } + + module.exports = Selector; diff --git a/src/struct.js b/src/struct.js new file mode 100644 index 0000000..72547f7 --- /dev/null +++ b/src/struct.js @@ -0,0 +1,133 @@ +// Define [Obj]C struct types, e.g. NSRange, which client code can instantiate and pass to/from methods +// +// e.g. full[1] encoding string for NSRange as defined in Foundation.bridgesupport: +// +// '{_NSRange="location"Q"length"Q}' +// +// ([1] it is not clear if struct encodings can include byte widths of fields, as method encodings can) +// +// Caution: when defined by an ObjC method (argument/return value), the property names are elided: +// +// '{_NSRange=QQ}' +// +// This is not an issue for ObjC but is problematic for StructType as it needs both name and type to define a property. +// +// Additionally, when a struct type appears inside another type encoding, it is contracted to name-only: +// +// '{CGRect="origin"{CGPoint}"size"{CGSize}}' +// +// The _structType cache should automatically alias these short encodings to the full encoding strings for lookup as and when it encounters them (TO DO: this may need work); however, the full struct type must be defined first to ensure the StructType itself is created correctly and fully usable. +// + + +// TO DO: use ref-struct-di's API for structs and struct types? or use original objc API, where structs are instantiated using [required] positional arguments, not an object of [optional] named values as ref-struct-di? e.g. objc.NSRange(1,2) vs objc.NSRange({location:1,length:2}) + + +// TO DO: should methods accept an object where a struct is expected, e.g. passing `{location:1, length:2}` for an NSRange argument, and perform the object-to-struct conversion automatically? (we'd need to define our own ObjCStructType codec for this, which boxes/replaces the original ref-struct-di version); it would be convenient for users (also slower, but convenience may outweigh that) + +// TO DO: do Struct objects created by StructType provide a way (e.g. valueOf) to convert from struct to JS object? (they do allow access to individual properties by name, so shallow copying object properties might [or might not] work if there isn't a built-in method for getting object directly) + + +const util = require('util'); + +const constants = require('./constants'); + +const objctypes = require('./objctypes'); // this is ref-napi types extended with objc types... + +const ref = Object.assign({}, require('ref-napi')); // ...however, the ref-struct-di module wants the entire `ref` module, not just `ref.types`, so clone the original ref-napi module (for politeness)... + +ref.types = objctypes; // ...and amend the copy... + +const ObjCStructType = require('ref-struct-di')(ref); // ...and now we can create new ObjC struct types // TO DO: is there any point to parameterizing ref-struct-di with objctypes? (I suspect it's only needed to support populating field types from strings, which requires coerceType) if so, we'll need to dynamically add copies of struct and block types to objctypes so those are available; however, we're trying to insulate user from ref.types (not least because if we want to migrate off ffi-napi to our own [Obj]C extension we'll likely need to replace ref-napi too, or at least provide a fast alternative as first choice with ref-napi as slow fallback); honestly, parameterizing ref-struct-di with a complete ref module, instead of the function[s] it actually uses, is so over-broad that it's just bad design - I suspect we'll end up implementing our own StructType (as we've done for BlockType) as it doesn't have good inspection strings and I suspect it may be hard to distinguish a struct's type from an instance of that type + + +// TO DO: another reason to define our own ObjCStructType: it can work with or without field names, e.g. '{_NSRange=QQ}' is a valid type encoding from an ObjC method, but it doesn't include field names, only field types, so a standard StructType created from it won't correctly pack e.g. `{location:1,length:2}` (and worse, silently packs it as {0,0} instead of indicating there's a problem, because JS is slop) + + +/******************************************************************************/ +// StructType cache + +const _structTypes = Object.create(null); + +// for internal use + +function addStructType(type) { + // add a StructType to the cache; called by ObjCTypeEncodingParser.readStructType() upon successfully parsing a struct encoding string, e.g. '{CGRect="origin"{CGPoint}"size"{CGSize}}' + // type : ObjCStructType -- a StructType object + + if (!type.objcName || !type.objcEncoding) { throw new Error(`BUG: missing StructType.objcName/objcEncoding:\n${type}\n`); } // DEBUG + + if (_structTypes[type] !== undefined) { + //throw new Error(`Can't add ObjCStructType named '${type.name}' as it already exists: '${type.objcEncoding}'`); // TO DO: temporarily disabled while we figure a satisfactory resolution for `{_NSRange=QQ}` definition found in method (we definitely want to alias all variants of '{_NSRange...}', but at same time we want the actual type definition to contain the most detailed definition) + } + _structTypes[type.objcName] = type; + _structTypes[type.objcEncoding] = type; +} + + +function aliasStructType(type, aliasName) { // e.g. `aliasStructType(CGRect, 'NSRect')`; used by ObjCTypeEncodingParser.readStructType + // store an existing StructType under an alias name; equivalent to C's `typedef NAME ALIAS` + // type : ObjCStructType + // aliasName : string + if (_structTypes[aliasName] === undefined) { // skip if already defined (i.e. throwing 'already defined' errors here causes more problems than it solves) // TO DO: check new definition is same (or superset) of existing definition? (TBH, we probably need to replace ref-struct-di as its API is not a great fit for our needs; as part of that, being able to upgrade an existing struct type's definition in-place as more details become available; we can also revert to objc's original `TYPE(prop1,prop2,...)` positional API and require all fields to be given, which ref-struct-di's `StructType(OBJECT)` does not) + if (!type) { + throw new Error(`BUG in aliasStructType: missing type argument`); + } + const structType = constants.isString(type) ? _structTypes[type] : type; + if (!structType) { + throw new Error(`Can't alias an ObjCStructType named '${type}' as it isn't defined.'`); + } + _structTypes[aliasName] = structType; // TO DO: confirm that storing type named 'Foo' under 'Bar' doesn't cause any problems due to mismatched names + } +} + + +/******************************************************************************/ +// for external use; exported as `objc.defineStruct`; unlike getClassByName, which fetches an existing ObjC Class that was defined when its ObjC framework was imported, this is equivalent to a C declaration: `typedef struct {...} NAME;` + +function defineStructType(encoding, ...names) { + // returns a new StructType object with name and properties described by the encoding string, creating and caching it as needed + // encoding : string -- an ObjC type encoding string for an ObjC struct type + // ...names : string -- zero or more [alias] names + // Result: ObjCStructType -- a StructType object for creating new instances of the specified struct + // if the StructType does not already exist, it is created and also stored on `objc` under both its name and its encoding for reuse + let type = _structTypes[encoding]; + if (!type) { + const typeParser = new objctypes.ObjCTypeEncodingParser(encoding); + type = typeParser.parseType(); + const objcEncoding = type.objcEncoding; + //console.log(`'${objcEncoding}'`, type.constructor.name) + if (!objcEncoding || objcEncoding[0] !== '{') { // quick-and-dirty check for a valid ObjC encoding string + throw new Error(`Expected an ObjC struct type encoding, got: '${encoding}'`); + } + } + for (let name of names) { + if (_structTypes[name] !== undefined) { + throw new Error(`Can't add ObjCStructType alias named '${name}' as it already exists.`); + } + _structTypes[name] = type; + } + return type; +} + + +function isStructType(value) { + return value instanceof ObjCStructType; +} + + +/******************************************************************************/ +// caution: there are circular dependencies between this module and objctypes, so don't use `module.exports = {...}` + +// used by ObjCTypeEncodingParser.readStructType, to add StructTypes to cache as they are parsed +module.exports.addStructType = addStructType; +module.exports.aliasStructType = aliasStructType; +module.exports.getStructTypeByName = (name) => _structTypes[name]; // used by `objc` Proxy +module.exports.ObjCStructType = ObjCStructType; // StructType (this has extended access to ObjC types, although I suspect this is only needed when specifying property types as strings); used by objctypes + +// public API; these are re-exported on `objc` +module.exports.defineStructType = defineStructType; +module.exports.isStructType = isStructType; +module.exports.isStruct = isStructType; // TO DO: check if ref-struct-di's StructType is also prototype for struct objects + + diff --git a/src/structs.js b/src/structs.js deleted file mode 100644 index f7df1ea..0000000 --- a/src/structs.js +++ /dev/null @@ -1,56 +0,0 @@ -const ref = require('ref-napi') -const struct = require('ref-struct-di')(ref); - -const CompoundInit = Symbol('structs.CompoundInit'); -const IsStructSymbol = Symbol(); -const structs = {}; - -const createStructInitializer = (name, StructType) => { - const fields = Object.getOwnPropertyNames(StructType.fields).sort((key0, key1) => { - return StructType.fields[key0].offset - StructType.fields[key1].offset; - }); - - StructType.new = function (...args) { - if (args.length === 0) { - return new StructType(); - } - - const retval = new StructType(); - - if (args.length === 2 && args[0] === CompoundInit) { - for (const [key, value] of Object.entries(args[1])) { - retval[key] = value; - } - } else { // Array-like init - if (fields.length !== args.length) { - throw new TypeError(`Invalid number of fields passed to '${name}' constructor. Expected ${fields.length}, got ${args.length}`); - } - args.forEach((arg, index) => { - retval[fields[index]] = arg; - }); - } - - return retval; - }; - StructType[IsStructSymbol] = true; - return StructType; -}; - -module.exports = { - CompoundInit, - - defineStruct: (name, fields) => { - if (name in structs) { - throw new Error(`Struct '${name}' is already defined`); - } - const type = struct(fields); - if (name !== null) { - structs[name] = type; - } - return createStructInitializer(name, type); - }, - - getStructType: name => structs[name], - - isStructFn: obj => obj[IsStructSymbol] === true -}; diff --git a/src/subclass.js b/src/subclass.js new file mode 100644 index 0000000..1e5e1e5 --- /dev/null +++ b/src/subclass.js @@ -0,0 +1,79 @@ +// subclass an ObjC class in JS + + +// TO DO: how to delegate to an ObjC superclass method from a JS method? + + +const ffi = require('ffi-napi'); +const ref = require('ref-napi'); + +const constants = require('./constants'); +const runtime = require('./runtime'); +const objctypes = require('./objctypes'); +const instance = require('./instance'); +const selectorNameFromJS = require('./selector').selectorNameFromJS; + + +const _retained = new Set(); // prevent GC collecting Callback objects + + +const addMethod = (dest, selector, encoding, fn) => { + // dest : object -- pointer to class to which this method will be added + // selector : object -- SEL + // encoding : string -- ObjC type encoding + // fn : function + const argTypes = objctypes.coerceObjCType(encoding); + const returnType = argTypes.shift(); + // TO DO: replace argTypes[1] with pointerType here? (there's no point unpacking it into Selector instance as it isn't used) + // TO DO: should probably check fn.length===argTypes-1 + const imp = ffi.Callback(returnType, argTypes, function(self, sel, ...args) { // eslint-disable-line new-cap + const retval = fn.apply(null, [self, ...args]); + + // TO DO: as with ObjC methods, any inout args need additional processing, although this time it works in reverse, updating Ref.__ptr with the new (packed) Ref.value + + return retval; + }); + _retained.add(imp); + return runtime.class_addMethod(dest, selector, imp, encoding); +}; + + + +module.exports.addMethod = addMethod; // also used by swizzle + +module.exports.defineClass = (name, superclass, encodings, methods) => { + // name : string -- the new ObjC class's name (this name must not already exist in global ObjC namespace) + // superclass : string | Class -- typically 'NSObject' + // encodings : {string} -- ObjC encoding types for class and instance methods + // methods : {function} -- class and instance methods + // Result: Class -- the newly defined class; also available as `objc.NAME` + // note: methods use JS-style names and class names are prefixed '$'; e.g. `$foo_` is a class method named `foo:` + if (!constants.isString(name)) { + throw new TypeError(`Expected string for ObjC class name, got ${typeof name}: ${name}`); + } + // caution: ObjC runtime requires each class name to be unique + if (runtime.classExists(name)) { throw new Error(`Can't create class ${name} as it already exists.`); } + let superPtr; + if (constants.isString(superclass)) { // superclass is a string containing name of an existing ObjC class + superPtr = runtime.objc_getClass(superclass); + } else if (!(typeof superclass === 'object' && (superPtr = superclass[constants.__objcClassPtr]))) { + throw new TypeError(`Expected an ObjC class or its name, got ${typeof superclass}: ${superclass}`); + } + const classPtr = runtime.objc_allocateClassPair(superPtr, name, 0); // TODO add ivar support? + runtime.objc_registerClassPair(classPtr); + // add class and instance methods + const classMethodsDest = runtime.object_getClass(classPtr), instanceMethodsDest = classPtr; + for (const methodName of Object.getOwnPropertyNames(methods)) { + const fn = methods[methodName]; + if (typeof fn !== 'function') { throw new Error(`Expected function for '${methodName}' method, got ${typeof fn}`); } + const encoding = encodings[methodName]; + if (!constants.isString(encoding)) { throw new Error(`Missing encoding for '${methodName}'`); } + const isClassMethod = methodName[0] === '$'; + const dest = isClassMethod ? classMethodsDest : instanceMethodsDest; + const selectorName = selectorNameFromJS(isClassMethod ? methodName.slice(1) : methodName); // e.g. 'foo:barBaz:' + const success = addMethod(dest, runtime.sel_getUid(selectorName), encoding, fn); + if (!success) { throw new Error(`Failed to add method '${methodName}' to ${runtime.class_getName(classPtr)}`); } + } + // return the new class for convenience, and for consistency with other `objc.defineTYPE` functions + return instance.wrapClass(classPtr); +}; diff --git a/src/swizzle.js b/src/swizzle.js index 711707b..edd1ec1 100644 --- a/src/swizzle.js +++ b/src/swizzle.js @@ -1,60 +1,32 @@ -const runtime = require('./runtime'); -const Block = require('./block'); - -const startCase = string => string[0].toUpperCase() + string.substr(1); - -// Swizzle a class or instance method -// cls: either a wrapped class (like what you get from `objc.NSDate`) or the classname as a string -// selector: selector of the method you want to swizzle, as a string -// fn: your new implementation, as a javascript function -// [type]: whether you want to swizzle a class or instance method -// returns a function that restores the original implementation -module.exports = (cls, selector, fn, type = 'instance') => { - if (!['instance', 'class'].includes(type)) { - throw new Error(`Invalid argument '${type}' passed as type`); - } - - if (typeof cls === 'string' || cls instanceof String) { - cls = runtime.objc_getClass(cls); - } else { - cls = cls.class(); - } - - if (type === 'class') { - cls = runtime.object_getClass(cls); - } - - const originalSelector = runtime.sel_getUid(selector); - const swizzledSelector = runtime.sel_getUid(`xxx__${selector}`); - - const getMethod = runtime[`class_get${startCase(type)}Method`]; - - const originalMethod = getMethod(cls, originalSelector); - // Create the block for the method's implementation - const returnType = runtime.method_copyReturnType(originalMethod); - const argc = runtime.method_getNumberOfArguments(originalMethod); - const argtypes = []; - - for (let i = 0; i < argc; i++) { - argtypes.push(runtime.method_copyArgumentType(originalMethod, i)); - } - - const block = new Block(fn, returnType, argtypes); - - const success = runtime.class_addMethod( - cls, - swizzledSelector, - runtime.imp_implementationWithBlock(block.makeBlock()), - runtime.method_getTypeEncoding(originalMethod) - ); - - if (!success) { - throw new Error(`Unable to add method '${selector}' to class ${runtime.class_getName(cls)}`); - } - - const swizzledMethod = getMethod(cls, swizzledSelector); - runtime.method_exchangeImplementations(originalMethod, swizzledMethod); - - return () => runtime.method_exchangeImplementations(originalMethod, swizzledMethod); +const constants = require('./constants'); +const runtime = require('./runtime'); +const subclass = require('./subclass'); + + +module.exports.swizzle = (classObject, selectorName, fn, isClassMethod = false) => { + // Swizzle a class or instance method + // classObject : ObjCClass -- wrapped ObjCClass, e.g. `objc.NSDate` + // selectorName : string -- NS-style name of the method to swizzle, e.g. "foo:barBaz:" + // fn : function -- the new implementation; this must match ObjC method's arguments and return value + // Result: function -- a swapMethods function that restores the original implementation + let dest = classObject[constants.__objcClassPtr]; + if (isClassMethod) { dest = runtime.object_getClass(dest); } + + const swizzledName = `xxx__${selectorName}`; // TO DO: is there a standard naming convention for swizzled methods? + const originalSelector = runtime.sel_getUid(selectorName); + const swizzledSelector = runtime.sel_getUid(swizzledName); + + const getMethod = isClassMethod ? runtime.class_getClassMethod : runtime.class_getInstanceMethod; + + const originalMethod = getMethod(dest, originalSelector); + const success = subclass.addMethod(dest, swizzledSelector, runtime.method_getTypeEncoding(originalMethod), fn); + if (!success) { throw new Error(`Unable to add method '${selectorName}' to class ${classObject}`); } + const swizzledMethod = getMethod(dest, swizzledSelector); + + const swapMethods = () => runtime.method_exchangeImplementations(originalMethod, swizzledMethod); + swapMethods.swizzledName = swizzledName; + swapMethods(); + return swapMethods; }; + diff --git a/src/type-encodings.js b/src/type-encodings.js deleted file mode 100644 index 867ee12..0000000 --- a/src/type-encodings.js +++ /dev/null @@ -1,270 +0,0 @@ -/* eslint-disable quote-props */ - -const ref = require('ref-napi'); -const structs = require('./structs'); - -// This file contains the following: -// - The `TypeEncodingParser` class, which is a simple recursive descent parser -// for parsing an objective-c type encoding string -// - DataStructure{Primitive|Pointer|Struct|Array|Union}, describing the parsed type encoding -// Notes: -// - The `DataStructure` classes all have a `toRefType` function, which returns -// a type object compatible with the `ffi`, `ref` and `ref-struct` modules -// - Currently, this is only supported for primitives, pointers and structs - -const types = { - 'c': 'char', - 'i': 'int32', - 's': 'short', - 'l': 'long', - 'q': 'longlong', - 'C': 'uchar', - 'I': 'uint32', - 'S': 'ushort', - 'L': 'ulong', - 'Q': 'ulonglong', - 'f': 'float', - 'd': 'double', - 'B': 'int8', - 'v': 'void', - '*': 'string', - '@': 'pointer', - '#': 'pointer', - ':': 'pointer', - '?': 'pointer' -}; - -const isNumber = arg => !isNaN(parseInt(arg, 10)); - -const guard = (cond, errorMessage) => { - if (!cond) { - throw new Error(errorMessage); - } -}; - -class DataStructure { - constructor() { - this.isConst = false; - } - - toRefType() { - throw new Error('should never reach here'); - } -} - -class DataStructurePrimitive extends DataStructure { - constructor(type) { - super(); - this.type = type; - } - - toRefType() { - const type = this.type === 'string' ? 'CString' : this.type; - const refType = ref.types[type]; - if (refType) return refType; // eslint-disable-line curly - throw new Error(`Unknown type ${type}`); - } -} - -class DataStructurePointer extends DataStructurePrimitive { - toRefType() { - return ref.refType(this.type.toRefType()); - } -} - -class DataStructureStruct extends DataStructure { - constructor(name, fields) { - super(); - this.name = name; - this.fields = fields; - } - - toRefType() { - const StructType = structs.getStructType(this.name); - if (StructType) { - return StructType; - } - throw new Error(`Missing struct definition for '${this.name}'`); - } - - static endDelimiter() { - return '}'; - } -} - -class DataStructureArray extends DataStructure { - constructor(length, type) { - super(); - this.length = length; - this.type = type; - } -} - -class DataStructureUnion extends DataStructureStruct { - static endDelimiter() { - return ')'; - } -} - -class TypeEncodingParser { - get currentToken() { - return this.encoding[this.position]; - } - - get nextToken() { - return this.encoding[this.position + 1]; - } - - step() { - this.position += 1; - } - - parse(encoding) { - this.encoding = encoding; - this.position = 0; - const type = this.parseType(); - guard(this.position === this.encoding.length, `Unable to parse type encoding '${encoding}'`); - return type; - } - - parseType() { - switch (this.currentToken) { - case '[': { - const type = this.parseArray(); - guard(this.currentToken === ']'); - this.step(); - return type; - } - - case '{': - case '(': { - const isUnion = this.currentToken === '('; - const type = this.parseStructOrUnion(isUnion ? DataStructureUnion : DataStructureStruct); - guard(this.currentToken === (isUnion ? ')' : '}')); - this.step(); - return type; - } - - case '^': - return this.parsePointer(); - - default: { - let retval; - - if (this.currentToken === 'r') { - this.step(); - const type = this.parseType(); - type.isConst = true; - retval = type; - } else if (this.currentToken === '@' && this.nextToken === '?') { - // `@?` is the encoding used for blocks. We simply return a void pointer - this.step(); - this.step(); - retval = new DataStructurePointer(new DataStructurePrimitive('void')); - } else if (types[this.currentToken]) { - let type; - const primitiveType = types[this.currentToken]; - if (primitiveType === 'pointer') { - type = new DataStructurePointer(new DataStructurePrimitive('void')); - } else { - type = new DataStructurePrimitive(primitiveType); - } - this.step(); - retval = type; - } else { - throw new Error(`Unexpected token ${this.currentToken}`); - } - - // Problem: type encodings for method parameters can contain offsets - // We can safely skip these since we don't actually need that data for creating the ref type - while (isNumber(this.currentToken)) { - this.step(); - } - return retval; - } - } - } - - // Array type encoding: `[LENGTH TYPE]` (without the space) - parseArray() { - this.step(); - - let length = 0; - let _char; - while ((_char = this.currentToken) && isNumber(_char)) { - length = (length * 10) + parseInt(_char, 10); - this.step(); - } - - guard(length > 0, 'Invalid array length'); - - const type = this.parseType(); - return new DataStructureArray(length, type); - } - - // Struct type encoding: `{name=type...}` - parseStructOrUnion(Type) { - this.step(); - - let typename = ''; - let _char; - - for (; (_char = this.currentToken) && _char !== '='; this.step()) { - typename += _char; - - if (_char === '?' && this.nextToken === Type.endDelimiter()) { - break; - } - } - this.step(); - - const fields = []; - while (this.currentToken !== Type.endDelimiter()) { - fields.push(this.parseType()); - } - - return new Type(typename, fields); - } - - // Pointer type encoding: `^type` - parsePointer() { - this.step(); - return new DataStructurePointer(this.parseType()); - } -} - -const parser = new TypeEncodingParser(); -const cachedParseResults = {}; - -module.exports = { - mapping: types, - TypeEncodingParser, - - DataStructurePrimitive, - DataStructurePointer, - DataStructureStruct, - DataStructureArray, - DataStructureUnion, - - coerceType: type => { - if (typeof type === 'string') { - if (type === 'pointer') { - return ref.refType(ref.types.void); - } - let parseResult = cachedParseResults[type]; - if (parseResult) { - return parseResult; - } - parseResult = parser.parse(type).toRefType(); - cachedParseResults[type] = parseResult; - return parseResult; - } else if (typeof type === 'object') { - return type; - } - if (structs.isStructFn(type)) { - return type; - } - - throw new TypeError(`Unable to coerce type from ${type}`); - } -}; diff --git a/src/types.js b/src/types.js deleted file mode 100644 index 87212a2..0000000 --- a/src/types.js +++ /dev/null @@ -1,16 +0,0 @@ -const ref = require('ref-napi'); -const structs = require('./structs'); - -const pointer = ref.refType(ref.types.void); -const id = pointer; -const NSInteger = ref.coerceType('int64'); -const NSUInteger = ref.coerceType('uint64'); - -module.exports = Object.assign({}, ref.types, { - pointer, id, NSInteger, NSUInteger, - - NSRange: structs.defineStruct('_NSRange', { - location: NSUInteger, - length: NSUInteger - }) -}); diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 5f20e4d..0000000 --- a/src/util.js +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable no-multi-assign */ - -const Selector = require('./selector'); -const Instance = require('./instance'); -const {InstanceProxy} = require('./proxies'); - -const retainedGlobals = []; -module.exports._retainGlobal = obj => { - retainedGlobals.push(obj); -}; - -const proxyForClass = classname => new InstanceProxy(new Instance(classname)); - -const ClassLoader = new Proxy({}, { - get: (_, classname) => proxyForClass(classname) -}); - -const { - NSBundle, NSDate, NSString, NSNumber, NSArray, NSMutableArray, NSDictionary, NSMutableDictionary -} = ClassLoader; - -module.exports.importFramework = name => { - const path = name.includes('/') ? name : `/System/Library/Frameworks/${name}.framework`; - const bundle = NSBundle.bundleWithPath_(path); - if (!bundle) { - throw new Error(`Unable to find bundle named '${name}'`); - } - return bundle.load(); -}; - -const js = module.exports.js = (object, returnInputIfUnableToConvert = false) => { - if (object.isKindOfClass_(NSString)) { - return object.UTF8String(); // eslint-disable-line new-cap - } - - if (object.isKindOfClass_(NSNumber)) { - return object.doubleValue(); - } - - if (object.isKindOfClass_(NSDate)) { - return new Date(object.timeIntervalSince1970() * 1000); - } - - if (object.isKindOfClass_(NSArray)) { - const newArray = []; - for (const obj of object) { - newArray.push(js(obj, true)); - } - return newArray; - } - - if (object.isKindOfClass_(NSDictionary)) { - const newObject = {}; - for (const key of object) { - newObject[String(key)] = js(object.objectForKey_(key), true); - } - - return newObject; - } - - // Return null if there's no JS counterpart for the objc type - return returnInputIfUnableToConvert ? object : null; -}; - -const ns = module.exports.ns = (object, hint = '@') => { - if (object.___is_instance_proxy === true) { - return object; - } - - // String -> {NSString|SEL|Class} - if (typeof object === 'string' || object instanceof String) { - // Convert to NSString, SEL or Class, depending on the hint - if (hint === '@') { - return NSString.stringWithUTF8String_(object); - } else if (hint === ':') { - return new Selector(object); - } - return proxyForClass(object); - } - - // Date -> NSDate - if (object instanceof Date) { - const secondsSince1970 = Number(object) / 1000; - return NSDate.dateWithTimeIntervalSince1970_(secondsSince1970); - } - - // Array -> NSArray - if (Array.isArray(object)) { - const newArray = NSMutableArray.array(); - - for (let i = 0; i < object.length; i++) { - newArray.addObject_(ns(object[i])); - } - return newArray; - } - - // Number -> NSNumber - if (typeof object === 'number') { - return NSNumber.numberWithDouble_(object); - } - - // Object -> NSDictionary - if (typeof object === 'object') { - const dictionary = NSMutableDictionary.new(); - - for (const key of Object.getOwnPropertyNames(object)) { - dictionary.setObject_forKey_(object[key], key); - } - - return dictionary; - } - - // Return null if there's no objc counterpart for the js type - return null; -}; diff --git a/test/pyobjc-speed-comparison.py b/test/pyobjc-speed-comparison.py new file mode 100755 index 0000000..3c4d636 --- /dev/null +++ b/test/pyobjc-speed-comparison.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +from Foundation import * +from datetime import datetime as dt + +n = 100_000 + +def test(): + v = NSString.stringWithString_('') + t = dt.now() + b = True + for i in range(n): + v = NSString.stringWithString_(str(i)) #and v + b = 1 and v.description() + print(b, 'P:', ((dt.now()-t).total_seconds()))# * 1_000) / n, 'ms') + +test() \ No newline at end of file diff --git a/test/test-codecs.js b/test/test-codecs.js new file mode 100755 index 0000000..2495583 --- /dev/null +++ b/test/test-codecs.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +const objc = require('objc') + +console.log(objc) + +let v + +v = objc.ns(['one', 2, false, {a:4}, new Date()]) + +console.log(v) // [objc ( one, 2, 0, { a = 4; }, "2022-02-19 16:12:45 +0000" )] + +// TO DO: booleans aren't roundtripping +console.log(objc.js(v)) // [ 'one', 2, 0, { a: 4 }, 2022-02-19T16:12:45.164Z ] + diff --git a/test/test-inout.js b/test/test-inout.js new file mode 100755 index 0000000..d081558 --- /dev/null +++ b/test/test-inout.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +const ref = require('ref-napi') +const objc = require('objc') + +//let v = objc.NSString.stringWithString_('') + + +objc.defineStruct('{AEDesc="descriptorType"I"dataHandle"?}'); + +// +objc.defineStruct('{OpaqueAEDataStorageType=}'); + + +let desc = objc.NSAppleEventDescriptor.descriptorWithString_('test') + +//console.log(desc.aeDesc()) + +const ptrType = ref.refType(ref.types.void) + +const b = Buffer.alloc(8) + +b.writeUInt64LE(Math.pow(2, 32)) + +console.log(b) + +const p = ref.alloc(ptrType) + +console.log(p) + +console.log(p.deref()) + +console.log(p.deref().deref()) + + +/* + +let code = "" + +//code = "2 + 2" + +let scpt = objc.NSAppleScript.alloc().initWithSource_(code) + + +console.log("did compile="+scpt.isCompiled()) +//console.log(objc.js(scpt.isCompiled())) // js() will throw type error here, as isCompiled method returns an ObjC primitive, not an ObjC object, so no unwrapping required +console.log("source code="+objc.js(scpt.source())) + + + +//let error = null //objc.NSString.stringWithString_('') + +let errorRef = new objc.Ref() + +let res = scpt.compileAndReturnError_(errorRef) + +console.log('RES =', res) + +error = errorRef.value + +console.log("error="+typeof error + ' ' + error instanceof objc.__internal__.ObjCObject) +if (error) { error = error.description().UTF8String() } + +console.log("compileAndReturnError: success=" + res + ' err=' + error) +//console.log('J: <'+v.description().UTF8String()+'>') + +console.log('test class: '+objc.NSString) + +*/ + diff --git a/test/test-js.js b/test/test-js.js new file mode 100755 index 0000000..7db5e11 --- /dev/null +++ b/test/test-js.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +const objc = require('objc') + +/* +Here's the error that was throwing when the method Proxy, on receiving a Symbol.toPrimitive key, tried to return self[Symbol.toPrimitive] - the resulting method's 'this' was being rebound to the Proxy, not to the ObjCClass which actually owned that method; the solution is for the Proxy always to return a function closure that it creates itself, which correctly captures the underlying ObjCObject as 'self', because 'this' cannot be left to its own devices - whether due to Proxy's own magic behavior or JS's general sloppiness as to what 'this' actually points to in any given context - + +method Proxy is looking up symbol: Symbol(Symbol.toPrimitive) +switching on toPrimitive +found it +method Proxy is looking up symbol: Symbol(__objcObject) +/Users/has/dev/javascript/objc/src/instance.js:380 + throw new Error(`BUG: ObjCClass[Symbol.toPrimitive] thinks 'this' is a method Proxy, not itself`); + ^ + +Error: BUG: ObjCClass[Symbol.toPrimitive] thinks 'this' is a method Proxy, not itself + at Proxy.[Symbol.toPrimitive] (/Users/has/dev/javascript/objc/src/instance.js:380:13) + at Object. (/Users/has/dev/javascript/objc/test/test-js.js:9:27) + at Module._compile (node:internal/modules/cjs/loader:1101:14) + at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10) + at Module.load (node:internal/modules/cjs/loader:981:32) + at Function.Module._load (node:internal/modules/cjs/loader:822:12) + at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) + at node:internal/main/run_main_module:17:47 + +*/ + +//objc.NSString.blahBlah +//console.log() + +console.log(`class representation: '${objc.NSString}'`) // "[ObjCClass: NSString]" -- this is currently returned by ObjCClass._toPrimitive_() + +/* +//console.log(Boolean) // [Function: Boolean] +class Foo extends Array {} +console.log(Foo) // [class Foo extends Array] +class Bar {} +console.log(Bar) // [class Bar] +*/ + +// console.log(objc.NSData.data()) // this needs workΩ as it's returning a raw inspection string of the ObjCInstance and its contents, which is NOT what we want to display + +let s = objc.NSString.stringWithString_('This is a test string.') + +// console.log(s) // this is raw inspect string (eventually we should implement [util.inspect.custom] methods that return a shorter, neater,friendlier representation) + +console.log() +console.log(s) // [objc: This is a test string.] +console.log() +console.log(`${s}`) // This is a test string. +console.log() +console.log(String(s)) // This is a test string. +console.log() + + + +// TO DO: getting representations seems to require an excessive number of ObjC method calls for what it does (objc.js is a particularly bad offender, for obvious reasons, though bouncing around toString,toStringTag,toPrimitive,etc does too) + +let v = 44 + +v = objc.NSMutableArray.arrayWithObject_(v) + +v = objc.ns(v) + +v.addObject_(objc.NSAppleScriptErrorAppName) + +console.log('v is ObjC instance = '+objc.isInstance(v)) + +// util.inspect uses hybrid representation: a JS-style "[objc:...]" containing ObjC description string +console.log(`ObjCInstance inspect:`, v) // ObjCInstance inspect: [objc: ( 44,NSAppleScriptErrorAppName )] + +// toString uses JS-style representation +console.log(`ObjCInstance toString: '${v}'`) // ObjCInstance toString: '44,NSAppleScriptErrorAppName' + + diff --git a/test/test-speed.js b/test/test-speed.js new file mode 100755 index 0000000..72f6b2a --- /dev/null +++ b/test/test-speed.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + + + +const objc = require('objc') + +let v + +v = objc.NSString.stringWithString_('') +v.description() + +let v2 = v + +console.log() + +// objc.NSAppleScriptErrorAppName + +objc.__internal__.types.reset() +objc.__internal__.reset() + +let t = process.hrtime.bigint() + +/* + +10K calls: + +TOTAL1.656526795sec +FFI: 1.620336908sec // time spend in ref-ffi ForeignFunction(...), including in codecs +REF: 0.90476087sec // time spent in ref-napi get(...) and set(...) codecs + +*/ + + +let i, n = 10000 // currently does 1K repeat calls in ~0.15sec, which is slow but usable; however, there is still significant overhead when converting argument and return values, operations which themselves involve calling multiple ObjC methods - currently each of those method invocations also goes through the full objc bridge, but since there's only a handful of standard JS types to bridge we could bypass the wrappers and use raw pointers to their corresponding ObjC classes, instances, and methods directly in ffi) -- this is still 10x slower than ffi-napi's factorial example + +let b = true + +for (i=0;i { test('description of null proxy', t => { - const obj = objc.allocRef(); + const obj = objc.allocRef(); // TO DO: replace this obj.__ptr = obj.__ptr.deref(); const description = util.inspect(obj); t.is(description, NULL_DESC);