Bug 513778 - Support passing JS functions as callbacks to C APIs. Part 3: Add closures for callback

support. r=benjamn
This commit is contained in:
Dan Witte 2010-03-29 09:38:17 -07:00
parent 1490a0cfa7
commit af9398763b
6 changed files with 367 additions and 39 deletions

View File

@ -83,7 +83,7 @@ static JSClass sCTypeProtoClass = {
"CType",
JSCLASS_HAS_RESERVED_SLOTS(CTYPEPROTO_SLOTS),
JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, CType::FinalizeProtoClass,
NULL, NULL, ConstructAbstract, ConstructAbstract, NULL, NULL, NULL, NULL
};
@ -114,6 +114,14 @@ static JSClass sCDataClass = {
NULL, NULL, FunctionType::Call, FunctionType::Call, NULL, NULL, NULL, NULL
};
static JSClass sCClosureClass = {
"CClosure",
JSCLASS_HAS_RESERVED_SLOTS(CCLOSURE_SLOTS) | JSCLASS_MARK_IS_TRACE,
JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, CClosure::Finalize,
NULL, NULL, NULL, NULL, NULL, NULL, JS_CLASS_TRACE(CClosure::Trace), NULL
};
#define CTYPESFN_FLAGS \
(JSFUN_FAST_NATIVE | JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)
@ -560,8 +568,9 @@ static JSBool
AttachProtos(JSContext* cx, JSObject* proto, JSObject** protos)
{
// For a given 'proto' of [[Class]] "CTypeProto", attach each of the 'protos'
// to the appropriate CTypeProtoSlot.
for (PRUint32 i = 0; i < CTYPEPROTO_SLOTS; ++i) {
// to the appropriate CTypeProtoSlot. (SLOT_UINT64PROTO is the last slot
// of [[Class]] "CTypeProto".)
for (PRUint32 i = 0; i <= SLOT_UINT64PROTO; ++i) {
if (!JS_SetReservedSlot(cx, proto, i, OBJECT_TO_JSVAL(protos[i])))
return false;
}
@ -2386,6 +2395,21 @@ CType::Finalize(JSContext* cx, JSObject* obj)
}
}
void
CType::FinalizeProtoClass(JSContext* cx, JSObject* obj)
{
// Finalize the CTypeProto class. The only important bit here is our
// SLOT_CLOSURECX -- it contains the JSContext that was (lazily) instantiated
// for use with FunctionType closures. And if we're here, in this finalizer,
// we're guaranteed to not need it anymore. Note that this slot will only
// be set for the object (of class CTypeProto) ctypes.FunctionType.prototype.
jsval slot;
if (!JS_GetReservedSlot(cx, obj, SLOT_CLOSURECX, &slot) || JSVAL_IS_VOID(slot))
return;
JS_DestroyContextNoGC(static_cast<JSContext*>(JSVAL_TO_PRIVATE(slot)));
}
void
CType::Trace(JSTracer* trc, JSObject* obj)
{
@ -4214,51 +4238,60 @@ FunctionType::ConstructData(JSContext* cx,
return JS_FALSE;
}
if (argc > 1) {
JS_ReportError(cx, "constructor takes zero or one argument");
return JS_FALSE;
}
JSObject* result = CData::Create(cx, obj, NULL, NULL, true);
if (!result)
return JS_FALSE;
*rval = OBJECT_TO_JSVAL(result);
if (argc == 1) {
// Construct from a raw pointer value.
if (!ExplicitConvert(cx, argv[0], obj, CData::GetData(cx, result)))
return JS_FALSE;
if (argc == 0) {
// Construct a null pointer.
return JS_TRUE;
}
return JS_TRUE;
}
if (argc == 1 || argc == 2) {
jsval arg = argv[0];
PRFuncPtr* data = static_cast<PRFuncPtr*>(CData::GetData(cx, result));
JSObject*
FunctionType::ConstructWithLibrary(JSContext* cx,
JSObject* typeObj,
JSObject* libraryObj,
PRFuncPtr fnptr)
{
JS_ASSERT(CType::IsCType(cx, typeObj));
JS_ASSERT(CType::GetTypeCode(cx, typeObj) == TYPE_function);
if (JSVAL_IS_OBJECT(arg) && JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(arg))) {
// Construct from a JS function, and allow an optional 'this' argument.
JSObject* thisObj = NULL;
if (argc == 2) {
if (JSVAL_IS_OBJECT(argv[1])) {
thisObj = JSVAL_TO_OBJECT(argv[1]);
} else if (!JS_ValueToObject(cx, argv[1], &thisObj)) {
return JS_FALSE;
}
}
// Create a CData object with the Library as a referent, for GC safety.
JSObject* result = CData::Create(cx, typeObj, libraryObj, &fnptr, true);
if (!result)
return NULL;
JSAutoTempValueRooter root(cx, result);
JSObject* fnObj = JSVAL_TO_OBJECT(arg);
JSObject* closureObj = CClosure::Create(cx, obj, fnObj, thisObj, data);
if (!closureObj)
return JS_FALSE;
JSAutoTempValueRooter root(cx, closureObj);
// Seal the CData object, to prevent modification of the function pointer.
// This permanently associates this object with the library, and avoids
// having to do things like remove the Library from SLOT_REFERENT when
// someone tries to change the pointer value.
// XXX This will need to change when bug 541212 is fixed -- CData::ValueSetter
// could be called on a sealed object.
if (!JS_SealObject(cx, result, JS_FALSE))
return NULL;
// Set the closure object as the referent of the new CData object.
if (!JS_SetReservedSlot(cx, result, SLOT_REFERENT,
OBJECT_TO_JSVAL(closureObj)))
return JS_FALSE;
return result;
// Seal the CData object, to prevent modification of the function pointer.
// This permanently associates this object with the closure, and avoids
// having to do things like reset SLOT_REFERENT when someone tries to
// change the pointer value.
// XXX This will need to change when bug 541212 is fixed -- CData::ValueSetter
// could be called on a sealed object.
return JS_SealObject(cx, result, JS_FALSE);
}
if (argc == 1) {
// Construct from a raw pointer value.
return ExplicitConvert(cx, arg, obj, data);
}
}
JS_ReportError(cx, "constructor takes 0, 1, or 2 arguments");
return JS_FALSE;
}
JSBool
@ -4421,6 +4454,202 @@ FunctionType::ABIGetter(JSContext* cx, JSObject* obj, jsval idval, jsval* vp)
return JS_TRUE;
}
/*******************************************************************************
** CClosure implementation
*******************************************************************************/
JSObject*
CClosure::Create(JSContext* cx,
JSObject* typeObj,
JSObject* fnObj,
JSObject* thisObj,
PRFuncPtr* fnptr)
{
JSObject* result = JS_NewObject(cx, &sCClosureClass, NULL, NULL);
if (!result)
return NULL;
JSAutoTempValueRooter root(cx, result);
// Get the FunctionInfo from the FunctionType.
FunctionInfo* fninfo = FunctionType::GetFunctionInfo(cx, typeObj);
nsAutoPtr<ClosureInfo> cinfo(new ClosureInfo());
if (!cinfo) {
JS_ReportOutOfMemory(cx);
return NULL;
}
// Get the prototype of the FunctionType object, of class CTypeProto,
// which stores our JSContext for use with the closure.
JSObject* proto = JS_GetPrototype(cx, typeObj);
JS_ASSERT(proto);
JS_ASSERT(JS_GET_CLASS(cx, proto) == &sCTypeProtoClass);
// Get a JSContext for use with the closure.
jsval slot;
ASSERT_OK(JS_GetReservedSlot(cx, proto, SLOT_CLOSURECX, &slot));
if (!JSVAL_IS_VOID(slot)) {
// Use the existing JSContext.
cinfo->cx = static_cast<JSContext*>(JSVAL_TO_PRIVATE(slot));
JS_ASSERT(cinfo->cx);
} else {
// Lazily instantiate a new JSContext, and stash it on
// ctypes.FunctionType.prototype.
JSRuntime* runtime = JS_GetRuntime(cx);
cinfo->cx = JS_NewContext(runtime, 8192);
if (!cinfo->cx) {
JS_ReportOutOfMemory(cx);
return NULL;
}
if (!JS_SetReservedSlot(cx, proto, SLOT_CLOSURECX,
PRIVATE_TO_JSVAL(cinfo->cx))) {
JS_DestroyContextNoGC(cinfo->cx);
return NULL;
}
}
cinfo->closureObj = result;
cinfo->typeObj = typeObj;
cinfo->thisObj = thisObj;
cinfo->jsfnObj = fnObj;
#ifdef DEBUG
cinfo->thread = PR_GetCurrentThread();
#endif
// Create an ffi_closure object and initialize it.
void* code;
cinfo->closure =
static_cast<ffi_closure*>(ffi_closure_alloc(sizeof(ffi_closure), &code));
if (!cinfo->closure || !code) {
JS_ReportError(cx, "couldn't create closure - libffi error");
return NULL;
}
ffi_status status = ffi_prep_closure_loc(cinfo->closure, &fninfo->mCIF,
CClosure::ClosureStub, cinfo, code);
if (status != FFI_OK) {
ffi_closure_free(cinfo->closure);
JS_ReportError(cx, "couldn't create closure - libffi error");
return NULL;
}
// Stash the ClosureInfo struct on our new object.
if (!JS_SetReservedSlot(cx, result, SLOT_CLOSUREINFO,
PRIVATE_TO_JSVAL(cinfo.get()))) {
ffi_closure_free(cinfo->closure);
return NULL;
}
cinfo.forget();
*fnptr = (PRFuncPtr) code;
return result;
}
void
CClosure::Trace(JSTracer* trc, JSObject* obj)
{
JSContext* cx = trc->context;
// Make sure our ClosureInfo slot is legit. If it's not, bail.
jsval slot;
if (!JS_GetReservedSlot(cx, obj, SLOT_CLOSUREINFO, &slot) ||
JSVAL_IS_VOID(slot))
return;
ClosureInfo* cinfo = static_cast<ClosureInfo*>(JSVAL_TO_PRIVATE(slot));
// Identify our objects to the tracer. (There's no need to identify
// 'closureObj', since that's us.)
JS_CALL_TRACER(trc, cinfo->typeObj, JSTRACE_OBJECT, "typeObj");
JS_CALL_TRACER(trc, cinfo->thisObj, JSTRACE_OBJECT, "thisObj");
JS_CALL_TRACER(trc, cinfo->jsfnObj, JSTRACE_OBJECT, "jsfnObj");
}
void
CClosure::Finalize(JSContext* cx, JSObject* obj)
{
// Make sure our ClosureInfo slot is legit. If it's not, bail.
jsval slot;
if (!JS_GetReservedSlot(cx, obj, SLOT_CLOSUREINFO, &slot) ||
JSVAL_IS_VOID(slot))
return;
ClosureInfo* cinfo = static_cast<ClosureInfo*>(JSVAL_TO_PRIVATE(slot));
if (cinfo->closure)
ffi_closure_free(cinfo->closure);
delete cinfo;
}
void
CClosure::ClosureStub(ffi_cif* cif, void* result, void** args, void* userData)
{
JS_ASSERT(cif);
JS_ASSERT(result);
JS_ASSERT(args);
JS_ASSERT(userData);
// Initialize the result to zero, in case something fails.
if (cif->rtype != &ffi_type_void)
memset(result, 0, cif->rtype->size);
// Retrieve the essentials from our closure object.
ClosureInfo* cinfo = static_cast<ClosureInfo*>(userData);
JSContext* cx = cinfo->cx;
JSObject* typeObj = cinfo->typeObj;
JSObject* thisObj = cinfo->thisObj;
JSObject* jsfnObj = cinfo->jsfnObj;
#ifdef DEBUG
// Assert that we're on the thread we were created from.
PRThread* thread = PR_GetCurrentThread();
JS_ASSERT(thread == cinfo->thread);
#endif
JSAutoRequest ar(cx);
// Assert that our CIFs agree.
FunctionInfo* fninfo = FunctionType::GetFunctionInfo(cx, typeObj);
JS_ASSERT(cif == &fninfo->mCIF);
// Get a death grip on 'closureObj'.
JSAutoTempValueRooter root(cx, cinfo->closureObj);
// Set up an array for converted arguments.
nsAutoTArray<jsval, 16> argv;
if (!argv.SetLength(cif->nargs)) {
JS_ReportOutOfMemory(cx);
return;
}
for (PRUint32 i = 0; i < cif->nargs; ++i)
argv[i] = JSVAL_VOID;
JSAutoTempValueRooter roots(cx, argv.Length(), argv.Elements());
for (PRUint32 i = 0; i < cif->nargs; ++i) {
// Convert each argument, and have any CData objects created depend on
// the existing buffers.
if (!ConvertToJS(cx, fninfo->mArgTypes[i], NULL, args[i], false, false,
&argv[i]))
return;
}
// Call the JS function. 'thisObj' may be NULL, in which case the JS engine
// will find an appropriate object to use.
jsval rval;
if (!JS_CallFunctionValue(cx, thisObj, OBJECT_TO_JSVAL(jsfnObj), cif->nargs,
argv.Elements(), &rval))
return;
// Convert the result. Note that we pass 'isArgument = false', such that
// ImplicitConvert will *not* autoconvert a JS string into a pointer-to-char
// type, which would require an allocation that we can't track. The JS
// function must perform this conversion itself and return a PointerType
// CData; thusly, the burden of freeing the data is left to the user.
ImplicitConvert(cx, rval, fninfo->mReturnType, result, false, NULL);
}
/*******************************************************************************
** CData implementation
*******************************************************************************/

