Tab Julius

Subscribe to Tab Julius: eMailAlertsEmail Alerts
Get Tab Julius: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Article

Architecting with Director

Writing Xtras

In my last article (MXDJ, Vol. 2, issue 2), we looked at how Macromedia Director is extensible, primarily through Xtras (plug-ins); and that there are four major types of Xtras - Scripting/Lingo Xtras, Sprite Xtras, Transition Xtras, and Tool Xtras.

Now it's time to look specifically at what's involved in rolling your own Xtra. A full treatment of all the ins and outs, gotchas and nuances of writing Xtras is unfortunately beyond the scope of this article (indeed, it would fill a small book), but we do have enough space to start to show you how to write a basic Scripting Xtra.

A Scripting Xtra (they used to be called Lingo Xtras) allows you to call C/C++ functions from within Lingo. You commonly call them in one of two ways - the first by instantiating the Xtra, as in:

infile =new(xtra "fileio")

which returns an instance you can work with. Alternatively, you can create a global command that you can just invoke from Lingo, no instantiating involved, as in:

printMyFile("filename")

If such a command existed, you could just use it directly, no objects to fiddle with, no muss, no fuss. Although the global commands might seem less troublesome to use, and are easier for beginners, the instantiation method is actually more powerful, because each instance can carry along its own set of internal variables. Thus, in our FileIO usage example above (FileIO is an Xtra that comes with Director for reading and writing text files), you can open up multiple instances, one for an input file, one for an output file, and so on, and each instance will keep track of its own internal data.

On Windows, Xtras are really .DLLs with a special entry point; you compile them in Visual C. The Macintosh is similar, but on the Mac they're code fragments. CodeWarrior is used for compiling on the Mac.

To write an Xtra, you should really be familiar with C/C++. Some people have experimented with writing Xtras in other languages like Delphi and Visual Basic, but the developer's kit assumes you're using C/C++ and adapting it to other languages isn't trivial. Speaking of the developer's kit, you will need one. It's called the XDK (Xtras Developer's Kit) and it's free. You get it from Macromedia at their website - just search for XDK. There are XDKs for various products, such as Authorware, but in this article we will focus on writing an Xtra for Director.

The XDK for Windows has no binaries, just header files. The XDK for Macintosh now has some special binaries for Carbon development that will need to be included in your projects. The XDK includes both examples and template projects. For reasons of space, we'll simply focus on Windows in this article.

You should unpack the XDK onto your hard drive. I usually make the version of the XDK part of the filepath, as XDKs differ with successive releases. The current XDK is for Director 8.5 (yes, it lags behind the product release) and I use a folder named \XDK_D85. Within that folder will be a folder named INCLUDE, which will need to be in your compiler's include path; a folder called DOCS made up of .HTM files documenting the API; and a folder called EXAMPLES.

The Examples folder is subdivided into sections relating to each of the different common Xtra types. We'll be looking in the Examples\Script folder. That folder is further divided into DrAccess, Skeleton, Skeleton2, and valueChecker. Of these, DrAccess and valueChecker are actual examples and Skeleton and Skeleton2 are the templates that you can base your project on.

