This is what I think a "for dummies" SpiderMonkey C++ API might look like.
"But who is this for?" is an open question.
The API exposes five types:
namespace ezjs {
class env;
class temp;
class var;
class args;
typedef temp (*native)(env &, const args &);
}
env is the environment. It can run JS code and do stuff with JS values.
temp is a temporary variable that can hold any JS value. The only caveat is that, like pointers to C++ locals, they go out of scope. Don't use a temp (by storing it in a global C++ variable, say) after the env that produced it goes out of scope.
var is a more durable JS value. Create one of these when you need to store a temp to use later.
args encapsulates the JavaScript arguments to a native function.
native is the type of native functions. You can write functions with this signature, then easily expose them to JavaScript.
(Implementation details:
env encapsulates JSRuntime and JSContext, and a local root scope. Ordinarily it's pretty lightweight, but the first one owns the JSRuntime.
native, of course, is the EZ JSNative.
temp is just a jsval. (For type-safety only, it's a small class instead of an uintptr.) Each gcthing exposed to the user as a temp is protected by the local root scope of an env. This turns out to be really easy to implement; it only has to be done explicitly at one point, in env::evalv().
var's constructor roots it.)
temp and var The only methods exposed by temp and var are simple type-checking methods.
namespace ezjs {
class temp {
public:
// exactly one of these is true:
bool is_undefined() const;
bool is_null() const;
bool is_boolean() const;
bool is_number() const;
bool is_string() const;
bool is_object() const;
};
class var {
public:
// constructors
var(env &e, temp x);
explicit var(env &e); // x defaults to `undefined`
// same public methods as class temp
};
}
temp and var both have the usual copy constructors and operator=. This means you can use either type with C++ STL containers. (But in the case of var, this creates a ton of roots and wastes time rooting and unrooting. Blah.) In addition, you can assign var = temp.
temp has a default constructor. The default temp is the JS value undefined.
Object lifetime rules:
temp value must not survive past the lifetime of the env that produced it.
var object must not survive past the lifetime of the env passed to its constructor.
namespace ezjs {
class env {
public:
env();
temp eval(const char *code, ...); // actually a type-safe flavor of "...", see below
temp evalv(const char *code, int argc, const temp *argv);
// converting C++ values to JS
temp null();
temp undefined();
temp boolean(bool b);
temp number(double n);
temp string(const char *s);
temp string(const char *s, size_t len);
temp function(const char *name, native fn);
// converting JS values to C++
void unpack(const args &a, ...); // type-safe "..." again
bool to_c_bool(temp v);
double to_c_double(temp v);
int to_c_int(temp v);
const char * to_c_str(temp v);
// get the value out of a var
temp get(const var &v);
};
}
eval(const char *code, ...)
code and return the resulting value.
temp, create a custom scope with properties $0, $1 ... $(n-1) bound to the resulting temps, then evaluate code.
e.eval("2+2") returns 4. (Or rather, a temp representing the number 4. The rest of these examples will ignore that distinction.)
e.eval("function sqr(x) { return x*x; }") creates a new global function sqr. It returns undefined.
e.eval("Math.abs($0)", -7) returns 7.
e.eval("Math.abs($0)", x) converts the C++ variable x, whatever type it may be, to a JavaScript value and then calls Math.abs on it. If x is a const char *, for example, this returns NaN.
e.eval("/^([^@]+)@([^@]+)$/.exec($0)", mystr), where mystr is either a const char * or a string temp, tries to match mystr against a regular expression and eval() has 10 signatures - each one is a C++ member function template with 0-9 parameters, and they're all __force_inline. boost-style. The header file will be ugly, and the compiler errors might be bizarre, but it's type-safe. And the compiler should generate good machine code for each call—adjacent slots on the stack for the args, call a conversion routine for each one that needs it. As for the ugliness, my idea is to document it so people won't have to look at the header file.)
__parent__ is the global object.)
evalv(const char *code, int argc, const temp *argv)
eval(), but the caller passes in the array of already-converted arguments.
temp null()
null.
temp undefined()
undefined.
temp boolean(bool b)
bool value to a JS boolean.
temp number(double n)
double to a JS number.
temp string(const char *s)
e.string(s, strlen(s)).
temp string(const char *s, size_t len)
len is the size of the string, in bytes. If s isn't valid UTF-8, this may throw an exception. (FIXME: Haven't yet figured out what to do in this case; depends on what the JSAPI provides.)
len).)
temp function(const char *name, jsnative fn)
Function for fn. (FIXME: I guess the arity property will be 0.)
void unpack(const args &a, ...)
using namespace ezjs;
temp file_open(env &e, args &a)
{
temp thisobj;
const char * filename;
const char * mode;
e.unpack(a, thisobj, filename, default(mode, "r"));
e.eval("if ($0._fileptr) $0.close();", thisobj);
FILE *f = fopen(filename, mode);
if (f == NULL)
e.eval("throw new Error($0)", strerror(errno)); // throws a C++ exception
e.eval("$0._fileptr = $1;", thisobj, e.???(f)); // hmm, encapsualation is painful - need an opaque type here
return e.undefined();
}
void initFileClass(env &e)
{
e.eval("function File(filename, mode) { this.open(filename, mode); }");
e.eval("File.prototype.open = $0;", e.function(file_open));
e.eval("File.prototype.close = $0;", e.function(file_close));
e.eval("File.prototype.read = $0;", e.function(file_read));
e.eval("File.prototype.readline = $0;", e.function(file_readline));
e.eval("File.prototype.lines = function() {"
" let line;"
" while ((line = this.readline()) != null)"
" yield line;"
"};");
}
I keep wanting to make env mostly implicit. This would make a really pleasant API. Two concerns: (a) it would require thread-local storage, which everyone claims is slow; (b) it would make GC-safety *too* implicit, since the lifetime of a temp is the lifetime of the env that created it.
Against (a), I claim thread-local storage is going to Really Work on the major platforms Real Soon Now. My theory is that accessing a thread-local variable will be a few fast load instructions in this brave new world.
(A moderately heinous alternative to TLS is just to carry around the env pointer with every value.)
Against (b), I claim that some complicated DEBUG-only machinery will give me enough asserting power to fix that right up.
Let's just fantasize about this API for a second:
namespace ezjs {
class temp {
public:
temp();
// Auto-convert from var to temp.
// (var could even be a subclass of temp; haven't thought it through.)
temp(const var &v);
explicit temp(double n);
explicit temp(const char *s);
temp(const char *s, size_t len);
// With these two operators, C++ code can say:
// if (argv[0]) doThis();
// or:
// if (!v) v = Array();
//
operator bool();
bool operator!();
// The equality operators would work the same as they do in JS.
// This means that identical strings would compare equal.
//
temp operator==(temp);
temp operator!=(temp);
...
// We could even provide the math operators, although this is
// pretty silly--they're painful enough to use in JS!
//
// Still, with operator overloading coming to ES4, it might be
// nice to have an easy way of spelling "a+b" without resorting
// to eval.
//
temp operator+(temp);
temp operator-();
temp operator-(temp);
temp operator*(temp);
...
// We can say `obj["prop"]` instead of `e.eval("$1.$2", obj, "prop")`.
//
// Instead of returning a temp, this could return something assignable,
// such that `obj["prop"] = foo` also works.
//
temp operator[](const char *);
temp operator[](temp);
// A property-checking method also seems useful.
bool has(const char *prop); // like JS `in`
// We can even call JS functions using good old
// function-call syntax.
temp operator()(...); // type-safe varargs
// In addition to isString(), isNumber(), etc., we can
// actually provide a method that works exactly like the JS
// `instanceof` keyword.
bool instanceof(temp);
// We can even provide common methods from JS.
bool hasOwnProperty(temp);
};
env is implicit in pretty much all of these operations.
The point of all this is not "Look, we can re-create JS in C++!!111", but rather "Look how much easier it is to write native functions that really interact with JS." You can, for example, easily write a native method that takes a JS callback, like the addModeHook function here:
function fixTheStupidDefaults() {
...
}
addModeHook("C", fixTheStupidDefaults);
addModeHook("C++", fixTheStupidDefaults);
Calling that callback then looks quite natural. You just call it as though it were a C++ function. Can't do that with my explicit-env API or with the JSAPI.
Likewise, how about a native method that takes a JS "options" object:
var pf = createPacketFilter({protocol:'tcp', port:80});
This too is a lot easier in this style of API.
temp createPacketFilter(temp this, int argc, temp *argv) {
temp options = argv[0];
PacketFilter *pf = new PacketFilter;
if (options.has("protocol"))
pf->setProtocol(parseProtocol(options["protocol"]));
if (options.has("port"))
pf->setPort((uint16_t) Number(options["port"]));
if (options.has("flags"))
pf->setFlags(Number(options["port"]));
return pf;
}
Page last modified 16:54, 23 Jan 2008 by Jorend