JSAPI Cookbook 编辑
This article shows the JSAPI equivalent for a tiny handful of common JavaScript idioms.
Note: The FOSS wiki page contains a few links to other libraries and programs that can make life easier when using SpiderMonkey and JSAPI.Basics
Working with values
The basic, undifferentiated value type in the JSAPI is JS::Value
. To query whether a value has a particular type, use a correspondingly named member testing function:
// JavaScript var v = computeSomeValue(); var isString = typeof v === "string"; var isNumber = typeof v === "number"; var isNull = v === null; var isBoolean = typeof v === "boolean"; var isObject = typeof v === "object" && v !== null;
/* JSAPI */ JS::RootedValue v(cx, ComputeSomeValue()); bool isString = v.isString(); bool isNumber = v.isNumber(); bool isInt32 = v.isInt32(); // NOTE: internal representation, not numeric value bool isNull = v.isNull(); bool isBoolean = v.isBoolean(); bool isObject = v.isObject(); // NOTE: not broken like typeof === "object" is :-)
To set a value use a correspondingly named member mutator function, or assign the result of the correspondingly named standalone function:
// JavaScript var v; v = 0; v = 0.5; v = someString; v = null; v = undefined; v = false;
/* JSAPI */ JS::RootedValue v(cx); JS::RootedString someString(cx, ...); v.setInt32(0); // or: v = JS::Int32Value(0); v.setDouble(0.5); // or: v = JS::DoubleValue(0.5); v.setString(someString); // or: v = JS::StringValue(someString); v.setNull(); // or: v = JS::NullValue(); v.setUndefined(); // or: v = JS::UndefinedValue(); v.setBoolean(false); // or: v = JS::BooleanValue(false);
Finding the global object
Many of these recipes require finding the current global object first.
// JavaScript var global = this;
There is a function, JS_GetGlobalForScopeChain(cx)
, that makes a best guess, and sometimes that is the best that can be done. But in a JSNative
the correct way to do this is:
/* JSAPI */ bool myNative(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JSObject *global = JS_GetGlobalForObject(cx, &args.callee()); ... }
Defining a function
// JavaScript function justForFun() { return null; }
/* JSAPI */ bool justForFun(JSContext *cx, unsigned argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); args.rval().setNull(); return true; } ... /* * Add this to your JSContext setup code. * This makes your C function visible as a global function in JavaScript. */ if (!JS_DefineFunction(cx, global, "justForFun", &justForFun, 0, 0)) return false;
To define many JSAPI functions at once, use JS_DefineFunctions
.
Creating an Array
// JavaScript var x = []; // or "x = Array()", or "x = new Array"
/* JSAPI */ JS::RootedObject x(cx, JS_NewArrayObject(cx, 0)); if (!x) return false;
Creating an Object
// JavaScript var x = {}; // or "x = Object()", or "x = new Object"
/* JSAPI */ JS::RootedObject x(cx, JS_NewPlainObject(cx)); // or JS_NewObject(cx, JS::NullPtr(), JS::NullPtr(), JS::NullPtr()); if (!x) return false;
Constructing an object with new
// JavaScript var person = new Person("Dave", 24);
It looks so simple in JavaScript, but a JSAPI application has to do three things here:
- look up the constructor,
Person
- prepare the arguments
("Dave", 24)
- call
JS_New
to simulate thenew
keyword
(If your constructor doesn't take any arguments, you can skip the second step and call JS_New(cx, constructor, 0, NULL)
in step 3.)
/* JSAPI */ /* Step 1 - Get the value of |Person| and check that it is an object. */ JS::RootedValue constructor_val(cx); if (!JS_GetProperty(cx, JS_GetGlobalObject(cx), "Person", &constructor_val)) return false; if (!constructor_val.isObject()) { JS_ReportError(cx, "Person is not a constructor"); return false; } JS::RootedObject constructor(cx, &constructor_val.toObject()); /* Step 2 - Set up the arguments. */ JS::RootedString name_str(cx, JS_NewStringCopyZ(cx, "Dave")); if (!name_str) return false; JS::AutoValueArray<2> args(cx); args[0].setString(name_str); args[1].setInt32(24); /* Step 3 - Call |new Person(...args)|, passing the arguments. */ JS::RootedObject obj(cx, JS_New(cx, constructor, args)); if (!obj) return false;
Calling a global JS function
// JavaScript var r = foo(); // where f is a global function
/* JSAPI * * Suppose the script defines a global JavaScript * function foo() and we want to call it from C. */ JS::RootedValue r(cx); if (!JS_CallFunctionName(cx, JS_GetGlobalObject(cx), "foo", 0, NULL, &r)) return false;
Calling a JS function via a local variable
// JavaScript var r = f(); // where f is a local variable
/* JSAPI * * Suppose f is a local C variable of type JS::Value. */ JS::AutoValueVector args(cx); // empty argument list JS::RootedValue r(cx); if (!JS_CallFunctionValue(cx, NULL, f, args, &r) return false;
Returning an integer
// JavaScript return 23;
/* JSAPI * * Warning: This only works for integers that fit in 32 bits. * Otherwise, convert the number to floating point (see the next example). */ JS::CallArgs args = JS::CallArgsFromVp(argc, vp); args.rval().setInt32(23); return true;
Returning a floating-point number
// JavaScript return 3.14159;
/* JSAPI */ JS::CallArgs args = JS::CallArgsFromVp(argc, vp); args.rval().setDouble(3.14159);
Exception handling
throw
The most common idiom is to create a new Error
object and throw that. JS_ReportError
does this. Note that JavaScript exceptions are not the same thing as C++ exceptions. The JSAPI code also has to return false
to signal failure to the caller.
// JavaScript throw new Error("Failed to grow " + varietal + ": too many greenflies.");
/* JSAPI */ JS_ReportError(cx, "Failed to grow %s: too many greenflies.", varietal); return false;
To internationalize your error messages, and to throw other error types, such as SyntaxError
or TypeError
, use JS_ReportErrorNumber
instead.
JavaScript also supports throwing any value at all, not just Error
objects. Use JS_SetPendingException
to throw an arbitrary JS::Value
from C/C++.
// JavaScript throw exc;
/* JSAPI */ JS_SetPendingException(cx, exc); return false;
When JS_ReportError
creates a new Error
object, it sets the fileName
and lineNumber
properties to the line of JavaScript code currently at the top of the stack. This is usually the line of code that called your native function, so it's usually what you want. JSAPI code can override this by creating the Error
object directly and passing additional arguments to the constructor:
// JavaScript throw new Error(message, filename, lineno);
/* JSAPI */ bool ThrowError(JSContext *cx, JSObject *global, const char *message, const char *filename, int32 lineno) { JSString *messageStr; JSString *filenameStr; JS::Value args[3]; JS::Value exc; messageStr = JS_NewStringCopyZ(cx, message); if (!messageStr) return false; filenameStr = JS_NewStringCopyZ(cx, filename); if (!filenameStr) return false; args[0] = STRING_TO_JSVAL(messageStr); args[1] = STRING_TO_JSVAL(filenameStr); args[2] = INT_TO_JSVAL(lineno); if (JS_CallFunctionName(cx, global, "Error", 3, args, &exc)) JS_SetPendingException(cx, exc); return false; } ... return ThrowError(cx, global, message, __FILE__, __LINE__);
The JSAPI code here is actually simulating throw Error(message)
without the new
, as new
is a bit harder to simulate using the JSAPI. In this case, unless the script has redefined Error
, it amounts to the same thing.
catch
// JavaScript try { // try some stuff here; for example: foo(); bar(); } catch (exc) { // do error-handling stuff here }
/* JSAPI */ /* try some stuff here; for example: */ if (!JS_CallFunctionName(cx, global, "foo", 0, NULL, &r)) goto catch_block; /* instead of returning false */ if (!JS_CallFunctionName(cx, global, "bar", 0, NULL, &r)) goto catch_block; /* instead of returning false */ return true; catch_block: js::RootedValue exc(cx); if (!JS_GetPendingException(cx, &exc)) return false; JS_ClearPendingException(cx); /* do error-handling stuff here */ return true;
finally
// JavaScript try { foo(); bar(); } finally { cleanup(); }
If your C/C++ cleanup code doesn't call back into the JSAPI, this is straightforward:
/* JSAPI */ bool success = false; if (!JS_CallFunctionName(cx, global, "foo", 0, NULL, &r)) goto finally_block; /* instead of returning false immediately */ if (!JS_CallFunctionName(cx, global, "bar", 0, NULL, &r)) goto finally_block; success = true; /* Intentionally fall through to the finally block. */ finally_block: cleanup(); return success;
However, if cleanup()
is actually a JavaScript function, there's a catch. When an error occurs, the JSContext
's pending exception is set. If this happens in foo()
or bar()
in the above example, the pending exception will still be set when you call cleanup()
, which would be bad. To avoid this, your JSAPI code implementing the finally
block must:
- save the old exception, if any
- clear the pending exception so that your cleanup code can run
- do your cleanup
- restore the old exception, if any
- return
false
if an exception occurred, so that the exception is propagated up.
/* JSAPI */ bool success = false; if (!JS_CallFunctionName(cx, global, "foo", 0, NULL, &r)) goto finally_block; /* instead of returning false immediately */ if (!JS_CallFunctionName(cx, global, "bar", 0, NULL, &r)) goto finally_block; success = true; /* Intentionally fall through to the finally block. */ finally_block: /* * Temporarily set aside any exception currently pending. * It will be automatically restored when we return, unless we call savedState.drop(). */ JS::AutoSaveExceptionState savedState(cx); if (!JS_CallFunctionName(cx, global, "cleanup", 0, NULL, &r)) { /* The new error replaces the previous one, so discard the saved exception state. */ savedState.drop(); return false; } return success;
Object properties
Getting a property
// JavaScript var x = y.myprop;
The JSAPI function that does this is JS_GetProperty
. It requires a JSObject *
argument. Since JavaScript values are usually stored in JS::Value
variables, a cast or conversion is usually needed.
In cases where it is certain that y
is an object (that is, not a boolean, number, string, null
, or undefined
), this is fairly straightforward. Use JSVAL_TO_OBJECT
to cast y
to type JSObject *
.
/* JSAPI */ JS::RootedValue x(cx); assert(y.isObject()); JS::RootedObject yobj(cx, &y.toObject()); if (!JS_GetProperty(cx, yobj, "myprop", &x)) return false;
That code will crash if y
is not an object. That's often unacceptable. An alternative would be to simulate the behavior of the JavaScript .
notation exactly. It's a nice thought—JavaScript wouldn't crash, at least—but implementing its exact behavior turns out to be quite complicated, and most of the work is not particularly helpful.
Usually it is best to check for !y.isObject()
and throw an Error
with a nice message.
/* JSAPI */ JS::RootedValue x(cx); if (!y.isObject()) return ThrowError(cx, global, "Parameter y must be an object.", __FILE__, __LINE__); /* see the #throw example */ JS::RootedObject yobj(cx, &y.toObject()); if (!JS_GetProperty(cx, yobj, "myprop", &x)) return false;
Setting a property
// JavaScript y.myprop = x;
See "Getting a property", above, concerning the case where y
is not an object.
/* JSAPI */ assert(y.isObject()); JS::RootedObject yobj(cx, &y.toObject()); if (!JS_SetProperty(cx, yobj, "myprop", &x)) return false;
Checking for a property
// JavaScript if ("myprop" in y) { // then do something }
See "Getting a property", above, concerning the case where y
is not an object.
/* JSAPI */ bool found; assert(y.isObject()); JS::RootedObject yobj(cx, &y.toObject()); if (!JS_HasProperty(cx, yobj, "myprop", &found)) return false; if (found) { // then do something }
Defining a constant property
This is the first of three examples involving the built-in function Object.defineProperty()
, which gives JavaScript code fine-grained control over the behavior of individual properties of any object.
You can use this function to create a constant property, one that can't be overwritten or deleted. Specify writable: false
to make the property read-only and configurable: false
to prevent it from being deleted or redefined. The flag enumerable: true
causes this property to be seen by for-in loops.
// JavaScript Object.defineProperty(obj, "prop", {value: 123, writable: false, enumerable: true, configurable: false});
The analogous JSAPI function is JS_DefineProperty
. The property attribute JSPROP_READONLY
corresponds to writeable: false
, JSPROP_ENUMERATE
to enumerable: true
, and JSPROP_PERMANENT
to configurable: false
. To get the opposite behavior for any of these settings, simply omit the property attribute bits you don't want.
/* JSAPI */ if (!JS_DefineProperty(cx, obj, "prop", INT_TO_JSVAL(123), JS_PropertyStub, JS_StrictPropertyStub, JSPROP_READONLY | JSPROP_ENUMERATE | JSPROP_PERMANENT)) { return false; }
Defining a property with a getter and setter
Object.defineProperty()
can be used to define properties in terms of two accessor functions.
// JavaScript Object.defineProperty(obj, "prop", {get: GetPropFunc, set: SetPropFunc, enumerable: true});
In the JSAPI version, GetPropFunc
and SetPropFunc
are C/C++ functions of type JSNative
.
/* JSAPI */ if (!JS_DefineProperty(cx, obj, "prop", JS::UndefinedValue(), (JSPropertyOp) GetPropFunc, (JSStrictPropertyOp) SetPropFunc, JSPROP_SHARED | JSPROP_NATIVE_ACCESSORS | JSPROP_ENUMERATE)) { return false; }
Defining a read-only property with only a getter
// JavaScript Object.defineProperty(obj, "prop", {get: GetPropFunc, enumerable: true});
In the JSAPI version, to signify that the property is read-only, pass NULL
for the setter.
/* JSAPI */ if (!JS_DefineProperty(cx, obj, "prop", JS::UndefinedValue(), GetPropFunc, NULL, JSPROP_SHARED | JSPROP_NATIVE_ACCESSORS | JSPROP_ENUMERATE)) { return false; }
Working with the prototype chain
Defining a native read-only property on the String.prototype
// JavaScript Object.defineProperty(String.prototype, "md5sum", {get: GetMD5Func, enumerable: true});
The following trick couldn't work if someone has replaced the global String object with something.
/* JSAPI */ JSObject *string; JSObject *string_prototype; JS::Value val; // Get the String constructor from the global object. if (!JS_GetProperty(cx, global, "String", &val)) return false; if (JSVAL_IS_PRIMITIVE(val)) return ThrowError(cx, global, "String is not an object", __FILE__, __LINE__); string = JSVAL_TO_OBJECT(val); // Get String.prototype. if (!JS_GetProperty(cx, string, "prototype", &val)) return false; if (JSVAL_IS_PRIMITIVE(val)) return ThrowError(cx, global, "String.prototype is not an object", __FILE__, __LINE__); string_prototype = JSVAL_TO_OBJECT(val); // ...and now we can add some new functionality to all strings. if (!JS_DefineProperty(cx, string_prototype, "md5sum", JS::UndefinedValue(), GetMD5Func, NULL, JSPROP_SHARED | JSPROP_NATIVE_ACCESSORS | JSPROP_ENUMERATE)) return false;
Wanted
- Simulating
for
andfor each
. - Actually outputting errors.
- How to write your own JSClass with reserved slots.
- Create global variable __dirname to retrieve the current JavaScript file name, like in NodeJS
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论