Of the skeletons, Skeleton is the older style template, with multiple files. In Skeleton2, the template has been reduced from five files to two (it's the second version of the skeleton). In fact, Skeleton itself has undergone significant changes since the early releases, but it is only with Director 8.5 that the slimmer Skeleton2 has been introduced. In this article, though, we will use the original Skeleton; that way if you're writing an Xtra for Director 8, which only supports the original Skeleton, you won't be lost.

To begin, copy the files from the skeleton to wherever you want to be working. For example, C:\XTRASDEV\MYXTRA\, copying the winproj and source subfolders. You will have five source files:

  • CREGSTER.CPP
  • CREGSTER.H
  • CSCRIPT.CPP
  • CSCRIPT.H
  • XTRA.CPP
If you choose to open up the project SCRIPT.DSW (on Windows) you will want to remove the listed source files from the workspace and reimport them. The reason for this is that they won't point to your source files in your new location, and it's just easier to reimport them. Make sure to import the .DEF file as found in your WinProj folder. Failure to import this file means that the Xtra will compile but will be ignored by Director when you go to run Director. It will seem as if it didn't exist.

Now is also a good time to correct your project settings. In the Link settings I like to compile the Xtra directly into the Director Xtras folder (it makes debugging much easier than compiling to one place and copying it over). In the Preprocessor settings for the C/C++ language I usually correct the search path to be ..\source because the project folder is one folder over from the source folder; and I correct the path to the XDK Include folder because I don't compile from within the Xtras folder and the path is relative to the examples.

Now you can work on the files themselves. As I mentioned, there are five files in the original version of the skeleton. The first, Xtra.cpp, mainly concerns itself with versioning. You can put your version numbers here, but for now we'll leave it alone. You won't have to touch it for this example.

The CREGSTER files control the registration aspect of loading an Xtra. When Director starts up, the runtime Xtra looks in the Xtras folder for any Xtras and queries them to see what kind of Xtra they are and what capabilities they possess. This is the point at which the Xtras register themselves, and is where the CREGSTER files come into play.

For a scripting Xtra, there are three things you need to do with the CREGISTER files. The first is in CREGSTER.H. You must generate a GUID for the registration class. Each class has a GUID, or unique identifier. You need to have unique identifiers for each class, and to generate a new set for every Xtra you write; failure to do so will cause Director to complain that it has duplicate Xtras in its folder. GUIDs are generally a combination of your Ethernet address from your network card and a date/time stamp. Visual C comes with a utility in its BIN folder called GUIDGEN.EXE; you can run this program, make a GUID, and copy it into your source file.

CSCRIPT.H and CREGSTER.H both have sections where they want a GUID, and you have to give them different ones. Look in each .H file for a line that says #error PLEASE DEFINE A NEW CLSID

Although we haven't gotten to CSCRIPT.H in this discussion yet, I usually create both my GUIDs at the same time just because it's quicker.

To make a GUID, run GUIDGEN.EXE, choose the format of GUID that says DEFINE_GUID(...), and then press the Copy button to copy the GUID to the clipboard. You can then paste that into the .H file. To make a second one, press New GUID and then Copy again. When you paste, the number will come in like:

// {A53645A1-557D-11d8-9F02-0010B53FC39F}
DEFINE_GUID(<<name>>,
0xa53645a1, 0x557d, 0x11d8, 0x9f, 0x2, 0x0, 0x10, 0xb5, 0x3f, 0xc3, 0x9f);

You will need to replace <<name>> with CLSID(CRegister) in CREGSTER.H and CLSID(CScript) in CSCRIPT.H. Then you your GUIDs will be defined for the CScript class and the Cregister class.

This is a good time to point out that all of the example files have a comment at the top that says "How to Customize This File" and tells you to rename the file, adjust the included filename references accordingly, rename all the classes to be the name of your class, and so on. I can tell you that the easiest thing to do is to not bother with any of that. I used to spend all sorts of time renaming and customizing and realized it was not only a huge waste but prone to error as well. Now I just use the default file names and classes and it works just fine. As long as you keep your Xtras in separate folders, which is a good idea anyway, you won't run into trouble.

There isn't anything else in CREGSTER.H that you have to bother with in a scripting Xtra. There are no class instance variables here that you would normally set up. So, the next stop is CREGISTER.CPP.

There are only two things you'll probably need to do in CREGSTER.CPP, at least for a scripting Xtra. One is that if you intend for your Xtra to run in Shockwave, you will have to declare it as "Safe for Shockwave". Declaring it as safe doesn't make it so, it just tells Shockwave that you say it is so, and Shockwave will allow the Xtra to load. It is your responsibility to make sure that the Xtra does not allow Shockwave access to the user's machine without their knowledge in any way that could violate their security, data integrity, or privacy.

If you do want to declare it as Safe for Shockwave you'll need to incorporate Code I into the CRegister_IMoaRegister::Register function, just after the section that registers the method/message table - generally the last block of code before the return code.

You may or may not need the #define's depending on what version of the XDK you have. If the compiler complains they're already defined, comment them out.

Defining Your Message Table
The message table is where the fun starts. This is where you get to declare the commands your Xtra will support. In the original skeleton the method table appears about halfway through the file (you can't miss it). The format of the message table is something like Code II.

You can see many examples of message tables by using PUT in the message window to put the interface for other scripting Xtras (in Director MX you can use the third-party scripting Xtras button on the message window to do the same thing).

If you look at the second line of the sample table above, you'll see where it says xtra MyXtra. That declares the internal name of the Xtra to be "MyXtra". Thus, when you instantiate it, or put the interface out to the message window for it, you'll refer to it as xtra "MyXtra", as in:

put interface(xtra "MyXtra")

Note that in the message table there are no quotes around the name itself, but there are when the command is issued from the message window.

After the declaration of the name of the Xtra you can have comments - multiple comments if you wish - then you define the commands. There is always a "new" command, but after that you can have one or more of your own defined commands. This example defines two. You can follow each command with a comment describing the command, although I don't often do this on Xtras with lots of functions lest I run past the maximum string space (remember, the message table is a string, just a big one).

Commands preceded by "*" are global commands, meaning you can just issue them from Lingo without instantiating anything. Commands without the asterisk (like the "new" command shown) are child commands and must be tied to an instance, meaning that you must instantiate the Xtra before you issue a call to that function.

After the command name, you can optionally allow parameters to be passed in. Specify the type that is required and Lingo will do basic validation during a call to make sure that the right type is passed in. For instance, you could specify integer, integer, string, and Lingo would require the user to pass in two integers and a string; otherwise, it would throw an error. The argument types are integer, float, string, symbol, object, any, and *; the asterisk is a wild card, allowing any number of arguments, of any type. If used, it should be the last entry on the line.

Optionally, you can have names with each arg, to make it easier for the user, as shown in the FixCertainBug example; or you can omit the names and just have the arg types.

Type Any is used to allow any type - not restricted - but it's also used to allow types that aren't listed. For instance, if you wanted to pass down a member spec (e.g., member 8 of castlib 1), it won't officially be an integer, float, string, etc. For a parameter like that you would use Any to allow it in.

If you required a minimum of four parameters, the third of which can be of any type, you could do:

FixCertainBug integer, string, any, string, *

Lingo will only allow that call to go through if the user supplies at least four parameters, the first of which is an integer, and the second and fourth of which are strings. Note that Lingo will do as much validation as it can for you, but if you use Any or * you will be responsible for validating those parts when called, to make sure that you got what you wanted from the user.

Child functions (that must be instantiated) must always have an object type as the first parameter, as in:

"myChild object me, string newName\n"

The Enum Table and CSCRIPT.H
When your Xtra is invoked from Lingo you will be called through the function ::Call, and passed in a number that tells you which command from your message table was invoked. New (the first command) is always #0; in the example message table, FixAllBugs would be #1 and FixCertainBug would be #2, etc.

It's easy to introduce bugs if you're just checking by number. If you were to move commands around in the table you'd have to reorder all your code. To keep problems to a minimum, the Xtras use an enum table, found in CSCRIPT.H, that looks like:

enum
{
m_new =0,

m_fixAllBugs,
m_fixCertainBug,

m_XXXX
};

With such a table, all you have to do is make sure the order of enums in the enum table matches the order of commands in the message table. If you move them around in the message table, you must do so in the enum table. If you delete, or comment out, entries in the message table, you must make the corresponding changes in the enum table. As long as they're in sync, things will work. If you forget to make an entry, or change an entry, then you'll find your program executing one command when you intended for it to execute another. The m_XXXX entry at the end serves no purpose other than to let you keep a comma at the end of all of your entries, without having to remember to add or remove commas when moving enums around.

Class Instance Variables
Any variables your Xtra wants to keep around between calls should be kept in the Class Instance Variable section in CSCRIPT.H, rather than using globals. If your Xtra is instantiated, then the variables here are specific to each instance. If you don't instantiate your Xtra and just use global Lingo commands, they'll still work - they'll just apply to the one "global instance" of the Xtra. You only need to use true global, non-class instance variables if there's something you need to share between instances, although this practice is discouraged.

Here's Where It All Happens
Let's turn our attention to CSCRIPT.CPP, which is where the guts of the Xtra will be. There are three parts you need to concern yourself with:

  1. The ::Call function, which is the main entry point for the Xtra, where you will test for how your Xtra was called and react accordingly.
  2. Any individual functions you wish to set up. Although you could do all your work in the ::Call function, typically developers use ::Call simply as a dispatcher and make a corresponding function for each command supported by the Xtra.
  3. The Create/Destroy method, which is where you will typically acquire (or free) any interfaces you need - and you will typically need a few - as well as where you will initialize your instance variables.
Each function you make will have a corresponding prototype; on some versions of the XDK the prototypes are kept in CSCRIPT.H, in other versions they are at the top of CSCRIPT.CPP. In the D8.5 XDK they are in CSCRIPT.H - look for the section EXTERN_BEGIN_DEFINE_CLASS_INTERFACE for CScript, which declares Call as a public interface and then lists XScriptGlobalHandler, XScriptParentHandler, and XScriptChildHandler prototypes. These are examples only of Global/Parent/Child handlers, and the names are not a requirement. In fact, I would delete them and replace them with the equivalent XScriptFixAllBugs, XScriptFixCertainBug, etc.

In CSCRIPT.CPP you will find the corresponding functions near the top of the file - again, replace the names with the names of your functions and delete the ones you're not using. Make them match your prototypes.

At this point, the only major thing left to do is to tweak the ::Call function so that it invokes the functions properly. It will still be necessary to access the args passed in, and learn how to return values, and so forth, but we'll have to defer that part until next time. For the moment we'll get it up and limping, and get it to say "Hello World", which is enough to claim victory for now.

As I mentioned, when someone calls a function in your Xtra, the ::Call function is invoked. For our example, the ::Call function should end up looking like Code III.

The whole flow of events is now in place. The message table lists two commands besides New - fixAllBugs and fixCertainBug. FixAllBugs takes no parameters. The enum table lists three enums, one for New and one each for m_fixAllBugs and m_fixCertainBug. When the user invokes FixAllBugs(), that will be found as the second entry (0-based) in the message table, or rather the first one after New (0). Since the entries in the enum table are in the same order as those in the message table, m_FixAllBugs has an internal value of 1. This is how it's supposed to work, because when ::Call is invoked it is passed callPtr, which has a field, methodSelector, that has a value of 0..n, up to the number of entries in your message table less one. The net effect is that by syncing an enum table to your message table, you can respond with mnemonic enum names, not by keeping track of index numbers into the table.

The code in ::Call will switch to the case statement for m_fixAllBugs, which will invoke XScrpFixAllBugs. If you then modify the function XScripFixAllBugs you can complete the cycle (see Code IV).

Compile it, and you now can restart Director. If everything is in your favor, the Xtra will be loaded, you can issue the command fixAllBugs() from either the message window or from a script, and a "Hello World" message box should pop up!

Conclusion
Next time we'll take a closer look at how parameter passing works, how to return data back to Director, and what gotchas to look out for; and we'll take a peek at some of the MOA (Macromedia Open Architecture) classes. Until then, enjoy!

More Stories By Tab Julius

Tab Julius has been writing software since the mid-70s, and now works for a software firm developing medical imaging applications, although he still does limited consulting on the side.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.