UNPKG

nodobjc-x

Version:

The Node.js ⇆ Objective-C bridge

327 lines (299 loc) 11.3 kB
module.exports = (function() { /** * Logic for importing a Framework into the node process. * * "Importing" a framework is a multi-step process: * * 1. `resolve()` the absolute path of the given framework name. * 1. Load the framework's binary `dylib` file into the process. * 1. Usually locate the `BridgeSupport` files for the framework and process. * 1. Define any new class getters for newly loaded Objective-C classes. */ /*! * Module exports. */ var ex = {}; /*! * Module dependencies. */ var fs = require('fs') , read = require('fs').readFileSync , assert = require('assert') , libxmljs = require('../../node_modules/libxmljs') , path = require('path') , core = require('../../lib/core') , Class = require('../../lib/class') , ID = require('../../lib/ID') , join = path.join , basename = path.basename , exists = fs.existsSync || path.existsSync , SUFFIX = '.framework' , PATH = [ '/System/Library/Frameworks' , '/System/Library/PrivateFrameworks' ] , join = path.join , exists = fs.existsSync || path.existsSync , DY_SUFFIX = '.dylib' , BS_SUFFIX = '.bridgesupport'; /*! * A cache for the frameworks that have already been imported. */ var cache = {} /*! * Architecture-specific functions that return the Obj-C type or value from one * of these BridgeSupport XML nodes. */ var typePrefix = (process.arch=='x64') ? '64' : ''; var typeSuffix = (process.arch=='x64') ? '' : '64'; function getType(node) { var v = node.attr('type'+typePrefix) || node.attr('type'+typeSuffix); return v.value(); } function getValue(node) { var v = node.attr('value'+typePrefix) || node.attr('value'+typeSuffix); return v.value(); } /** * This module takes care of loading the BridgeSupport XML files for a given * framework, and parsing the data into the given framework object. * * ### References: * * * [`man 5 BridgeSupport`](http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/BridgeSupport.5.html) * * [BridgeSupport MacOS Forge website](http://bridgesupport.macosforge.org) */ /** * Attempts to retrieve the BridgeSupport files for the given framework. * It synchronously reads the contents of the bridgesupport files and parses * them in order to add the symbols that the Obj-C runtime functions cannot * determine. */ function bridgesupport (fw, onto, recursive) { var bridgedir = join(fw.basePath, 'Resources', 'BridgeSupport') , bridgeSupportXML = join(bridgedir, fw.name + BS_SUFFIX) , bridgeSupportDylib = join(bridgedir, fw.name + DY_SUFFIX); // If there's no BridgeSupport file, then bail... if (!exists(bridgeSupportXML)) return; // Load the "inline" dylib if it exists if (exists(bridgeSupportDylib)) fw.inline = core.dlopen(bridgeSupportDylib); var contents = read(bridgeSupportXML, 'utf8') , doc = libxmljs.parseXmlString(contents) , nodes = doc.childNodes(); nodes.forEach(function (node) { var name = (node.attr('name')) ? node.attr('name').value() : null; switch (node.name()) { case 'depends_on': if(recursive && recursive > 0) importFramework(node.attr('path').value(), true, onto, --recursive); break; case 'string_constant': onto[name] = getValue(node); break; case 'enum': var ignore = node.attr('ignore'); if (!ignore || ignore.value() != "true") onto[name] = Number(getValue(node)); break; case 'struct': onto[name] = core.Types.getStruct(getType(node)); break; case 'constant': var type = getType(node); // This may seem strange but it helps us limit the amount of memory // so rather than actually loading the constant we only load it when // requested. onto.__defineGetter__(name, function () { var ptr = fw.lib.get(name); // TODO: Cache the pointer after the 1st call ptr._type = '^' + type; return ptr.deref(); }); break; case 'function': var isInline = node.attr('inline') && node.attr('inline').value() == 'true' ? true : false; var passedTypes = {}; passedTypes.args = []; passedTypes.name = name; node.childNodes().forEach(function (n, i) { var type = n.name(); switch (type) { case 'arg': passedTypes.args.push(flattenNode(n)); break; case 'retval': passedTypes.retval = flattenNode(n); break; default: break; } }); // This may seem strange that were redefining our own property on the // first execution (or access) of this property, but its due to that // the symbol won't exist until we've finished loading the framework // so we set it as a proxy, it also may help save a tiny amount of // memory and library loads. NOTE: no references to "node" can be in // side this function otherwise we'll leak all of the XML data in // memory and cause GC issues. Object.defineProperty(onto, name, { get:function() { // TODO: Handle 'variadic' arg functions (NSLog), will require // a "function generator" to get a Function from the passed // in args (and guess at the types that were passed in...) if (isInline) assert.ok(fw.inline, name+', '+fw.name+': declared inline but could not find inline dylib!'); var ptr = (isInline ? fw.inline : fw.lib).get(name); var unwrapper = core.createUnwrapperFunction(ptr, passedTypes); delete onto[name]; return onto[name] = unwrapper; } }); break; case 'text': case 'class': // classes are read in from runtime, not the bridge file. case 'opaque': case 'informal_protocol': case 'function_alias': case 'field': case 'cftype': break; default: throw new Error('unknown tag: '+ node.name); break; } delete node; }); // Ensure we completely remove references to the XML bridge files // otherwise we will leak a lot of memory. delete nodes; delete doc; delete contents; } function flattenNode (node) { var rnode = {}; rnode.type = getType(node); var functionPointer = node.attr('function_pointer'); if (functionPointer && functionPointer.value() === 'true') { rnode.function_pointer = 'true'; // XXX: Remove? Used by the function_pointer test case rnode.args = []; node.childNodes().forEach(function (n, i) { switch (n.name()) { case 'arg': rnode.args.push(flattenNode(n)); break; case 'retval': rnode.retval = flattenNode(n); break; default: break; } }) } return rnode; } /** * Accepts a single framework name and resolves it into an absolute path * to the base directory of the framework. * * In most cases, you will not need to use this function in your code. * * $.resolve('Foundation') * // '/System/Library/Frameworks/Foundation.framework' * * @param {String} framework The framework name or path to resolve. * @return {String} The resolved framework path. */ function resolve (framework) { // strip off a trailing slash if present if (framework[framework.length-1] == '/') framework = framework.slice(0, framework.length-1); // already absolute, return as-is if (~framework.indexOf('/')) return framework; var i=0, l=PATH.length, rtn=null; for (; i<l; i++) { rtn = join(PATH[i], framework + SUFFIX); if (exists(rtn)) return rtn; } throw new Error('Could not resolve framework: ' + framework); } /** * Allocates a new pointer to this type. The pointer points to `nil` initially. * This is meant for creating a pointer to hold an NSError*, and pass a ref() * to it into a method that accepts an 'error' double pointer. * XXX: Tentative API - name will probably change */ function allocReference(classWrap) { // We do some "magic" here to support the dereferenced // pointer to become an obj-c type. var ptr = core.REF.alloc('pointer', null); ptr._type = '@'; var _ref = ptr.ref; ptr.ref = function() { var v = _ref.call(ptr,arguments); var _deref = v.deref; v.deref = function() { var rtnval = _deref.call(v,arguments) return ID.wrap(rtnval, classWrap.classPointer); }; return v; } return ptr; } /** * Accepts a single framework name and imports it into the current node process. * `framework` may be a relative (singular) framework name, or a path (relative or * absolute) to a Framework directory. * * $.NSObject // undefined * * $.import('Foundation') * * $.NSObject // [Class: NSObject] * * @param {String} framework The framework name or path to load. */ function importFramework (framework, skip, onto, recursive) { framework=resolve(framework) var shortName=basename(framework, SUFFIX) // Check if the framework has already been loaded var fw=cache[shortName]; if (fw) return; // Load the main framework binary file var frameworkPath=join(framework, shortName), lib=core.dlopen(frameworkPath); fw={ lib:lib, name:shortName, basePath:framework, binaryPath:frameworkPath }; // cache before loading bridgesupport files cache[shortName] = fw; // Parse the BridgeSupport file and inline dylib, for the C functions, enums, // and other symbols not introspectable at runtime. bridgesupport(fw, onto, recursive); // Iterate through the loaded classes list and define "setup getters" for them. if (!skip) { var classes = core.getClassList(); classes.forEach(function (c) { if (c in onto) return; // This may seem odd but it helps to create the definition that pulls the // object on the first loop rather than load every class/obj into memory // that may not be needed. This is huge savings, difference of 60MB for // definitions vs. 500MB. onto.__defineGetter__(c, function () { var clazz = Class.getClassByName(c, onto); delete onto[c]; return onto[c] = clazz; }); }); } } /*! * Module exports. */ ex.bridgesupport = bridgesupport //ex.import = importFramework; ex.resolve = resolve; ex.framework = importFramework; ex.PATH = PATH; ex.allocReference = allocReference; ex.import = function (p,q) { q = (typeof(q) == 'undefined') ? 99999999 : q; importFramework(p,false,ex,q); }; return ex; })();