View File

@ -110,6 +110,20 @@ struct FunctionInfo
nsTArray<ffi_type*> mFFITypes;
};
// Parameters necessary for invoking a JS function from a C closure.
struct ClosureInfo
{
JSContext* cx; // JSContext to use
JSObject* closureObj; // CClosure object
JSObject* typeObj; // FunctionType describing the C function
JSObject* thisObj; // 'this' object to use for the JS function call
JSObject* jsfnObj; // JS function
ffi_closure* closure; // The C closure itself
#ifdef DEBUG
PRThread* thread; // The thread the closure was created on
#endif
};
JSBool InitTypeClasses(JSContext* cx, JSObject* parent);
JSBool ConvertToJS(JSContext* cx, JSObject* typeObj, JSObject* dataObj, void* data, bool wantPrimitive, bool ownResult, jsval* result);
@ -135,6 +149,7 @@ enum CTypeProtoSlot {
SLOT_FUNCTIONDATAPROTO = 8, // common ancestor of all CData objects of FunctionType
SLOT_INT64PROTO = 9, // ctypes.Int64.prototype object
SLOT_UINT64PROTO = 10, // ctypes.UInt64.prototype object
SLOT_CLOSURECX = 11, // JSContext for use with FunctionType closures
CTYPEPROTO_SLOTS
};
@ -166,6 +181,11 @@ enum CDataSlot {
CDATA_SLOTS
};
enum CClosureSlot {
SLOT_CLOSUREINFO = 0, // ClosureInfo struct
CCLOSURE_SLOTS
};
enum TypeCtorSlot {
SLOT_FN_CTORPROTO = 0 // ctypes.{Pointer,Array,Struct}Type.prototype
// JSFunction objects always get exactly two slots.
@ -187,6 +207,7 @@ public:
static JSObject* DefineBuiltin(JSContext* cx, JSObject* parent, const char* propName, JSObject* typeProto, JSObject* dataProto, const char* name, TypeCode type, jsval size, jsval align, ffi_type* ffiType);
static void Trace(JSTracer* trc, JSObject* obj);
static void Finalize(JSContext* cx, JSObject* obj);
static void FinalizeProtoClass(JSContext* cx, JSObject* obj);
static JSBool ConstructAbstract(JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval);
static JSBool ConstructData(JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval);
@ -289,7 +310,7 @@ public:
static JSObject* CreateInternal(JSContext* cx, jsval abi, jsval rtype, jsval* argtypes, jsuint arglen);
static JSBool ConstructData(JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval);
static JSObject* ConstructWithLibrary(JSContext* cx, JSObject* typeObj, JSObject* libraryObj, PRFuncPtr fnptr);
static JSObject* ConstructWithObject(JSContext* cx, JSObject* typeObj, JSObject* refObj, PRFuncPtr fnptr, JSObject* result);
static JSBool Call(JSContext* cx, JSObject* obj, uintN argc, jsval* argv, jsval* rval);
@ -300,6 +321,15 @@ public:
static JSBool ABIGetter(JSContext* cx, JSObject* obj, jsval idval, jsval* vp);
};
class CClosure {
public:
static JSObject* Create(JSContext* cx, JSObject* typeObj, JSObject* fnObj, JSObject* thisObj, PRFuncPtr* fnptr);
static void Trace(JSTracer* trc, JSObject* obj);
static void Finalize(JSContext* cx, JSObject* obj);
static void ClosureStub(ffi_cif* cif, void* result, void** args, void* userData);
};
class CData {
public:
static JSObject* Create(JSContext* cx, JSObject* typeObj, JSObject* refObj, void* data, bool ownResult);

View File

@ -254,12 +254,19 @@ Library::Declare(JSContext* cx, uintN argc, jsval* vp)
return JS_FALSE;
JSAutoTempValueRooter root(cx, typeObj);
JSObject* fn = FunctionType::ConstructWithLibrary(cx, typeObj, obj, func);
JSObject* fn = CData::Create(cx, typeObj, obj, &func, true);
if (!fn)
return JS_FALSE;
JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(fn));
return JS_TRUE;
// Seal the CData object, to prevent modification of the function pointer.
// This permanently associates this object with the library, and avoids
// having to do things like reset SLOT_REFERENT when someone tries to
// change the pointer value.
// XXX This will need to change when bug 541212 is fixed -- CData::ValueSetter
// could be called on a sealed object.
return JS_SealObject(cx, fn, JS_FALSE);
}
}

