nodobjc-x
Version:
The Node.js ⇆ Objective-C bridge
355 lines (319 loc) • 10.9 kB
JavaScript
/**
* 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 dependencies.
*/
var fs = require('fs')
, ref = require('ref')
, read = fs.readFileSync
, path = require('path')
, core = require('./core')
, Class = require('./class')
, join = path.join
, basename = path.basename
, exists = fs.existsSync || path.existsSync
, SUFFIX = '.framework'
, PATH = [
'/System/Library/Frameworks'
, '/System/Library/PrivateFrameworks'
, '/Library/Frameworks'
, process.env['HOME'] + '/Library/Frameworks'
]
, 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) {
return node.attrib['type' + typePrefix] || node.attrib['type' + typeSuffix];
}
function getValue(node) {
return node.attrib['value' + typePrefix] || node.attrib['value' + typeSuffix];
}
function fastIndexOf(subject, target, fromIndex) {
var length = subject.length, i = 0;
if (typeof fromIndex === 'number') {
i = fromIndex;
if (i < 0) {
i += length;
if (i < 0) i = 0;
}
}
for (; i < length; i++) if (subject[i] === target) return i;
return -1;
}
function parseAttrib(tag,attr) {
var coll = {}, split = 0, name = '', value = '', i=0;
attr = attr.split('\' ');
for(; i < attr.length; i++) {
split = fastIndexOf(attr[i], "=");
name = attr[i].substring(0, split);
value = attr[i].substring(split+2, (i==attr.length-1) ? attr[i].length-1 : undefined);
if(fastIndexOf(value, '&') != -1) value = value.replace(quoteRegExp, '"')
coll[name]=value;
}
return coll;
}
function parseTag(names, tag, content) {
var sattr = 2 + tag.name.length
, eattr = fastIndexOf(content,'>',1)
, isBodyless = (content[eattr - 1]=='/');
var sbody = eattr + 1
, ebody = isBodyless ? eattr - 1 : content.indexOf('</'+tag.name, eattr); // cannot use fastIndexOf
tag.end = isBodyless ? ebody + 2 : ebody + tag.name.length + 3;
tag.attrib = parseAttrib(tag,content.substring(sattr, eattr + (isBodyless ? -1 : 0)));
if(sbody == ebody || names[tag.name] == null) tag.children = [];
else tag.children = findTags(names[tag.name], content.substring(sbody,ebody));
return tag;
}
function findTags(names, content) {
var ndx = 0,
i = 0,
tagKeys = Object.keys(names),
ftags=[ { end: 1 } ],
key = '';
do {
content = content.substring(ndx);
for(i=0; i < tagKeys.length; i++) {
key = tagKeys[i];
// quick break for non-matching keys, cheaper on fails (which happen a lot)
// rather than a full substring. Three is both the max benefit and after that
// we'll cause some odd behavior (e.g., matching conditions)
if( content[1] == key[0] &&
content[2] == key[1] &&
content[3] == key[2])
{
if(key.length < 4 || key == content.substring(1,key.length+1)) {
delete ftags[ftags.length-1].end;
ftags.push(parseTag(names,{name:key},content));
break;
}
}
}
ndx = fastIndexOf(content, '<', ftags[ftags.length-1].end );
} while(ndx != -1);
ftags.shift();
return ftags;
}
var quoteRegExp = new RegExp(""","g");
/**
* 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 parseBridgeFile(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.lib = core.dlopen(bridgeSupportDylib);
var tags = {'function':{'retval':{'retval':null,'arg':null},'arg':{'retval':null,'arg':null}},'depends_on':null,'string_constant':null,'enum':null,'struct':null,'constant':null};
var nodes = findTags(tags, read(bridgeSupportXML, 'utf8'));
nodes.forEach(function (node) {
var name = node.attrib.name;
if(node.name == 'depends_on')
{
if(recursive && recursive > 0)
importFramework(node.attrib.path, true, onto, --recursive);
}
else if (node.name == 'string_constant')
{
onto[name] = getValue(node);
}
else if (node.name == 'enum')
{
if (node.attrib.ignore !== "true") onto[name] = parseInt(getValue(node));
}
else if (node.name == 'struct')
{
var type = getType(node);
onto[name] = core.Types.knownStructs[core.Types.parseStructName(type)] = type;
}
else if (node.name == 'constant')
{
var type = getType(node);
Object.defineProperty(onto, name, {
enumerable: true,
configurable: true,
get: function () {
var ptr = fw.lib.get(name);
var derefPtr = ref.get(ptr, 0, core.Types.map(type))
delete onto[name];
return onto[name] = core.wrapValue(derefPtr, type);
}
});
}
else if (node.name == 'function')
{
if(node.attrib.original) { // Support for function aliases
onto[node.attrib.original] = onto[node.attrib.name];
return;
}
var isInline = node.attrib.inline === 'true' ? true : false
, isVariadic = node.attrib.variadic === 'true' ? true : false
, passedTypes = {args:[],name:name};
node.children.forEach(function (n, i) {
var type = n.name;
if(type == 'arg') passedTypes.args.push(flattenNode(n));
else if (type == 'retval') passedTypes.retval = flattenNode(n);
});
Object.defineProperty(onto, name, {
enumerable: true,
configurable: true,
get: function () {
var ptr = fw.lib.get(name);
var unwrapper = core.createUnwrapperFunction(ptr, passedTypes, isVariadic);
delete onto[name];
return onto[name] = unwrapper;
}
});
}
});
}
function flattenNode (node) {
var rnode = {};
rnode.type = getType(node);
if (node.attrib.function_pointer === 'true') {
rnode.args = [];
node.children.forEach(function (n) {
if(n.name == 'arg') rnode.args.push(flattenNode(n));
else if(n.name == 'retval') rnode.retval = flattenNode(n);
});
}
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 = 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 core.wrapValue(rtnval,'@');
};
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);
var 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.
parseBridgeFile(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;
Object.defineProperty(onto, c, {
enumerable: true,
configurable: true,
get: function () {
var clazz = Class.getClassByName(c, onto);
delete onto[c];
return onto[c] = clazz;
}
});
});
}
}
/*!
* Module exports.
*/
exports.import = importFramework;
exports.resolve = resolve;
exports.PATH = PATH;
exports.allocReference = allocReference;
exports.createBlock = function (func, types) {
return core.wrapValue(core.createBlock(func, types), '@');
};