From af9398763b6a85981f1dadde60d3e5df66200d6b Mon Sep 17 00:00:00 2001 From: Dan Witte Date: Mon, 29 Mar 2010 09:38:17 -0700 Subject: [PATCH] Bug 513778 - Support passing JS functions as callbacks to C APIs. Part 3: Add closures for callback support. r=benjamn --- js/ctypes/CTypes.cpp | 301 ++++++++++++++++++++--- js/ctypes/CTypes.h | 32 ++- js/ctypes/Library.cpp | 11 +- js/ctypes/tests/jsctypes-test.cpp | 14 ++ js/ctypes/tests/jsctypes-test.h | 8 + js/ctypes/tests/unit/test_jsctypes.js.in | 40 +++ 6 files changed, 367 insertions(+), 39 deletions(-) diff --git a/js/ctypes/CTypes.cpp b/js/ctypes/CTypes.cpp index 1ee94fb3d98..9a437b97d1f 100644 --- a/js/ctypes/CTypes.cpp +++ b/js/ctypes/CTypes.cpp @@ -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(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(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 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(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_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(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(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(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 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 *******************************************************************************/ diff --git a/js/ctypes/CTypes.h b/js/ctypes/CTypes.h index e8b18271c38..67015f17ae2 100644 --- a/js/ctypes/CTypes.h +++ b/js/ctypes/CTypes.h @@ -110,6 +110,20 @@ struct FunctionInfo nsTArray 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); diff --git a/js/ctypes/Library.cpp b/js/ctypes/Library.cpp index 92ced3cfdd8..04e4f8b7836 100644 --- a/js/ctypes/Library.cpp +++ b/js/ctypes/Library.cpp @@ -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); } } diff --git a/js/ctypes/tests/jsctypes-test.cpp b/js/ctypes/tests/jsctypes-test.cpp index 8ba62428c21..52f5d6d4ae1 100644 --- a/js/ctypes/tests/jsctypes-test.cpp +++ b/js/ctypes/tests/jsctypes-test.cpp @@ -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) */ + diff --git a/js/ctypes/tests/jsctypes-test.h b/js/ctypes/tests/jsctypes-test.h index 7b6602ebe83..b3ad8c4f986 100644 --- a/js/ctypes/tests/jsctypes-test.h +++ b/js/ctypes/tests/jsctypes-test.h @@ -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(); } diff --git a/js/ctypes/tests/unit/test_jsctypes.js.in b/js/ctypes/tests/unit/test_jsctypes.js.in index c8adc6c57a3..64c181c3381 100644 --- a/js/ctypes/tests/unit/test_jsctypes.js.in +++ b/js/ctypes/tests/unit/test_jsctypes.js.in @@ -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() {