Article

MOA City

MOA City

If I promised to take you to see a foreign city, one where you were not fluent in the language, and didn't know your way around, and then actually flew you there to visit, would you wnat me to just drop you off in the center of town and leave you there?

Well, that's sort of what has happened in my last two articles on Xtras (MXDJ, Vol. 2, issues 2-3) - space considerations precluded us from doing everything at once.

In my last article, I got you to the city. We went through the steps of building an Xtra and got it so that you could issue a Lingo command that would invoke a "Hello, World" alert box generated from within the Xtra.

And there we left it. No time to discuss what happens next, no time to talk about how to get data in or out of Director, no discussion of what powers you have access to. I just left you in the city and drove away.

Now we're going to rectify some of that. You've made it to MOA (Macromedia Open Architecture) City. What now?

Structure
The first thing we need to do is to get oriented. Before we can be productive, we have a few basic chores to do.

All of MOA is organized into classes. It is very COM-like. If you need a particular function, you first have to get a pointer to the interface for the class that it exists in, and from there you can call the function you want. The most common interfaces are typically acquired up front, during create time, and released at the end; then you just keep the pointers around and use them as needed.

The classes I most commonly acquire in advance are:

  • IMoaDrPlayer: A pointer to the interface for the runtime engine, this lets you get a pointer to the active movie (the one currently executing). From the active movie you can work your way to the score, or to the casts, and so on. This is a Director-only interface.
  • IMoaMmUtils2: This has some color support, but mostly it has functions to print messages to the message window - great for debugging.
  • IMoaMmValue: This is an invaluable (no pun intended) interface - it lets you do conversions from the generic value format to most anything else (strings, integers, symbols, lists, and so on). Lingo is a loosely typed language, which means that a variable can be freely assigned to anything, it's not restricted to a certain type of data (as opposed to C/C++, where integers can only be assigned to variables declared to be integer variables; that would be an example of a more strongly typed language). To pull off the loose typing of Lingo, a variable really has two internal components: a struct consisting of the "type"component, a flag indicating what type of data it contains; and a "data" component, which would be a pointer to the data. All variables are assigned to these value structs; to assign a different kind of data, MOA just changes the type indicator and sets a pointer to the new data. That's MOA, though - you won't fiddle with a variable's innards yourself directly. What it all comes down to, though, is that all data passed in or out of the Xtra, to or from Lingo, is in the form of values, and you must use the IMoaMmValue interface to make the conversions.
  • IMoaMmList: Gives you the power to manipulate lists in Lingo. You can parse ones sent in, create ones to send back, and so on.

    There are many others to work with, though, since there are well over 100 class interfaces. There are classes for manipulating files, memory, and streams, getting app info; creating dialogs; and more. Some are for obscure uses, some you may never use, some you may use a lot. It all depends on what your project entails.

    The interfaces are documented in the DOCS section of the XDK. This is a good time to quickly review the files in the docs, since you'll be referencing them a lot from this point forward. The DOCS section is organized into folders, such as MOADG, MOREF, MMDG, MMREF, DRDG, DRREF, AWDG, and AWREF. Clearly there is a pattern here.

    The first two or three letters refer to the category. MOA and MO mean MOA itself, MM is the multimedia functions, DR refers to classes specific to Director, and AW refers to classes specific to Authorware. The last two or three refer to the type of documentation - DG means "design guide" (more of a how-to discussion), and REF means "reference" (where you would go to look things up). So to look up a Director-specific interface, such as getting a pointer to the score, you would look in DRREF. Once in there, though, you will find over 30 files, so you will have to root around, but each folder has an index.htm file that you can start with and work your way from there.

    So, how do you "get" an interface anyway? You do so by calling QueryInterface and telling it what interface you want, and where you want the pointer for it to be kept. As mentioned, I liked to get the common interfaces at Create time and keep them around, so most of my QueryInterface calls are made in MoaCreate_CScript, which is invoked when the Xtra is instantiated. First, though, it is necessary to declare variables for the pointers. The variables should go in the class instance variables section in CSCRIPT.H, as in:

    EXTERN_BEGIN_DEFINE_CLASS_INSTANCE_VARS(CScript)
    PIMoaMmValue pMmValue;
    PIMoaMmUtils2 pMmUtils;
    PIMoaMmList pMmList;
    PIMoaDrPlayer pDrPlayer;

    /* your other variables are defined here */
    EXTERN_END_DEFINE_CLASS_INSTANCE_VARS

    Now, any time your Xtra is called the functions will have access to these variables (they'll stay static). However, we need to set them up when the Xtra is instantiated and, as mentioned, we'll do that in MoaCreate_CScript like so:

    err = This->pCallback->QueryInterface( &IID_IMoaMmValue,
    (PPMoaVoid)&This->pMmValue);
    err = This->pCallback->QueryInterface( &IID_IMoaMmUtils2,
    (PPMoaVoid)&This->pMmUtils);
    err = This->pCallback->QueryInterface( &IID_IMoaMmList,
    (PPMoaVoid)&This->pMmList);
    err = This->pCallback->QueryInterface( &IID_IMoaDrPlayer,
    (PPMoaVoid)&This->pDrPlayer);

    I have never had the common interfaces (Value, Utils, etc.) fail, as they are common to most of MOA. If you want to be a good citizen you could check the return code in case there is an attempt to call your Xtra from some Macromedia product other than that for which it was intended. More likely you might run into problems with Director-specific interfaces, like DrPlayer, which would not be available in, say, Authorware. If you were trying to make a cross-product Xtra, you'd have to test (via IID_IMoaAppInfo) to see what product you were running in and get the corresponding interface (or refuse to run).

    Now you should have those interfaces for the life of your Xtra's instance. The other key thing we need to do now is to remember to release them upon our destroy, which would be done as shown in Code I.

    Using the Interfaces
    Now that you've got some interfaces, let's put them to use! In the last article we discussed the message table, which is where you define scripting commands and the parameters you can allow to be passed in, and we managed to make a call into the Xtra itself. But we didn't have a chance to look at how to access those parameters, or how to pass a return value back out.

    The example function we had with parameters was:

    "* FixCertainBug integer bugNum, string fixName\n"

    As a quick refresher, the asterisk in front means that it's a global command (you don't have to explicitly instantiate the Xtra) and requires two parameters - an integer and a string. The given names bugNum and fixName are for user readability only; they have no impact on anything and are, in fact, optional.

    The base function we used last time for FixCertainBug looked like this:

    MoaError CScript_IMoaMmXScript::XScrpFixAllBugs(PMoaDrCallInfo callPtr)
    {
    	UNUSED(callPtr);
    	
    	MoaError err = kMoaErr_NoErr;
    
    	MessageBox(NULL, "Hello, World", "", MB_OK);
    
    	return(err);
    }
    

    We'll now strip it down a bit to:

    MoaError CScript_IMoaMmXScript::XScrpFixAllBugs(PMoaDrCallInfo callPtr)
    {
    	MoaError err = kMoaErr_NoErr;
    
    	return(err);
    } /* fix all bugs */
    

    I mainly just took out the UNUSED(callPtr) line and the MessageBox call. UNUSED() is just a macro to keep the compiler from complaining because callPtr is passed in but is not necessarily used in all functions.

    You might immediately think that one part is obvious - returning values back to Lingo, where at the end of the function there's a line:

    return(err)

    But, in fact, it's deceptive. That's not how you return a value to Lingo. What you are returning there is a result code that indicates to the Director runtime engine whether or not you were successful in processing the call. Err is set to kMoaErr_NoErr by default. You would only send back something different if you wanted Director to throw an error showing that you could not process the command.

    One example of where you might want to send something other than kMoaErr_NoErr back might be if you failed to, for example, allocate memory. You could choose to handle this yourself, or you might wish to have Director throw an error. If so, you could send back kMoaErr_OutOfMem. Or, possibly the user passed in a parameter that you don't allow; you could send back kMoaErr_BadParam and let Director do the complaining.

    The error codes are defined in three files: MOATYPES.H, MMTYPES.H, and DRTYPES.H. These are not only codes that you can send back, but also codes that you might receive as a result of some failed call to a function that you make internally to MOA. DRIXLNGO.H says you can just send back _ArgOutOfRange, _OutOfMem, _InternalError, and _ValueTypeMismatch, but I've had success sending back other values.

    You send back a result code to Lingo itself via the callPtr, which is a parameter that is a pointer to a structure that has all the pertinent information about Lingo's call to your Xtra - specifically methodSelector, resultValue, nargs, and pArgs.

  • methodSelector: The index into your method table so you know what function of yours was called. We coupled this with an enum to get as far as invoking the right function in your Xtra, so we've done that part already.
  • resultValue: What is returned to Lingo. This is what you would stuff with the value of whatever you want to send back - a string, a symbol, an integer, a list, and so on.
  • nargs: A count of the parameters passed in. If you set your message table to have a fixed number of arguments, you won't need this, but if the argument amount is variable (by using an asterisk as the last parameter on the parameter line), then you need to test to see how many were actually passed in.
  • pArgs: A pointer to the args, but you don't really use it directly. Instead, there's a macro we use to get at them called AccessArgByIndex.

    The args passed in may or may not be preceded by an initial arg for the object. This would depend on whether or not the command is a global command. A child command, as discussed last month, where the Xtra would need to be instantiated, always passes in the object instance as the first parameter, often in the form of:

    createFile object me, string fileName

    This allows you to call it from Lingo after having instantiated it, as in:

    CreateFile(fileObj, "C:\TEST.TXT")

    What this means to you on the Xtra side is that you have to account for that object when you access your args. Failure to do so will mean that you get an object instead of an integer or string or whatever you expected.

    I like to set up a define called ARG_BASE and set it to 0 or 1 depending on whether or not the commands are global (typically I have them all one or all the other; I usually don't mix globals and children in the same Xtra, although there's no technical reason why not). Then I can just reference the parameters in the order I'm expecting them, without worrying about remembering to account for any preceding object parameter. If I choose to convert all of them from global to child (or child to global), I only need to change the message table and the define, and not all the code I have internally.

    I might define:

    #define ARG_BASE 1

    (as you would for an Xtra that had child commands) and then could just reference each arg that I was expecting as ARG_BASE + 1 for the first arg, ARG_BASE + 2 for my second arg, etc. Try it; you'll see that it helps.

    At any rate, we need to look at how to access args, which are all in MoaMmValue form. As I mentioned earlier, the IMoaMmValue interface provides functions for converting to and from values. Normally it is your responsibility to release any values you create (because they do allocate memory). If you were to create a value for a symbol for the purposes of supplying it to some function, it would normally be your responsibility to release that value when you were done. The one notable exception to this has to do with parameter passing through the callPtr, which is what we're discussing here. You do not release the values passed in to you, nor do you release the value that you send back via resultValue. If you do release them, Director will die a most horrible death upon returning from a supposedly successful call to your Xtra, and you would otherwise have quite a time figuring out why.

    The AccessArgByIndex macro lets you get your fingers on the parameters. The FixCertainBug example function takes two parameters, an integer bugNum and a string fixName. We could process the call as shown in Code II.

    A couple of points here: pObj-> pMmValue is the pointer to the IMoaMmValue interface that you got during create time. An interesting distinction between create/destroy time and "the rest of the time" is that in create/destroy, when you reference your class instance variables you do so as This->pMmValue but in the rest of the program you use pObj->pMmValue. This is a lot more straightforward than it used to be, believe me. I mention it because the compiler will not allow pObj-> to be used from create/destroy, and it won't allow This-> to be used anywhere else. FYI.

    Also, I added ARG_BASE + to when I was referencing the arg number, which is strictly unnecessary because ARG_BASE is 0, but it's a good habit and saves trouble later, as we have already discussed.

    Finally, you will notice that I didn't release the value at any time. When we do the AccessArgByIndex we are not creating a value, we are simply accessing one that is already created and assigning it to our internal variable, hence no need to release it. Likewise on the resultValue, Director will release that as well. As I said, this is basically the only time this is the case. Normally you will always be responsible for releasing the values you create unless otherwise pointed out in the documentation for an interface.

    So, we have now accessed our passed-in parameters and also returned values back! The user on the other end would receive back an integer, 1 (TRUE).

    Since we were also passed in a name for a fix, we can practice printing the information out to the message window. The interface IMoaMmUtils2 has functions PrintMessage, PrintMessage1, PrintMessage2, PrintMessage3, and PrintMessage4, all of which print messages to the message window. The difference between them is how many optional parameters they take - PrintMessage by itself parses no extra parameters. Parameters are given in C format, as you might do with sprintf:

    pObj->pMmUtils->PrintMessage2("Now fixing bug %d, name: %s\n", 
                    whichBug, (MoaLong)&fixInfo[0])
    

    It's not a 100% carbon copy of sprintf form; for instance, you can't pass a string in directly but you have to give the pointer to it like I did here - but it's close enough.

    As you have undoubtedly noticed, there are variable types like MoaLong, MoaChar, and so forth. These are generally mapped 1: 1 to regular longs, ints, and chars. You can, in fact, use them interchangeably, but they do provide a layer of abstraction. I usually try to use the MOA form when I'm paying attention. At this point in the XDK life cycle there are no conversion functions, and I freely use MoaLong to pass to C functions or whatever, but that may change at some point in the future.

    Summary
    I will close with a couple of quick notes on IMoaMmValue and a comment on the naming of interfaces within MOA itself. First, everything we've said so far is predicated on knowing ahead of time what type of data (e.g., integer) a value is. What if you don't know what kind of data it holds? IMoaMmValue has a function called ValueType() which, if passed a value, will return kMoaMmValueType_xxxx where xxxx would be Void, Integer, Symbol, Member, and more (the complete list is in mmtypes.h). As a shortcut, though, if you allow a parameter to be either an integer or a string, rather than test for the valueType, you can try to convert it to integer and then check the error code. If it fails, try to convert it to string, and so on.

    You may note that I referred to IMoaMmUtils2; what happened to IMoaMmUtils1? Over time, the XDK has evolved, and the class interfaces are generally backward compatible when they are expanded. Occasionally, though, they are not, and when that has happened Macromedia has left the original class alone so as to not break any existing Xtras. Instead, they created a new variant that supersedes the old one, usually appending a 2 to the end (there are no 3s yet to my knowledge). So the original IMoaMmUtils is obsolete and you should use IMoaMmUtils2 instead. IMoaFile2 and IMoaStr2 are other examples of this. If I remember correctly, all the value conversions were originally in IMoaMmUtils but were later migrated out into IMoaMmValue, hence the reworking of IMoaMmUtils.

    You should now be in a position to start experimenting with passing information in and out of an Xtra, and maybe even acting on that information. Next time we'll delve a little further into MOA and learn a little bit more. 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 (1)

    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.