Mozilla.com

  1. User talk:Jorend

User talk:Jorend

This is what I think a "for dummies" SpiderMonkey C++ API might look like.

"But who is this for?" is an open question.

ezjs

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.
  • A 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.
  • A 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:

  • A temp value must not survive past the lifetime of the env that produced it.
  • A var object must not survive past the lifetime of the env passed to its constructor.

env

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, ...)
Run the given snippet of JavaScript code and return the resulting value.
If there are n additional arguments, convert each one to temp, create a custom scope with properties $0, $1 ... $(n-1) bound to the resulting temps, then evaluate code.
Examples:
  • 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
(Implementation note: 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.)
(Implementation note: This creates a temporary scope object whose __parent__ is the global object.)
evalv(const char *code, int argc, const temp *argv)
Same as eval(), but the caller passes in the array of already-converted arguments.
temp null()
Returns the JS value null.
temp undefined()
Returns the JS value undefined.
temp boolean(bool b)
Converts a C++ bool value to a JS boolean.
temp number(double n)
Converts a C++ double to a JS number.
temp string(const char *s)
Converts a null-terminated UTF-8 string to a JS string; this is exactly the same as e.string(s, strlen(s)).
temp string(const char *s, size_t len)
Converts a UTF-8 string to a JS string. 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.)
(Implementation note: This creates a UTF-16 copy of the string data, so it's O(len).)
temp function(const char *name, jsnative fn)
Create a JavaScript Function for fn. (FIXME: I guess the arity property will be 0.)
void unpack(const args &a, ...)
Each additional argument is a C++ reference. Convert the given arguments to the types of the references and assign a value to each reference.
(FIXME - Later I'll add stuff to cope with optional arguments and so forth, and specify in detail how the conversion works.)

Example

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;"
           "};");
}

Problems

  • API doesn't really do encapsulation, since it's leaning on JS so hard. Probably need a base class, and then we're slipping right into territory other APIs already cover!

Ideas

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

Tags:

Files (0)