![]() |
![]() |
![]() |
![]() |
Home | Documentation ... Contact |
![]() |
Table of Contents
This guide steps through the process of writing a new SashXB extension from scratch.
We'll construct the somewhat silly Echo
extension piece by piece. When we finish, it will have some properties, methods, and objects.
At the risk of sounding pedantic, we urge you to be extremely careful in following the instructions. It can be quite difficult to detect errors, especially with macros and IDs. Because of this, we recommend setting up the framework and verifying that you can access simple properties and methods before moving on to more interesting functionality.
New extensions open up more possibilities to weblication writers. The whole point of an extension is to encapsulate a useful tool or technology in an easy-to-use form. An extension hides the low-level implementation details, granting novice programmers the capability to use otherwise inaccessible tools and helping experienced developers write sophisticated applications quickly.
New extensions can be based on any sort of technology that you find interesting. They may also be based on Windows extensions. Many current SashXB extensions (such as Core, Filesystem, Registry, etc.) mimic Windows APIs since it'd be cool if, for a limited subset of functionality, the same weblication can run on both Windows and Linux. For this reason it is important to follow the Windows APIs whenever possible if you are developing an extension that is already present in the Windows version. If you find that the existing Windows APIs are not satisfactory, you may still want to support the same properties and functions.
While SashXB's LGPL license allows you to write closed-source code that links with it, you may nevertheless choose to open source your extension's code. If you do (which is, of course, encouraged), take care to follow open source conventions, like these listed on GNOME.
SashXB weblications are written in JavaScript and HTML. The purpose of Sash (both the Windows and Linux versions) is to allow Javascript and HTML programmers to create more powerful, interesting, and useful web applications. Sash extensions hide low-level implementation details while still providing enough flexibility so that they can be used in any appropriate situation.
Some weblication writers do not need to have a great deal of programming experience or knowledge of more advanced languages, such as C/C++, Perl, and Java. Ideally a weblication writer can read over the comments in your IDL file to see how your extensions and objects should be used and start writing something right away. Although the typical Linux user may be interested in seeing your source code, he or she should never be required to look it at to see how certain calls should be made.
When you write a SashXB extension, you are creating a tool that any Sash weblication writer can use. Because of this, it is particularly important to ensure that your interfaces are easy to use and easy to understand, and that your extension behaves as correctly as possible.
A good amount of time should be spent on designing the extension. Some extensions may involve wrappering existing code while others will be written from scratch. Here are some of the things that extension writers should think about (along with some comments in reference to the FTP extension):
There are many things to be careful of when designing a new extension. First of all, take care to decide exactly what functionality you want to have in your extension. Exposing too much can make your extension confusing to use, while exposing too little may make your extension difficult to use except in very specific situations. In addition, make sure that you test your extension thoroughly. Because it is used as a tool for writing weblications, developers who use your extension may become frustrated if they can't tell whether bugs are coming from their JavaScript code or your extension code.
Of course, you're free to code in whatever style you choose. Here are some techniques that you might choose to employ to make coding and debugging easier.
debugmsg.h
, found in SashXB/tools, and use the DEBUGMSG()
macro often in each of your extension's functions. It'll help you pinpoint crashes quickly without having to start up gdb, and it'll allow other developers to debug SashXB components that use your code without having to wade through it.We include the sash-gdb script with the runtime to facilitate debugging. It runs gdb with the proper environment variables set. Invoke it as
> sash-gdb sash-runtime-bin ... gdb> run wdf_of_weblicaton.wdf
The runtime uses DEBUGMSG in nearly all of its modules. You can use this output to determine if a bug is occuring in the runtime. Debug flags you can turn on include D_runtime, D_runtimetools, D_wdf, D_installer, D_datamanager, D_instmanager, D_downloader, D_filesystem, and so on. Often extensions employ their own debug flags as well.
Develop a simple weblication to test your extension as you add to it. Getting another Sash developer to use your extension in a weblication is one of the best ways to find bugs in your extension. It will also give you a lot of insight on how easy it is to use your extension, and whether improvements need to be made. Remember to provide good documentation so that weblication writers know how to use your interface.
If you'd like to check your progress, download a tarball of the completed sample extension, Sash.Echo.
An extension is at its core an XPCOM object that implements the sashIExtension interface. If you are not familiar with XPCOM, you can find detailed documentation at Mozilla's XPCOM site. However, this guide should provide you with enough knowledge of XPCOM to write a wide variety of extensions.
You define the methods and properties to be exposed by your extension in an XPCOM IDL file. On installation, an extension must have at least the following:Since you're probably going to be looking at source from other extensions, we suggest you pull down the SashComponents module from the GNOME CVS server:
> export CVSROOT=:pserver:anonymous@anoncvs.gnome.org:/cvs/gnome # for Bash or > setenv CVSROOT :pserver:anonymous@anoncvs.gnome.org:/cvs/gnome # for cshell > cvs login > cvs -z3 checkout -P SashComponents
The -P is very important, as we want to make sure we don't get any empty directories or the build will break. In SashComponents/extensions
, recursively copy the template/
directory to a new directory called echo/
. Familiarize yourself with the directory structure; you'll have to change lines in autogen.sh
and configure.in
for your new extension, and also src/Makefile.am
as you add source files to the build (all in the src/
directory, of course). grep
for 'TODO' in those three files to see where you have to make changes, and make sure you do them before you try to make your extension the first time. By this time you should also have the SashXB runtime installed on your system.
Finally, you need to grab and install the SashXB devel package. Now you're ready to go!
An extension's interface is what actually get exposed to JavaScript; it defines the methods and properties that weblication writers can access.
The sashIEcho.idl
file (locating, like all the other source files, in the src/
directory) will describe the interface for our extension. To start, let's give ours just one function, Print
, and one attribute, Prefix
, so our interface file will look like this:
#include "nsISupports.idl" #include "nsIVariant.idl" // we'll need this later [scriptable, uuid(E84BB338-5021-43BC-935D-94FC9307C43A)] interface sashIEcho : nsISupports { attribute string Prefix; void Print(in string toPrint); };
All XPCOM components inherit from nsISupports, so verify that yours does too, as above. Make sure that you generate a new uuid (using the program uuidgen
, for instance) for each extension. Now convert this IDL file into header and xpt files by running the xpidl program that comes with Mozilla:
> xpidl -m header -I [MOZILLA_IDL_DIR] sashIEcho.idl # generate sashIEcho.h
> xpidl -m typelib -I [MOZILLA_IDL_DIR] sashIEcho.idl # generate sashIEcho.xpt
where [MOZILLA_IDL_DIR]
is the location of the Mozilla IDL files, such as nsISupports.idl
. You'll use the header file in creating your implementation of SashEcho, and Mozilla will use the .xpt file to expose your interface to JS. The header file also contains a template for a concrete class declaration and definition -- take a look.
Note that the Print
function takes in a string and returns nothing; as the main function of Sash.Echo
its only job is to echo its input, preprended with an optional Prefix
. A return value could be specified in place of the void
, or as a second parameter (for instance, out boolean my_return_val
). If we wanted to prevent the user from modifying the value of Prefix
we could have prefixed its declaration with readonly
. The IDL semantics and types are similar to those of C. For more information, check out this comprehensive overview of IDL.
Once we're done, our Print
function can be accessed in JavaScript as Sash.Echo.Print()
. Of course, Echo
may have private functions and data, but we don't want to expose any of that to JavaScript, so we don't include it in the IDL file.
So, now we've created an interface, we need to construct a C++ class that implements that interface. Below is the listing for the header file of the implementing class, SashEcho.h
.
#ifndef SASHECHO_H #define SASHECHO_H #include "nsID.h" #include "sashIEcho.h" #include "sashIExtension.h" #include "secman.h" #include <string> //{2ab05691-ba8c-4766-9931-bac841b0d39c} #define SASHECHO_CID \ {0x2ab05691, 0xba8c, 0x4766, \ {0x99, 0x31, 0xba, 0xc8, 0x41, 0xb0, 0xd3, 0x9c}} NS_DEFINE_CID(kSashEchoCID, SASHECHO_CID); #define SASHECHO_CONTRACT_ID "@gnome.org/SashXB/echo;1" class SashEcho : public sashIEcho, public sashIExtension { public: NS_DECL_ISUPPORTS NS_DECL_SASHIEXTENSION NS_DECL_SASHIECHO SashEcho(); virtual ~SashEcho(); private: // store the Prefix here std::string m_Prefix; // the runtime security manager -- we'll need this later sashISecurityManager* m_pSecurityManager; // the JSContext, needed for callbacks JSContext* m_pContext; // also needed for callbacks sashIActionRuntime* m_pActionRuntime; }; #endif // SASHECHO_H
Our implementing class inherits from both sashIEcho
and sashIExtension
. sashIExtension
provides all the methods that needs to register and initialize the extension (see SashXB/includes/sashIExtension.idl
). The methods we defined to be exposed are provided in sashIEcho
. We do not need to define the Print
function and Prefix
attribute separately, as they are included in the NS_DECL_SASHIECHO
declaration. The class ID (CID) should be generated and different from the interface ID (IID) defined in the IDL file, since theoretically multiple classes can implement one interface. You can convert a generated UUID to the weird #define'd structure by hand or with SashXB/tools/uuid.py
-- but keep around the original UUID since you'll need it for the WDF file later on.
Now we must define the implementation of our class, in SashEcho.cpp
. There are several boilerplate functions necessary for writing an extension, but we provide some macros to ease the pain. The opening of our implementation file will look like this:
#include <string> #include <vector> #include <iostream> #include "xpcomtools.h" #include "extensiontools.h" #include "sashVariantUtils.h" #include "security.h" #include "sash_constants.h" #include "sashIActionRuntime.h" #include "SashEcho.h" SASH_EXTENSION_IMPL(SashEcho, sashIEcho, SASHECHO, "Echo", "A simple example extension");
The macro SASH_EXTENSION_IMPL
expands to several methods required by sashIExtension. It also expands to the module definition that Mozilla needs to register the extension. This macro will only work for extensions with a single object. There are other macros in extensiontools.h
to help implement extensions that expose multiple objects (and we'll get to those later, don't you worry). The arguments to SASH_EXTENSION_IMPL
are the concrete class you're implementing, the abstract base interface it instantiates, the capitalized name of the class, the name to expose to JavaScript (in this case, we're calling it "Echo" as in "Sash.Echo", although you could call it anything else), and a short description of the extension. Use of this macro requires that all of the defines in the header file have been with the same naming convention used above.
Next we have to write the constructor and destructor for our object, which will be just like any other XPCOM object:
SashEcho::SashEcho() { NS_INIT_ISUPPORTS(); } SashEcho::~SashEcho() { }
Some functions required by sashIExtension are not implemented by the macro because they require extension-specific behavior. Here we provide simple implementations of these functions:
NS_IMETHODIMP SashEcho::Initialize(sashIActionRuntime *act, const char *xml, const char *instanceGuid, JSContext *context, JSObject *object) { /* This function is called by the runtime once the extension is created. The first argument is the global runtime. The extension might want to call methods on this or store a pointer to it for later. The next argument is the XML string with which the extension is being registered. The structure of this XML is up to the extension-writer (you), and the values will be filled in by the weblication author. The easiest way to parse this is to depend on the XML extension and use that. The third argument is the guid of the currently running weblication. The JSContext is necessary to provide callbacks to the weblication. The JSObject is in fact the JavaScript representation of your extension; you might want this as well when invoking callbacks. We'll simply extract the security manager, and save the JSContext. */ act->GetSecurityManager(&m_pSecurityManager); m_pActionRuntime = act; m_pContext = context; return NS_OK; } NS_IMETHODIMP SashEcho::ProcessEvent() { /* This function will be called on request by the runtime, so we can perform any idle loop actions required by the extension. Specifically, if your extension has multiple threads, the JavaScript engine requires that you invoke callbacks on it from its own thread. So to call an event from another thread, save the callback info in an accessable structure, call the runtime's ProcessEvent method (passing it yourself), wait for the runtime to call ProcessEvent from the main thread, and invoke the callback from here. (sashIActionRuntime::ProcessEvent should probably have been called SignalEvent). Since Echo does none of this, we don't need to do anything here. */ return NS_OK; } NS_IMETHODIMP SashEcho::Cleanup() { /* The runtime calls Cleanup when the weblication is about to exit. Do any shutdown procedures here. One example is using Cleanup to close any open file and socket descriptors. */ return NS_OK; }
All XPCOM functions should return a proper nsresult
-- NS_OK if all things go well, or a variety of error results: NS_ERROR_FAILURE, NS_ERROR_NULL_POINTER, etc. Remember, these are XPCOM error codes, so you only want to return them if there is an XPCOM error, and not if there's just a JavaScript problem.
Now we can finally implement the functions that we want to expose to JavaScript. For the Echo extension, we will implement the method Print
and the attribute Prefix
. Read/write attributes such as this one require two functions, one each with the name of the attribute prefixed with Get and Set.
NS_IMETHODIMP SashEcho::GetPrefix(char **aPrefix) { // we use a macro defined in SashXB/extensiontools/xpcomtools.h that // allocates memory for the string and copies it XP_COPY_STRING(m_Prefix.c_str(), aPrefix); return NS_OK; } NS_IMETHODIMP SashEcho::SetPrefix(const char *aPrefix) { // just store the prefix for future reference // we don't own aPrefix, so we don't free it m_Prefix = aPrefix; return NS_OK; } NS_IMETHODIMP SashEcho::Print(const char *toPrint) { cout << m_Prefix << toPrint << endl; return NS_OK; }
In general, parameters passed to your functions are not owned by you, so you shouldn't deallocate them or assume they'll be available past the life of your function call -- make sure you duplicate any data you'd like to keep around. Similarly, you are responsible for allocating memory for any values you return, if necessary.
The last step before testing out your extension is to create a WDF file. In the src/ directory, run
> sash-wdf-editor echo.wdf
Make sure you set the type of component to "extension". You must use your CID (in its original, non-struct form -- but with the curly braces!) as the extension's GUID. Extensions have no actions, and Sash.Echo has no dependecies. If you've set up your Makefile properly (making sure to set the WDF_FILE
variable to "echo.wdf"), it will automatically tar up your shared object file and xpt file into echo.tgz
. Add echo.tgz
in the Files pane. While you're at it, set the platform to Linux.
You're just about ready to test out the Sash.Echo extension. Make sure you've added all the appropriate files to your makefile, in the toplevel echo/
dir, run ./autogen.sh
and then make install
. That should build the source and run sash-install on the wdf file. It's a good idea to verify that you can use your extension in a test weblication (built on the Console location, for instance) before continuing.
// sample js code that uses echo // prints out "Hello!" Sash.Echo.Print("Hello!"); Sash.Echo.Prefix = "TestEcho: "; // prints out "TestEcho: Goodbye!" Sash.Echo.Print("Goodbye!"); // prints out "TestEcho: TestEcho: " Sash.Echo.Print(Sash.Echo.Prefix); Sash.Console.Quit();
Variants provide an easy way to transfer arbitrary arguments between your C++ extension and a JavaScript weblication. Here are some potential uses. Make sure to include nsIVariant.idl
in your IDL file, and sashVariantUtils.h
in your cpp file. This latter file contains many useful functions that deal with variants, so it's best to familiarize yourself with them.
With variants, your one C++ function can take an argument of any type -- string, int, interface, or whatever. For instance, add this method to your sashIEcho interface (in its IDL file):
string WhatType(in nsIVariant anything);
This function will take in any type of object and return its type as a string description (somewhat like the typeof
operator). Here's the code we add to SashEcho.cpp
that does this (using several of the VariantUtils macros).
NS_IMETHODIMP SashEcho::WhatType(nsIVariant* my_variant, char** _retval) { if (VariantIsBoolean(my_variant)) XP_COPY_STRING("boolean", _retval); else if (VariantIsString(my_variant)) XP_COPY_STRING("string", _retval); else if (VariantIsNumber(my_variant)) XP_COPY_STRING("number", _retval); else XP_COPY_STRING("one of many other types we could check", _retval); return NS_OK; }
To access the actual value of the variant (as opposed to just checking its type), we would use the VariantGetBoolean
, VariantGetString
, etc. functions. Anyway, we can now test WhatType
in our weblication:
printsys("Should be string: " + Sash.Echo.WhatType("oogabooga")); printsys("Should be boolean: " + Sash.Echo.WhatType(true)); printsys("Should be number: " + Sash.Echo.WhatType(42)); printsys("Should be something else: " + Sash.Echo.WhatType(Sash.Echo));
You can pass in and return JavaScript arrays with variants, as well. Here's a function that returns the sum of its numeric arguments. The IDL declaration:
double Sum(in nsIVariant array_of_nums);
and the corresponding C++ code:
#include <numeric> NS_IMETHODIMP SashEcho::Sum(nsIVariant* array_of_nums, double* _retval) { *_retval = 0.; if (VariantIsArray(array_of_nums)) { vector<double> nums; VariantGetArray(array_of_nums, nums); // use an STL algorithm to do the work for us *_retval = accumulate(nums.begin(), nums.end(), 0.); } return NS_OK; }
Now we can call this function from JavaScript.
printsys("total is " + Sash.Echo.Sum([1, 5.3, 23])); // should be 29.3
IMPORTANT NOTE: Due to changes in Mozilla code, we have currently removed optional argument support. It may return in future versions of SashXB. This subsection is therefore invalid for the time being. You can still specify "optional" arguments with nsIVariants, but a weblication writer will have to explicity pass null
to use the default value.
Variants can be used to receive optional arguments. If the weblication writer does not actually pass a value for the argument in invoking your function, you will receive an empty variant. Let's modify the Print
function to take a second parameter that optionally suppresses output of the Prefix
.
void Print(in string toPrint, nsIVariant suppress_prefix);
Here's how we implement the C++ function.
NS_IMETHODIMP SashEcho::Print(const char *toPrint, nsIVariant* suppress_prefix) { // VGB will return false if suppress_prefix is set to false, or if it's not set at all if (! VariantGetBoolean(suppress_prefix)) cout << m_Prefix; cout << toPrint << endl; return NS_OK; }
Now we can suppress the prefix whenever we want:
Sash.Echo.Prefix = "---- "; // should print "---- testing" Sash.Echo.Print("testing"); // should print "---- testing" Sash.Echo.Print("testing", false); // should print "testing" Sash.Echo.Print("testing", true);
Note: We are attempting to convince the Mozilla maintainers to support optional arguments in the IDL, much as COM does, so that you could write void DoSomething([defaultvalue(false)] in boolean optional_boolean_arg)
. Hopefully this functionality will be included in a future release of Mozilla.
Returning variants is nearly as easy. Here's a pretty dumb function that returns either a random integer or a random string of five capital letters, depending on the input.
// pass 0 for int, 1 for string nsIVariant Random(in short int_or_string);
#include <stdlib.h> #include <time.h> // PRInt16 is the native type that corresponds to IDL's "short" NS_IMETHODIMP SashEcho::Random(PRInt16 int_or_string, nsIVariant** _retval) { srand(time(NULL)); // make sure to create a new variant first NewVariant(_retval); if (int_or_string == 0) { VariantSetInteger(*_retval, rand()); } else { string t; int i = 0; do { t += (char) (rand() % ('Z' - 'A') + 'A'); } while (++i < 6); VariantSetString(*_retval, t); } return NS_OK; }
Invoke it like everything else:
var a_num = Sash.Echo.Random(0); var a_string = Sash.Echo.Random(1); printsys(typeof(a_num)+" " + a_num + " | " + typeof(a_string) + " " + a_string);
If you'd like most extensive examples, Sash.Registry
uses variants effectively to return registry values. Many other extensions employ variants in one way or another.
Security is an integral aspect of of any extension. It is essential that you incorporate appropriate security checks whenever your do anything potentially harmful (that is, nearly anything) to the user's computer or to the network that is outside the bounds of JavaScript.
For compatibility reasons, we have reproduced the security architecture found in Sash for Windows. The security framework is composed of about a dozen general security settings and any number of extension-specific custom ones. Each security setting has one of four types: boolean, number, string enumeration (list of strictly ascending, security-wise, string values), or string. When a user runs a weblication, he can set the security permissions it has; your job as an extension writer is to enforce these permissions by verifying that the weblication has security access to whatever you're trying to do. The general settings can be found in SashXB/security/security.h
; if your extension employs additional functionality not covered by these setting, you can make your own custom settings that the user can set for any weblication that uses your extension.
Be sure to include SashXB/include/secman.h
; in it you'll find many useful security functions. To find out what a security setting is currently set to, use the series of QuerySecurity
function. Most of the time, though, you'll want to just assert that a security setting is within the appropriate limits. AssertSecurity
checks to see if the setting is within the limit you give it. If so, it returns, and if not, it gives the user a chance to reset the permissions or exit. So you need only call AssertSecurity
, and if it returns, you're free to go ahead and do whatever action you were checking about.
To illustrate this, let's add a fairly ridiculous function to Echo that, instead of echoing the input itself, runs /bin/echo
instead. Clearly this is a dangerous action that should be checked. Here's the IDL definition:
void SystemEcho(in string toPrint);
We notice that there is a "spawn process" general security setting, so we'll use that one. The code for this function, then, is quite straightforward.
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> NS_IMETHODIMP SashEcho::SystemEcho(const char* toPrint) { // we want to ensure that the user is letting the currently running // weblication spawn other processes AssertSecurity(m_pSecurityManager, GSS_PROCESS_SPAWNING, true); // If we've made it here then we must have received security clearance. // it's probably in /bin/echo, but we're not sure, so we'll use the even // more dangerous execlp int pid = fork(); if (pid == 0) execlp("echo", "echo", toPrint, NULL); else waitpid(pid, NULL, 0); return NS_OK; }
Now, if a weblication tries to invoke this function and it doesn't have permission to spawn processes, a dialog will pop up asking the user what to do: quit, allow it this once, or permanently change the setting to allow it hereafter. Try it. That's all there is to basic security.
One common check that you should be very careful about is filesystem access. If your extension ever opens any file, it needs to ensure that the weblication it's running under has access to that file. There are three settings for filesystem access: none
- no access at all; local
- only allowed to open files in its own data/
directory; all
- can open any file. You may find it difficult to determine whether an arbitrary path (with ..
's et al.) actually resolves to a weblication's data directory, so we provide the AssertFSAccess
function in the ActionRuntime. You simply pass it the path to check and it will verify with the Security Manager that the running weblication is allowed access to it.
You may find that your extension operates in what you consider a dangerous manner that is not covered by one of the general security settings. In this case, you can create a custom setting for your extension. Let's create a function in Sash.Echo
, called EchoNTimes
,
void EchoNTimes(in string toPrint, in unsigned long n);
that simply echoes the string n times. But we're afraid that a weblication may echo something too many times in a row, and we want the user to have control over exactly what "too many" means. So we make a custom security setting specific to Sash.Echo
. First, we open up the Echo wdf file, Edit Security, and Add Security Setting. Give it a suitable description, like "Max echoes per call", specify its type as a number, and set its unique ID to 1. Now we're ready to code the C++ function that implements EchoNTimes
. The AssertSecurity
call is slightly different this time, as we have to tell the runtime Sash.Echo
's GUID so it knows which custom settings to check. Luckily, we can use the fact that Echo's CID is the same as its GUID, as described above.
// must be the same as the ID we set in the wdf const SecuritySetting NUM_TIMES_SETTING_ID = 1; NS_IMETHODIMP SashEcho::EchoNTimes(const char* toPrint, PRUint32 n) { // this time, we pass along our own GUID, the ID of the custom setting we // want to check, and the number we're checking AssertSecurity(m_pSecurityManager, ToUpper(kSashEchoCID.ToString()), NUM_TIMES_SETTING_ID, (float) n); for (unsigned int i = 0 ; i < n ; i++) cout << m_Prefix << toPrint << endl; return NS_OK; }
After you reinstall Echo, when you reinstall your test weblication, note that the security pane now lists a separate entry for Echo security settings. Verify that a security alert occurs when your weblication tries to EchoNTimes
with a value greater than the num times setting permits.
You may want to allow a weblication to be notified when certain events occur, by providing it with a method for registering callback functions with your extension that you can invoke when those events happen.
If you're writing a single-threaded extension, dealing with callbacks is quite easy. Let's abuse Sash.Echo
some more by adding a callback. A callback is simply a string attribute that a weblication writer can set to the name of the function he wants invoked when the given event occurs. Of course, since there is no more rigorous definition, it is very important to document exactly what parameters get passed to the callback function in your IDL, as there is no other way for the weblication writer to know!
// Callback invoked when echo string is over 32 chars long. // The function is passed the echo string and must return true // if it wants echo to go ahead and echo the string. (see below) attribute string OnReallyLongEcho;
Undoubtedly your callbacks will be for more important occurrences. Anyway, to support this callback in our SashEcho class, first we add a private string in the class definition in SashEcho.h
: std::string m_OnReallyLongEcho
. Then we have to add the getter and setter for the IDL attribute we exposed:
NS_IMETHODIMP SashEcho::GetOnReallyLongEcho(char **aString) { XP_COPY_STRING(m_OnReallyLongEcho.c_str(), aString); return NS_OK; } NS_IMETHODIMP SashEcho::SetOnReallyLongEcho(const char *aString) { m_OnReallyLongEcho = aString; return NS_OK; }
Events are invoked using the CallEvent
method, found in SashXB/extensiontools/extensiontools.h
. This function takes the runtime, the JSContext, the name of the callback, and a vector of nsIVariant arguments to pass to the callback. Sometimes a callback function may return a value that we're interested in -- such as in WindowApp, where a "close window" directive may be aborted if the JavaScript callback returns the appropriate value. In these cases, use CallEventWithResult
, which returns the return value of the callback in an nsIVariant (which you must handle properly to avoid memory leaks). In fact, let's do that here, and only print the string out if the callback returns true (that's what the comment in the IDL file was for).
We modify the Print
function to handle this special case.
NS_IMETHODIMP SashEcho::Print(const char *toPrint) { if (strlen(toPrint) > 32) { // if the user has defined a callback... if (! m_OnReallyLongEcho.empty()) { // time to invoke our event callback // first, we construct its arguments, in this case the echo string vector<nsIVariant*> args; nsCOMPtr<nsIVariant> echo_string; NewVariant(getter_AddRefs(echo_string)); VariantSetString(echo_string, toPrint); args.push_back(echo_string); // we're careful about properly reference-counting the result // (normally, with just CallEvent, we wouldn't have to worry) nsCOMPtr<nsIVariant> res = getter_AddRefs( CallEventWithResult(m_pActionRuntime, m_pContext, m_OnReallyLongEcho, args)); // if the function doesn't return true, we're outta here if (VariantGetBoolean(res) == false) { return NS_OK; } } } cout << m_Prefix << toPrint << endl; return NS_OK; }
Now let's put this functionality to good use (to some use, at least) in our weblication with a little routine that takes input from the user and echoes it.
// we can actually do this: // Sash.Echo.OnReallyLongEcho = long_echo; // but that passes the entire function body through, which is less efficient // so we'll stick with quotes: Sash.Echo.OnReallyLongEcho = "long_echo"; // we know that this function receives the string as an argument only because // it is documented in the IDL function long_echo(str) { var t = Sash.Core.UI; return (t.MessageBox("Are you sure you want to echo the following " + str.length + "-character string?\n" + str, "Are you sure?", t.MB_YESNO | t.MB_ICONINFORMATION) == t.IDYES); } var a; while((a = Sash.Core.UI.PromptForValue("Echo String", "Please enter a string to echo:", "")) != "") { Sash.Echo.Print(a); }
Try this with a short string and a really long string. It's kind of dumb, but it works.
If you've written a multithreaded extension, you have to be very careful about calling events. All calls to Mozilla's JS engine need to be from the main thread. Therefore, you can't use CallEvent
from any of your other threads or stuff will crash. The way to get around this is to use the ProcessEvent
function that every extension has. It's generally a good idea to use an event queue of some sort (like FTP and Sockets do), have your other threads store pending event data in it, and actually invoke the events (with CallEvent
) in ProcessEvent
, which gets called by the runtime in the main thread.
When designing your extension, you may find that forcing the user to interact with it only through the Sash.*
interface is awkward or limiting. If this is the case, you may consider creating and exposing additional JavaScript objects. For instance, it would be very cumbersome to manage multiple FTP connections with calls to only Sash.FTP
, so the FTP extension exposes a Connection
, which the weblication writer can use as a normal object:
var con = new Sash.FTP.Connection(); con.OnLogin = on_login; con.Connect("ftp://ftp.netscape.com", 21); con.Login("anonymous", "anon@anon.net"); function on_login(c, success) { if (success) { printsys("currdir: " + con.CurrentDirectory); printsys(con.DetailedList()); } else { printsys("connection failed!"); } }
In this way the user can manage multiple connections easily. Similarly, Sash.FileSystem
returns File
objects, each with its own methods and properties pertaining to the actual file it is wrapping. If you think your extension would benefit from similar abstraction, it's time to introduce it to the world of objects. The only problem is that doing this properly in the world of Mozilla is a bit tricky: objects are by far the most difficult topic covered in this guide. So we'll take the usual approach of tacking something on to the lumbering giant that is Sash.Echo
, and hopefully we'll cover all the bases along the way.
So let's say we've decided that weblication writers may want to echo stuff in a bunch of different ways -- a certain number of times, with or without a prefix, with reversed output, whatever -- and it would be too painful to have to set all those preferences in a different configuration each time they want to echo something in one of those ways. So we expose the Echoer
class. Each Echoer
keeps track of its own state, so a developer can create as many as he needs, customize each in the way he wants, and then use the appropriate Echoer
for each occasion. Again, this is a simplified example, not very useful in real life, but it should serve to illustrate the steps to making objects with SashXB.
The first thing we need to do is expose an interface for Echoer
. We can do so in sashIEcho.idl
, although of course we could give it its own IDL file instead. We also need to specify that Echoer
can be constructed from the sashIEcho
interface. To this end, we use the sashIGenericConstructor
, which lets us create arbitrary objects with any number of constructor arguments. Here's the new sashIEcho.idl
:
#include "nsISupports.idl" #include "nsIVariant.idl" // let XPCOM know this interface exists interface sashIGenericConstructor; // for the getter, below interface sashISecurityManager; // don't forget to generate a new uuid for Echoer... [scriptable, uuid(9b244e27-709a-4027-9258-0e2533a537cf)] interface sashIEchoer : nsISupports { attribute string Prefix; attribute unsigned long NumTimes; attribute boolean Reverse; void Print(in string toPrint); }; [scriptable, uuid(E84BB338-5021-43BC-935D-94FC9307C43A)] interface sashIEcho : nsISupports { // Echoer can be constructed from here // takes either 0 or 3 arguments: // var e = new Sash.Echo.Echoer(prefix_string, num_times, reverse); readonly attribute sashIGenericConstructor Echoer; // the string to prepend to echo outputs attribute string Prefix; // Callback invoked when echo string is over 32 chars long. // The function is passed the echo string and must return true // if it wants echo to go ahead and echo the string. (see below) attribute string OnReallyLongEcho; void Print(in string toPrint); string WhatType(in nsIVariant anything); double Sum(in nsIVariant array_of_nums); nsIVariant Random(in short int_or_string); // pass 0 for int, 1 for string void SystemEcho(in string toPrint); void EchoNTimes(in string toPrint, in unsigned long n); };
For now, let's not worry about integrating Echoer
into the rest of the extension; instead we'll first define all of its properties and methods so it's ready to go. First, we'll create its header file, SashEchoer.h
, which is remarkably similar in structure to SashEcho.h
. The main differences are that it needs its own CID and that it inherits from sashIConstructor
instead of sashIExtension
.
#ifndef SASHECHOER_H #define SASHECHOER_H #include "nsID.h" #include "sashIConstructor.h" #include "sashIEcho.h" #include <string> #define SASHECHOER_CID {0x51696a30, 0x3e59, 0x46ca, \ {0xad, 0x7d, 0xcc, 0x20, 0x89, 0x7b, 0xe6, 0xf3}} NS_DEFINE_CID(ksashEchoerCID, SASHECHOER_CID); #define SASHECHOER_CONTRACT_ID "@gnome.org/SashXB/echo/echoer;1" class SashEchoer : public sashIEchoer, public sashIConstructor { NS_DECL_ISUPPORTS NS_DECL_SASHICONSTRUCTOR NS_DECL_SASHIECHOER SashEchoer(); virtual ~SashEchoer(); protected: std::string m_Prefix; PRBool m_Reverse; PRUint32 m_NumTimes; JSContext* m_pContext; sashISecurityManager* m_pSecurityManager; }; #endif //SASHECHOER_H
Again, we don't have to define its interface functions, as they'll be expanded from the NS_DECL_SASHIECHOER
function. Also, since SashEchoer
doesn't inherit from sashIExtension
, we don't need to implement ProcessEvent
and all the rest. We do, however, need to implement the sashIConstructor
interface, which consists of just one function, InitializeNewObject
, that takes the runtime, the parent extension, the JSContext, an argc/argv pairing of arguments, and a return value parameter. This function is invoked when the object is constructed; it's here we can set up the object, optionally taking parameters from JavaScript, which are passed in as nsIVariants with argc/argv. This allows the weblication writer to do such things as
var my_obj = new Sash.FileSystem.Folder(path_to_folder);
Any number of arguments can be passed in, so if your constructor takes a variable number of arguments (such as FileSystem.Folder
, which takes one or two), make sure you check the number of arguments you receive. In fact, Echoer
takes either zero or three arguments, so we see how it's done with SashEchoer.cpp
.
#include "SashEchoer.h" #include "SashEcho.h" #include "sashIExtension.h" #include "sashVariantUtils.h" #include "extensiontools.h" #include <algorithm> // Note that we have slightly different macros here, since sashIEchoer // is not an extension. NS_IMPL_ISUPPORTS2_CI(SashEchoer, sashIEchoer, sashIConstructor); SashEchoer::SashEchoer() : m_Reverse(false), m_NumTimes(1) { NS_INIT_ISUPPORTS(); } SashEchoer::~SashEchoer() { } NS_IMETHODIMP SashEchoer::InitializeNewObject(sashIActionRuntime* act, sashIExtension* ext, JSContext* cx, PRUint32 argc, nsIVariant **argv, PRBool *ret) { // check to see if the arguments are valid if ((argc != 0 && argc != 3) || (argc == 3 && (! VariantIsString(argv[0]) || ! VariantIsNumber(argv[1]) || ! VariantIsBoolean(argv[2])))) { *ret = false; return NS_OK; } if (argc == 3) { m_Prefix = VariantGetString(argv[0]); m_NumTimes = (PRUint32) VariantGetNumber(argv[1]); m_Reverse = VariantGetBoolean(argv[2]); DEBUGMSG(echo, "Creating echoer with params %s, %d, %d\n", m_Prefix.c_str(), m_NumTimes, m_Reverse); } // we need to keep a pointer to the security manager around for Print act->GetSecurityManager(&m_pSecurityManager); // save the context, in case we need to call events m_pContext = cx; // make sure to set the return value to true on success, *ret = true; return NS_OK; }
So far so good. Now we can implement the rest of the functions defined in the IDL as usual.
NS_IMETHODIMP SashEchoer::GetPrefix(char **aPrefix) { XP_COPY_STRING(m_Prefix.c_str(), aPrefix); return NS_OK; } NS_IMETHODIMP SashEchoer::SetPrefix(const char *aPrefix) { m_Prefix = aPrefix; return NS_OK; } NS_IMETHODIMP SashEchoer::GetReverse(PRBool* aReverse) { *aReverse = m_Reverse; return NS_OK; } NS_IMETHODIMP SashEchoer::SetReverse(PRBool aReverse) { m_Reverse = aReverse; return NS_OK; } NS_IMETHODIMP SashEchoer::GetNumTimes(PRUint32* aNumTimes) { *aNumTimes = m_NumTimes; return NS_OK; } NS_IMETHODIMP SashEchoer::SetNumTimes(PRUint32 aNumTimes) { m_NumTimes = aNumTimes; return NS_OK; } // we should really extern this from SashEcho but for ease of implementation // we'll do it this way const SecuritySetting NUM_TIMES_SETTING_ID = 1; NS_IMETHODIMP SashEchoer::Print(const char* toPrint) { // since it's still the Sash.Echo extension, we use that GUID AssertSecurity(m_pSecurityManager, ToUpper(kSashEchoCID.ToString()), NUM_TIMES_SETTING_ID, (float) m_NumTimes); string p(toPrint); if (m_Reverse) reverse(p.begin(), p.end()); // STL algo for (PRUint32 i = 0 ; i < m_NumTimes ; i++) cout << m_Prefix << p << endl; return NS_OK; }
Phew. That's a lot of code. But hopefully by now this kind of stuff is old hat to you. So at this point it looks like we've got a fully functioning object here. All that remains is integrating it with Sash.Echo
. This involves tinkering around with the Echo
code quite a bit, so let's get started.
The first thing we need to do is add the generic constructor to the class definition in SashEcho.h
.
class sashIGenericConstructor; class SashEcho : public sashIEcho, public sashIExtension { ... sashIGenericConstructor *m_pEchoerConstructor; ... }
Now, some retooling in SashEcho.cpp
. First, of course, we'll
#include "sashIGenericConstructor.h" #include "SashEchoer.h" // since we need its CID
Then, since we need to manually define some module information to inform Mozilla about Echoer, we need to change the SASH_EXTENSION_IMPL
macro to SASH_EXTENSION_IMPL_NO_NSGET
:
SASH_EXTENSION_IMPL_NO_NSGET(SashEcho, sashIEcho, "Echo");
This macros simply takes the class name, the interface name, and the name to expose to JavaScript. Now, we need to create the constructor in the Sash.Echo
Initialize function by adding this line:
NS_IMETHODIMP SashEcho::Initialize(...) { ... NewSashConstructor(act, this, SASHECHOER_CONTRACT_ID, SASHIECHOER_IID_STR, &m_pEchoerConstructor); ... }
Lastly, since the generic constructor is a readonly attribute, we need to write a getter for it.
NS_IMETHODIMP SashEcho::GetEchoer(sashIGenericConstructor **ret) { NS_ADDREF(m_pEchoerConstructor); *ret = m_pEchoerConstructor; return NS_OK; }
That's it for SashEcho.cpp
. So we're done, right? Haha. Not quite -- but nice try. Since we've used the NO_NSGET
macro, we've got to define some module information ourselves. So, time to make a new file, SashEchoModule.cpp
.
// include all of your implementation header files here #include "SashEcho.h" #include "SashEchoer.h" #include "extensiontools.h" // we need one each of these for each object we're exposing -- // including the base extension itself NS_DECL_CLASSINFO(SashEcho); NS_DECL_CLASSINFO(SashEchoer); NS_GENERIC_FACTORY_CONSTRUCTOR(SashEcho); NS_GENERIC_FACTORY_CONSTRUCTOR(SashEchoer); static nsModuleComponentInfo components[] = { MODULE_COMPONENT_ENTRY(SashEcho, SASHECHO, "A simple example extension"), MODULE_COMPONENT_ENTRY(SashEchoer, SASHECHOER, "Echoer object") }; NS_IMPL_NSGETMODULE(SashEcho, components)
Don't forget to add these files to your makefile.
make install
your extension one last time. Try this code in the test weblication:
var echoers = new Array(); echoers.push(new Sash.Echo.Echoer("Prefix1 ", 1, false)); echoers.push(new Sash.Echo.Echoer("Prefix2 ", 5, true)); echoers.push(new Sash.Echo.Echoer("Prefix3 ", 3, false)); echoers.push(new Sash.Echo.Echoer("Prefix4 ", 1, true)); while((a = Sash.Core.UI.PromptForValue("Echo String", "Please enter a string to pass to the Echoers:", "")) != "") { foreach (var p in echoers) { echoers[p].Print(a); } }
If this sample works, give yourself a pat on the back. You're a 1337 SashXB h@x0r. Woohoo! If it doesn't work, examine the completed Sash.Echo source and see where yours differs.
If the extension you want to build is more complicated than the examples given here, and you can't figure out how to get it to work, don't give up hope. The best resource for help is other extension code. Chances are something's already been written that uses a technique you're trying to employ: threads (FTP, Sockets), objects within objects (Core), interface flattening (GTK), and so on.
If you're still having trouble, feel free to contact us and we'll be happy to help you out.
If you've tested your extension to your satisfaction and are ready to release it, here's how to do so. It might help to verify that it works on a clean install of SashXB, and that all dependencies are pulled down successfully. Then, make a suitable install screen (describing what your extension does) and add it in the Install section of the WDF editor. Rerun autogen with release flags:
> ./autogen --enable-build-settings=release
and make clean
and make
one last time. Your extension distribution consists of these files: your .wdf file, .tgz file, and .html install page. Drop us a note if you'd like us to consider your extension for our components gallery, or of course you can host it on your own web space. If you think someone else might write an extension that depends on yours, make your .idl files available as well.
Once you've released your extension, you may find it desirable to fix bugs or upgrade it. It is extremely important that when you do so you follow these guidelines:
It is essential to maintain compatibility between weblications and extensions. If an extension is updated without modifying the semantics of the existing API, all dependent weblications will work flawlessly with the new version, and ones that use any new API features can specify this version of the extension as the minimum necessary. However, if the existing API is changed, a unique GUID will differentiate the new version from the old version, thus eliminating the potential for incompatibility. This process allows multiple versions of an extension to exist simultaneously on one machine, which ensures that weblications will not break due to API changes.
Creating a location that runs SashXB weblications is not much harder than creating an extension. Instead of using the SASH_EXTENSION_*
macros, use the SASH_LOCATION_*
macros. Locations must also list NS_DECL_SASHILOCATION
in their class declaration (in addition to NS_DECL_SASHIEXTENSION
) and include sashILocation.h
, as well as inherit from sashILocation
instead of sashIExtension
.
To support the sashILocation
interface, your location must implement the methods Run
and Quit
, neither of which takes any arguments, as well as all the functions in the sashIExtension
interface. Run()
is responsible for displaying and executing the weblication that is using it as a location. The names of the JS and/or HTML files to evaluate are generally listed in the XML registration string passed to Initialize()
. Quit()
lets your location shut down gracefully; make sure, however, to invoke the runtime's Cleanup()
function before exiting.
We provide two simplified ways of executing weblication code, whatever your chosen location might be. Rather than step through examples of each, we suggest that you look at existing locations to see how each component can be utilized. Once you've set up one of these components you can add additional functionality to your location just as you would an extension.
JSEmbed wraps a JavaScript interpreter. Use it if your location does not need a full HTML renderer (for instance if it relies on a totally native UI) and would benefit from the decreased overhead. Include SashXB/extensiontools/sashJSEmbed.h
(for the NewSashJSEmbed()
function), and use the interface defined in SashXB/includes/sashIJSEmbed.idl
to work with the interpreter. Check out the Console
location for a working example.
SashGtkBrowser wraps a Mozilla rendering engine and a JavaScript interpreter. It'll add the Sash.*
namespace for you. Use this if you want the weblication writer to be able to design his UI in HTML. Include SashXB/extensiontools/SashGtkBrowser.h
(for the NewSashGtkBrowser()
function), and use the SashXB/includes/sashIGtkBrowser.idl
interface (please don't ask me to justify the random naming "scheme") to use the browser. See the WindowApp
location (specifically, MainWindow.cpp
) for a working example.