Frequently Asked Questions about XPConnect and XPIDL

Table of Contents

1) My component isn't scriptable from js! What's going on?
Add the [scriptable] attribute.
mccabe - 7/17/99
2) I need to define a lot of constants in my interface. Why can't I use enum instead?
See discussion in bug 8781 and .xpcom.

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

jband - 7/17/99
3) How do I use interface methods with 'out' parameters from JavaScript?
JavaScript does not support reference or 'out' parameters in general, although objects can be thought of as references. To get the value of an out parameter of an interface method exposed to JavaScript, pass any JavaScript object. XPConnect will write the updated out parameter value to a property called 'value' on that object. Given an XPConnected object with this interface:
[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.

mccabe - 9/19/00
4) How do I use in/out parameters in JavaScript?
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"));
5) My component builds but the interface doesn't show up through "Components.interfaces.Foo"
Make sure that you have the magic invocations in your makefile. See the makefiles in mozilla/xpcom/sample/ for examples.
mang - 8/03/99
6) Where can I find some documentation on IDL?
XPIDL docs are at http://www.mozilla.org/scriptable/xpidl The xpidl syntax stuff here is out of date. Use this tracking bug to add comments or questions. There is simple types and keywords list that jband wrote as a basis for documentation. Hopefully this will be superceeded with better docs soon.
mang - 8/03/99
7) What's different between XPIDL and other IDLs?
XPIDL is based on Corba IDL. It is a simple subset of Corba IDL with some xpcom extensions added. Use this tracking bug to enter your flames.
mang - 8/03/99
8) Does xpidl support string constants?
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.
jband - 7/28/99
9) How do I make 'out string' work right? Why does my implementation of this crash when called from JavaScript?
XPConnect requires that objects follow the xpcom rules of transfer of ownership of pointer type 'out' params. I don't know that we have any definitive docs on this, but this has been hashed out in some detail on the mozilla xpcom newsgroup more than once. I'll try to explain...

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

10) I made my component scriptable and wrote some JavaScript code to test it. So, how do I run my tests?
Start small and work up.

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.
jband - 7/31/99
11) How do I pass array parameters?
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)
news://news.mozilla.org/37E9D66E.564E37E8@netscape.com
news://news.mozilla.org/38027C5D.793151CD@netscape.com
news://news.mozilla.org/387B8F3F.32095ECF@netscape.com
news://news.mozilla.org/3958DCDC.A529D2C8@netscape.com

jband - 9/19/01
12) What happens when a native method called from Javascript expects nsIFoo and gets passed something else? Does the native method receive NULL or is an exception thrown or what?

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


Last Modified: Wed Apr 03 2002 13:48:08 GMT-0800 (Pacific Standard Time)