What, you may ask, is js-ctypes? Let’s say you’re writing a Firefox extension in JavaScript that needs to call into native code. (For example, weave-crypto, which needs to call into the NSS library. Or your extension here, which, say, wants to call into NSPR, libc, or Win32 functions directly.) Currently, you’re limited to either a) using the scriptable XPCOM interfaces provided by libxul, or b) writing and implementing your own XPCOM interfaces and shipping binary code with your extension. If a) is inadequate, you’re stuck with b), which makes shipping an extension much harder — binary code must be built for each supported platform, and rolled up together into your cross-platform xpi.
Thusly, the answer is js-ctypes: it allows JavaScript to call into native C code and manipulate data types, without using XPCOM and without compiling a line of code. This means you don’t need to define XPCOM interfaces, and can use shared libraries like libc directly. As a side benefit, much of the type conversion overhead of XPConnect is avoided, so that execution can be faster. (I’ll be benchmarking in a later post.) This will ship with Gecko 1.9.3, which will be underpinning Firefox 4.
But how, you say? To the examples (tested on 32-bit x86 Linux; sans cross-platform safety):
1. Opening a library.
// First, import the ctypes module.
Components.utils.import("resource://gre/modules/ctypes.jsm");
// Open libc.
let library = ctypes.open("libc.so.6");
// Declare the 'fopen' function, which has a C prototype of:
// FILE* fopen(const char* name, const char* mode);
let fopen = library.declare("fopen", // symbol name
ctypes.default_abi, // cdecl calling convention
ctypes.StructType("FILE").ptr, // return type (FILE*)
ctypes.char.ptr, // first arg (const char*)
ctypes.char.ptr); // second arg (const char*)
// Call the function, and get a FILE* pointer object back.
let file = fopen("hello world.txt", "w");
// What is 'file'?
file.toString();
===> "ctypes.StructType("FILE").ptr(ctypes.UInt64("0x9781b38"))" // pointer value
// ... write data to file here ...
2. Declaring and using structs and arrays.
// Declare the 'hostent' struct, which contains five fields, each with their own
// types. This corresponds to the C declaration:
// struct hostent {
// char* h_name; // name of host
// char** h_aliases; // array of strings representing aliases of the host
// int h_addrtype; // whether the IP address is IPv4 or IPv6
// int h_length; // length, in bytes, of the IP address
// char** h_addr_list; // array of IP addresses from name server
// };
let hostent = ctypes.StructType("hostent",
[{ h_name : ctypes.char.ptr },
{ h_aliases : ctypes.char.ptr.ptr },
{ h_addrtype : ctypes.int },
{ h_length : ctypes.int },
{ h_addr_list : ctypes.uint8_t.array(4).ptr.ptr }]);
// Declare the 'gethostbyname' function, which has a C prototype of:
// struct hostent* gethostbyname(const char* name);
let gethostbyname = library.declare("gethostbyname",
ctypes.default_abi,
hostent.ptr, // use our 'hostent' type
ctypes.char.ptr);
// Ask our function to resolve a hostname.
let google = gethostbyname("mail.google.com");
// Dereference the pointer to 'hostent' struct, access the 'h_name' field, and
// convert it to a JS string. This is roughly equivalent to the C expression:
// printf("%s", google->h_name);
google.contents.h_name.readString();
===> "googlemail.l.google.com"
// Dereference the 'h_addr_list' field twice to get the first element in the
// array, which is an array of 4 bytes representing the IPv4 address of the host.
// Roughly equivalent C:
// printf("%u.%u.%u.%u", (int) h_addr_list[0][0], (int) h_addr_list[0][1],
// (int) h_addr_list[0][2], (int) h_addr_list[0][3]);
google.contents.h_addr_list.contents.contents.toString();
===> "ctypes.uint8_t.array(4)([74, 125, 19, 17])" // 74.125.19.17
3. Creating C function pointers for JS functions.
// Declare the type of a comparator function that takes two pointers to elements,
// and returns:
// -1 if i < j;
// 0 if i == j;
// 1 if i > j.
// Equivalent C:
// typedef int (comparator_t*)(const int8_t* i, const int8_t* j);
let comparator_t = ctypes.FunctionType(ctypes.default_abi, ctypes.int,
ctypes.int8_t.ptr, ctypes.int8_t.ptr).ptr;
// What's the C type of 'comparator_t'?
comparator_t.name;
===> "int (*)(int8_t*,int8_t*)"
// Declare the 'qsort' function which takes an array of elements and a comparator
// function, and sorts the array.
// void qsort(void* array, size_t length, size_t elemsize, comparator_t comp);
let qsort = library.declare("qsort", ctypes.default_abi, ctypes.void_t,
ctypes.voidptr_t, ctypes.size_t, ctypes.size_t, comparator_t);
// Implement a JS function that looks just like 'comparator_t' above.
function reverse(i, j) { return j.contents - i.contents; }
// Construct a C function pointer from our JS function.
let reverse_ptr = comparator_t(reverse);
// What's 'reverse_ptr'?
reverse_ptr.toString();
===> "ctypes.FunctionType(ctypes.default_abi, ctypes.int, ctypes.int8_t.ptr,
ctypes.int8_t.ptr).ptr(ctypes.UInt64("0x81a430cb"))"
// Construct an array of values and call 'qsort'.
let array_t = ctypes.int8_t.array();
let ints = array_t([3, 1, 5, 6, 4, 2]);
qsort(ints.address(), ints.length, array_t.elementType.size, reverse_ptr);
// Voilà!
ints.toString();
===> "ctypes.int8_t.array(6)([6, 5, 4, 3, 2, 1])"
So, if you’re an extension author, or writing code for the browser, keep js-ctypes in mind — and let us know how it goes!
Edit 8/7/10: updated syntax for API changes.