UNPKG

nodobjc-x

Version:

The Node.js ⇆ Objective-C bridge

320 lines (281 loc) 7.94 kB
/** * Logic for translating a given Objective-C "type" encoding into a node-ffi * type. * * ### References: * * * [Apple "Type Encoding" Docs](http://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html) * * [node-ffi "Type List"](https://github.com/rbranson/node-ffi/wiki/Node-FFI-Tutorial#wiki-type-list) */ /** * Module dependencies. */ /** * Internal module that parses *"struct type encodings"* and turns them into * proper node-ffi `Struct` instances. */ /*! * Module dependencies. */ var Struct = require('ref-struct'); var assert = require('assert'); var test = /^\{.*?\}$/; var structCache = {}; var knownStructs = {}; /** * Tests if the given arg is a `Struct` constructor or a string type encoding * describing a struct (then true), otherwise false. * * @api private */ function isStruct (type) { return !!type.__isStructType__ || test.test(type) } /** * Returns the struct constructor function for the given struct name or type. * * {CGPoint="x"d"y"d} * * @api private */ function getStruct (type) { // First check if a regular name was passed in var rtn = structCache[type]; if (rtn) return rtn; // If the struct type name has already been created, return that one var name = parseStructName(type); rtn = structCache[name]; if (rtn) return rtn; if(knownStructs[name]) type = knownStructs[name]; // Next parse the type structure var parsed = parseStruct(type); // Otherwise we need to create a new Struct constructor var props = []; parsed.props.forEach(function (prop) { props.push([ map(prop[1]), prop[0] ]) }); var z; try { z = Struct(props); } catch(e) { } return structCache[parsed.name] = z; } /** * Extracts only the name of the given struct type encoding string. * * @api private */ function parseStructName (struct) { var s = struct.substring(1, struct.length-1) , equalIndex = s.indexOf('=') if (~equalIndex) s = s.substring(0, equalIndex) return s } /** * Parses a struct type string into an Object with a `name` String and * a `props` Array (entries are a type string, or another parsed struct object) * * @api private */ function parseStruct (struct) { var s = struct.substring(1, struct.length-1) , equalIndex = s.indexOf('=') , rtn = { name: s.substring(0, equalIndex) , props: [] } s = s.substring(equalIndex+1); var curProp = [], numBrackets = 0, entries = []; for (var i=0; i < s.length; i++) { var cur = s[i]; switch (cur) { case '"': if (numBrackets > 0) curProp.push(cur); else { entries.push(curProp.join('')); curProp = []; numBrackets = 0; } break; case '{': case '[': case '(': numBrackets++; curProp.push(cur); break; case '}': case ']': case ')': numBrackets--; curProp.push(cur); break; default: curProp.push(cur); break; } } entries.push(curProp.join('')); curProp = []; numBrackets = 0; for (var i=1; i < entries.length; i+=2) { rtn.props.push([entries[i], entries[i+1]]); } return rtn } /** * A map of Objective-C type encodings to node-ffi types. * * @api private */ var typeEncodings = { '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' // String , '@': 'pointer' // id , '#': 'pointer' // Class , ':': 'pointer' // SEL , '?': 'pointer' // Unknown, used for function pointers } var DELIMS = Object.keys(typeEncodings) /** * A map of the additional type info for some ObjC methods. * * @api private */ var methodEncodings = { 'r': 'const' , 'n': 'in' , 'N': 'inout' , 'o': 'out' , 'O': 'bycopy' , 'R': 'byref' , 'V': 'oneway' } /** * Used to remove and method encodings present on the type. * NodObjC does not use them... */ var methodEncodingsTest = new RegExp('^(' + Object.keys(methodEncodings).join('|') + ')') /** * Maps a single Obj-C 'type' into a valid node-ffi type. * This mapping logic is kind of a mess... */ function map (type) { if (!type) throw new Error('got falsey "type" to map ('+type+'). this should NOT happen!'); if (type.type) type = type.type; if (isStruct(type)) return getStruct(type); type = type.replace(methodEncodingsTest, '') // if the first letter is a ^ then it's a "pointer" type if (type[0] === '^') return 'pointer' // now we can try matching from the typeEncodings map var rtn = typeEncodings[type] if (rtn) return rtn // last shot... try the last char? this may be a bad idea... rtn = typeEncodings[type[type.length-1]] if (rtn) return rtn // couldn't find the type. throw a descriptive error as to why: if (type[0] == '[') return 'pointer'; //throw new Error('Array types not yet supported: ' + type) if (type[0] == '(') return 'pointer'; //throw new Error('Union types not yet supported: ' + type) if (type[0] == 'b') return 'pointer'; //throw new Error('Bit field types not yet supported: ' + type) throw new Error('Could not convert type: ' + type) } /** * Accepts an Array of ObjC return type and argument types (i.e. the result of * parse() below), and returns a new Array with the values mapped to valid ffi * types. */ function mapArray (types) { return types.map(function (type) { return Array.isArray(type) ? mapArray(type) : map(type) }) } /** * This normalizes or finds objects (id's) and for the type returns back an * objective-c type of '@' or '#' */ function normalizeObjects(types) { var ret = types[0].type ? types[0].type : types[0]; var args = types[1].map(function(item) { return item.type ? item.type : item; }); return [ret, args]; } /** * Parses a "types string" (i.e. `'v@:'`) and returns a "types Array", where the * return type is the first array value, and an Array of argument types is the * array second value. */ function parse (types) { if (typeof types === 'string') { var rtn = [] , cur = [] , len = types.length , depth = 0 for (var i=0; i<len; i++) { var c = types[i] if (depth || !/(\d)/.test(c)) { cur.push(c) } if (c == '{' || c == '[' || c == '(') { depth++ } else if (c == '}' || c == ']' || c == ')') { depth-- if (!depth) add() } else if (~DELIMS.indexOf(c) && !depth) { add() } } function add () { rtn.push(cur.join('')) cur = [] depth = 0 } assert.equal(rtn[1], '@', '_self argument expected as first arg: ' + types) assert.equal(rtn[2], ':', 'SEL argument expected as second arg: ' + types) return [ rtn[0], rtn.slice(1) ] } else if (Array.isArray(types)) { return normalizeObjects(types); } else { var args = types.args assert.equal(args[0], '@', '_self argument expected as first arg: ' + types) assert.equal(args[1], ':', 'SEL argument expected as second arg: ' + types) return [ types.retval, args ] } } /** * @private * This registers the internal type maps for objc->ffi on an object * that's passed in. */ function registerTypes(onto) { for(var i=0; i < DELIMS.length; i++) { if(DELIMS[i] != 'pointer') { onto[typeEncodings[DELIMS[i]]] = DELIMS[i]; } } } exports.registerTypes = registerTypes; exports.map = map; exports.mapArray = mapArray; exports.parse = parse; exports.typeEncodings = typeEncodings; exports.methodEncodings = methodEncodings; exports.isStruct = isStruct; exports.getStruct = getStruct; exports.parseStructName = parseStructName; exports.parseStruct = parseStruct; exports.knownStructs = knownStructs;