here is some more text on the topic...
'enum' is real sticky. While powerful in C++ as sets of valid values and a 'types' for params, they are a mess in typelibs and, I think, should not be used as types for params in xpcom. A core problem is that the sizeof a given enum is something that the C++ compiler can decide based on the range of values for the enum. This makes them hard to specify in the typelib or to use safely in xpconnect. I say that they are evil as part of the contract between xpcom components. Another issue is that corba idl (upon which the xpidl compiler is built) requires named enum (no anonymous enums) and does *not* allow setting individual values; e.g. 'enum foo{a,b};' is valid but 'enum {a,b};' and 'enum foo {a=-1,b=50};' are not valid. xpidl *had* supported writing enums into the generated C++ headers, but not into typelibs. I removed that support and added stubs to the compiler that will do a -w warning if enums are used to let users know that they are being ignored. Instead of using enums for constant numerical identifiers in xpidl I propose that we use 'const's. shaver *had* support for many types of consts when generating typelibs (based on the typelib spec) and no support (yet) when generating headers. I decided that the *only* types of consts we need are those that can be supported in both headers and typelibs and that really important in the real world: integer consts. String consts are not supported - they are a pain to do safely in C++ without generating C++ implementation files, and would force linkage across modules (or duplication of strings). Any interface that wants the effect of string consts can declare string attributes and have some implementation of the interface return the strings. Yes, this does not declare the values in the interface declaration, but I argue that this is *good enough*. Floating point constants are messy in C++, not as common, and are not supported here. Integer constants are much more important and are supportable as part of the interface. I decided to implement support only for shorts and longs (16 and 32 bit integers) signed and unsigned. These are only supported *inside* interfaces (xpidl will -w warn when consts are seen outside interfaces). They look like: const short some_name = -1; When generating the C++ header this generates: enum { some_name = -1 }; So, constants are always available in C++ as iface_name::const_name. They are written into the typelib and are available in JavaScript via xpconnect as named properties on any instance of the given interface. xpidl supports some arithmetic in const declarations. This is resolved *before* emitting C++ headers. This includes things like: const short c1 = 1+1; const short c2 = c1 * 5; const short flag = 1 << 5; which produces: enum { c1 = 2 }; enum { c2 = 10 }; enum { flag = 32 };
[scriptable, uuid(some uuid)] interface nsIOutParam : nsISupports { string methodHasOutParam(out PRInt32 oob); };you can get the method result and out parameter value by passing an object and examining the 'value' property:
var nsio; // instance supporting nsIOutParam var outobj = new Object(); // make an empty JavaScript object // 'result' is the method's return value, a string; var result = nsio.methodHasOutParam(outobj); // 'outvalue' is the new value of the 'oob' out parameter, a number. var outvalue = outobj.value; // get the newly-set 'value' property.
inout params are reflected into JS, but the syntax is different from the syntax used for either in or out params. For out params xpconnect remaps the C++ result to be the return value when called by JS. This is as expected. But JS has no smooth intrinsic way to do inout because JS has no pointers. So for inout params the rule is that JS callers must supply an object with a 'value' property. The caller can set that 'value' property to the 'in' value before the call and extract the 'out' param from the 'value' property of that object after the function returns. I have added a test of this. I'll show parts of it here for illustration. - testxpc.idl gets a new method: void SetReceiverReturnOldReceiver(inout nsIEcho aReceiver); - TestXPC.cpp implements it: /* void SetReceiverReturnOldReceiver (inout nsIEcho aReceiver); */ NS_IMETHODIMP MyEcho::SetReceiverReturnOldReceiver(nsIEcho **aReceiver) { if(!aReceiver) return NS_ERROR_NULL_POINTER; nsIEcho* oldReceiver = mReceiver; mReceiver = *aReceiver; if(mReceiver) NS_ADDREF(mReceiver); /* don't release the reference, that is the caller's problem */ *aReceiver = oldReceiver; return NS_OK; } - testxpc.js uses it and verifies that the results are correct: var e1 = new Object(); var e2 = new Object(); var v1 = new Object(); var v2 = new Object(); var v3 = new Object(); var v4 = new Object(); echo.SetReceiver(null); all_ok = true; v1.value = e1; echo.SetReceiverReturnOldReceiver(v1); all_ok = all_ok && v1.value == null; v2.value = e2; echo.SetReceiverReturnOldReceiver(v2); all_ok = all_ok && v2.value == e1; v3.value = null; echo.SetReceiverReturnOldReceiver(v3); all_ok = all_ok && v3.value == e2; v4.value = e1; echo.SetReceiverReturnOldReceiver(v4); all_ok = all_ok && v4.value == null; print("inout of interface tests - "+(all_ok ? "passed" : "failed"));
The critical point is that when returning a pointer type (char*, PRUnichar*, nsID*) from an xpcom call you need to use the nsIAllocator service to allocate a copy of the thing and transfer ownership of the copy to the caller. This is completely analogous to the AddRef rules when passing nsISupports* as 'out' or 'inout' params.
All callers to a compliant interface should be able to safely assume that you did follow the transfer of ownership rule and thus they accept responsibility to free the copy when they are done with it.
The common mistake is to do something like:
/* XXX incorrect implementation! */ /* string GetStringA (); */ NS_IMETHODIMP xpcstringtest::GetStringA(char **_retval) { *_retval = "result of xpcstringtest::GetStringA"; return NS_OK; }This code is passing a pointer to static memory. However the caller expects an allocated block which it is expected to later free. So when free is called the heap manager complains.
While in this case xpconnect is the module tripping over this bit of non-compliance in the implementation, it could just as well be any other caller.
I just added some test code to xpconnect/idl and xpconnect/tests/components to demostrate and to use for tests.
given:
[scriptable, uuid(d970e910-30d8-11d3-9885-006008962422)] interface nsIXPCTestString : nsISupports { string GetStringA(); void GetStringB(out string s); };we get the header output:
class nsIXPCTestString : public nsISupports { public: NS_DEFINE_STATIC_IID_ACCESSOR(NS_IXPCTESTSTRING_IID) /* string GetStringA (); */ NS_IMETHOD GetStringA(char **_retval) = 0; /* void GetStringB (out string s); */ NS_IMETHOD GetStringB(char **s) = 0; };When your native implemenation returns pointer types it needs to use the shared allocator to make a copy. You can get the nsIAllocator service from the service manager to do this, or use the static nsMemory class. Here is an example from xpconnect/tests/components/xpctests_string.cpp:
/* string GetStringA (); */ NS_IMETHODIMP xpcstringtest::GetStringA(char **_retval) { const char myResult[] = "result of xpcstringtest::GetStringA"; if(!_retval) return NS_ERROR_NULL_POINTER; *_retval = (char*) nsMemory::Clone(myResult, sizeof(char)*(strlen(myResult)+1)); return *_retval ? NS_OK : NS_ERROR_OUT_OF_MEMORY; }Yes, this is a pain. But, it is the way to have consistent rules about thransfer of ownership. It looks overblown in this case where we have a static string that is being returned. But the same interface could be returning a newly allocated string on every call. By having a consistent convention and sticking to it we can avoid errors. It is the sticking to it part that is hard.
The shared allocator can be found by asking the ServiceManager for the service with the progid: "@mozilla.org/xpcom/memory-service;1" and interface: nsIMemory. The simpler method is to use the 'nsMemory' object that exposes all static methods see http://lxr.mozilla.org/mozilla/source/xpcom/base/nsIMemory.idl and http://lxr.mozilla.org/mozilla/source/xpcom/base/nsMemory.h.
For the *very* rare cases where you absolutely need to break the xpcom rules and
not do a transfer ownership, xpidl provides the [shared] keyword. This will let
xpconnect know that you are breaking the rules and not transfering ownership.
But this should never be used unless *every* possible caller also know that this
method breaks the rules. C++ has no standard way to convey this between
components. So if your component were used by some random other component in the
future that other component would have to know be some special means that this
method is *special*. Any 'generic' user of your method would crash just like
xpconnect did here. I strongly discourage the use of 'shared' in any public
interfaces. It is just a way of asking for future crashes.
jband - 7/30/99
If you have a js file called a.js then you can write an html file called a.html that looks like:
<html> <script src="a.js"></script> </html>Start apprunner and load the following url (assuming that the html file is in c:\temp):
file:///c|/temp/a.html
This should load a.js and run it. This works fine if you only want to dump stuff to the console to test your code.
For instance I have a.js with the contents:
try { var clazz = Components.classes["nsEcho"]; var iface = Components.interfaces.nsIEcho; dump("clazz: "+clazz+"\n"); dump("iface: "+iface+"\n"); var supports = clazz.createInstance(); dump("supports: "+supports+"\n"); var echo = supports.QueryInterface(iface); dump("echo: "+echo+"\n"); var result = echo.In2OutOneString("foo"); dump("call returned: "+result+"\n"); } catch(e) { dump("caught exception: "+e+"\n"); }When I load
file:///c|/temp/a.html
it prints to the console:
Going to reload FROM:chrome://navigator/skin/animthrob.gif TO:resource:/chrome/navigator/skin/default/animthrob.gif clazz: nsEcho iface: nsIEcho supports: [xpconnect wrapped nsISupports] echo: [xpconnect wrapped nsIEcho] call returned: foo Document file:///c|/temp/a.html loaded successfully Document: Done (0.271 secs)Note: if there is an error in your *.js file then apprunner will report that the error was in the *.html file that loaded the js file. This is a known bug on vidur's list.
XPConnect now has some support for arrays. We are talking about plain C/C++ arrays mapped to/from JS Arrays. Not nsISupportsArrays and that ilk. Things that can go in the arrays are all the types that we can use as scriptable xpidl params. This exclued 'natives'. Arrays of arrays are not supported. Arrays of interface pointers do work. Arrays are of homogenous type. Links to test code... http://lxr.mozilla.org/seamonkey/search?string=nsIXPCTestArray nsIXPCTestArray - http://lxr.mozilla.org/seamonkey/source/js/src/xpconnect/tests/idl/xpctest.idl#198 - with its eight methods is the one and only one interface I've written so far to test this. IDL syntax is as shown there. In addition to 'size_is' it should also respect 'length_is', but I've not really tested that (if you don't know what 'length_is' means then you probably don't need it). The typelib doc - http://www.mozilla.org/scriptable/typelib_file.html - has some info on this. 'size_is' is required for arrays. 'length_is' is optional. There is a sample C++ implementation of nsIXPCTestArray in http://lxr.mozilla.org/seamonkey/source/js/src/xpconnect/tests/components/xpctest_array.cpp There is a JavaScript test of this in http://lxr.mozilla.org/seamonkey/source/js/src/xpconnect/tests/js/old/xpctest_array.js This test includes a JS implementation of nsIXPCTestArray. So, the example shows and tests both a C++ version and a JS version of the same code. FWIW, this re-excited me about implementing interfaces in JS becasue the JS implementations were much neater and easier - no messy Alloc/Free or AddRef/Release. Arrays are pointer objects and follow all the regular transfer of ownership rules that I harp about. [shared] is not allowed. The params indicated by 'size_is' and 'length_is' *must* be of type PRUint32 (unsigned long). At present the JS side of this mapping is only for actual JS Arrays - no "objects with numbered properties" or mapping of char[] <-> JSString etc.See these old news articles... (Note that some of these articles are members of long threads with additional information)
Here you are calling a wrapped native interface that takes a particular interface as a param.
XPConnect will first check to see if the param was declared as nsIVariant. If so then it will package up whatever you passed into an nsIVariant container and pass that to the callee.
Otherwise, xpconnect will check to see if the param value is null or undefined. If so then it will pass nsnull to the callee.
Else, xpconnect will check to see if the param value is a JSObject (only a JSObject can represent an interface). If it is not a JSObject then xpconnect will throw a NS_ERROR_XPC_BAD_CONVERT_JS exception at the JS code that was trying to make the call. There is a special case for passing the number zero. In order to give a better error message that helps the JS programmer understand that 0 != null in JS we throw a NS_ERROR_XPC_BAD_CONVERT_JS_ZERO_ISNOT_NULL exception.
At this point xpconnect has detected that the passed param is in fact a JSObject. XPConnect then checks to see if the JSObject is in fact a wrapper for a native objects (usually a C++ implementation that was previously handed into JS code, but it could be actually implemented in Python or Java or whatever). If the JSObject does represent a wrapped native object then xpconnect unwraps the object and QIs it to the specified interface iid. If that succeeds then xpconnect passes the correct underlying interface pointer to the callee. Otherwise (in the case where the object *is* a wrapped native, but can't be QI'd to the specified interface) xpconnect throws a NS_ERROR_XPC_BAD_CONVERT_JS exception at the JS caller.
Otherwise, xpconnect does a hashtable lookup to discover if this JSObject has been previously wrapped to represent any xpcom interfaces. If so then xpconnect used the existing wrapper. Else, xpconnect makes a new wrapper. In either case the rest of the logic is pretty much the same.
The important thing to understand is that xpconnect is trying to make things easy for the JS programmer. It tries to require as little explicit declaration and implementation as possible. XPConnect accumulates 'assertions' about a given object and will happily represent the object as matching those assertions. For instance, if you pass a JSObject as an nsIFoo and later pass the same JSObject as an nsIBar, then xpconnect will have built a wrapper that a native caller can QI back and forth between nsIFoo and nsIBar. It will appear to have all the required methods etc.
The JSObject is not required to have a QueryInterface method for this to work. The JSObject *can* have a QueryInterface method. If so then xpconnect will call it in order to answer questions about support for interfaces that have not already been discovered on the object. The QI method is just another way for making 'assertions' about the interfaces that the JSObject supports.
The actual JSObject need not actually *have* all the methods and properties when the wrapper is built. It need not have any methods or properties at all for a wrapper to get built. The only checking that is done in this regard is when a specific method on the interface is actually *called*. At that point xpconnect will attempt to lookup the method on the object with the appropriate name. If the method is found then the call is made. Otherwise xpconnect will return a NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED error code to the caller.
This scheme allows for the dynamism of Javascript. It lets JS interface
implementors implement only the parts of the interface they think they need to
implement.
jband - 4/3/02