View File

@ -320,3 +320,17 @@ test_fnptr()
return (void*)test_ansi_len;
}
PRInt32
test_closure_cdecl(PRInt8 i, PRInt32 (*f)(PRInt8))
{
return f(i);
}
#if defined(_WIN32) && !defined(__WIN64)
PRInt32
test_closure_cdecl(PRInt8 i, PRInt32 (NS_STDCALL *f)(PRInt8))
{
return f(i);
}
#endif /* defined(_WIN32) && !defined(__WIN64) */

View File

@ -187,5 +187,13 @@ NS_EXTERN_C
NS_EXPORT SEVEN_BYTE test_7_byte_struct_return(RECT);
NS_EXPORT void * test_fnptr();
NS_EXPORT PRInt32 test_closure_cdecl(PRInt8, PRInt32 (*)(PRInt8));
#if defined(_WIN32) && !defined(__WIN64)
NS_EXPORT PRInt32 test_closure_stdcall(PRInt8, PRInt32 (NS_STDCALL *)(PRInt8));
#endif /* defined(_WIN32) && !defined(__WIN64) */
NS_EXPORT PRInt32 test_callme(PRInt8);
NS_EXPORT void* test_getfn();
}

View File

@ -188,6 +188,7 @@ function run_test()
run_string_tests(library);
run_struct_tests(library);
run_function_tests(library);
run_closure_tests(library);
// test the string version of ctypes.open() as well
let libpath = libfile.path;
@ -2065,6 +2066,45 @@ function run_function_tests(library)
do_check_eq(ptrValue(test_ansi_len), ptrValue(ptr));
}
function run_closure_tests(library)
{
run_single_closure_tests(library, ctypes.default_abi, "cdecl");
#ifdef _WIN32
#ifndef _WIN64
run_single_closure_tests(library, ctypes.stdcall_abi, "stdcall");
#endif
#endif
}
function run_single_closure_tests(library, abi, suffix)
{
let b = 23;
function closure_fn(i)
{
return "a" in this ? i + this.a : i + b;
}
do_check_eq(closure_fn(7), 7 + b);
let thisobj = { a: 5 };
do_check_eq(closure_fn.call(thisobj, 7), 7 + thisobj.a);
// Construct a closure, and call it ourselves.
let fn_t = ctypes.FunctionType(abi, ctypes.int32_t, [ ctypes.int8_t ]);
let closure = fn_t(closure_fn);
do_check_eq(closure(-17), -17 + b);
// Have C code call it.
let test_closure = library.declare("test_closure_" + suffix,
ctypes.default_abi, ctypes.int32_t, ctypes.int8_t, fn_t);
do_check_eq(test_closure(-52, closure), -52 + b);
// Do the same, but specify 'this'.
let closure2 = fn_t(closure_fn, thisobj);
do_check_eq(closure2(-17), -17 + thisobj.a);
do_check_eq(test_closure(-52, closure2), -52 + thisobj.a);
}
// bug 522360 - try loading system library without full path
function run_load_system_library()
{