Articles

Writing Externals with Xcode 2.2

Xcode is becoming increasing popular as an IDE to develop external objects for Max and MSP (Jitter support for Xcode is not yet supported, but is forthcoming). It offers several benefits over using Code Warrior, not least of which is that Xcode is free. However, if you've gotten used to the comforts of CodeWarrior, then Xcode can seem rather bizarre and alien.

In this article we will take a step-by-step approach on how to write externals from scratch using Apple's latest developer tools. We will not discuss the source code itself very much, as that information is well covered in the MaxMSP Software Development Kits. We will also approach this topic in a tutorial-like style. That means that we will let a few things slip in order to see what some common errors look like and how we can go about solving them.

Update [2006-1-26 8:53:54 by tim]: This article was originally written for Xcode 2.1, it has now been updated for Xcode 2.2. TAP.

Here we go:

  1. Download the SDK

  1. Copy the c74support folder into the /Library/Application Support folder.     

  1. Create a new project. This can be done by selecting the File > New Project... menu item. You will be presented with the assistant dialog. Choose Carbon Bundle and click next.

  2. Now you need to name your project and create it. For the purpose of this article, we will create a simple external that rounds floating-point numbers to the nearest integer. We will call this project round and save it in our home (~) folder.

  1. You should now see Xcode's main interface window.     

  1. The empty project that Xcode created for us has a source file called main.c. We could use this as-is, but to make things easier on ourselves later we will rename the source file to round.c.     

  1. In order to make this project work with Max, we have to add Max's glue framework.     Right-click (control-click) on the Frameworks folder, and choose Add > Existing Frameworks....     .     The Framework is located at /Library/Frameworks as shown in the screenshot     .

  1. Go to Project > Edit Active Target in the menu. In the Build tab, we need to tell Xcode about our search paths (where it needs to look for files). Scroll around in the list until you find the Header Search Paths. Fill it in with "/Library/Application Support/c74support/max-includes" (it is essential that you DO use the the quotes, as Xcode uses whitespace to separate multiple paths)

  2. We have a couple of more things to do while still in the Build tab:

  • You need to specify the Framework Paths as /Library/Frameworks     

  • Change the wrapper extension from bundle to mxo.     

  • Change the Other Linker Flags to have the value -lmx.     

  • Scroll down and clear the Installation Directory so that it has no value.     

  • Now switch to the Properties tab in the Active Target editor. Change the type to iLaX.     Then we can close the Active Target editor.

  • At this point, we are done making changes to the project settings.     To make sure that we haven't made a mistake, type the following code into     our source file - round.c - and compile the code by clicking on the hammer     icon or choosing Build > Build from the menu.

int main(void)
{
    return 0;
}

If all went well, it should say "Succeeded" in the lower right-hand corner     of the main project window.

  • Now lets put in the actual code for our object. We won't comment much on     the code, but it does use the newer Obex methods for defining the class,     attributes, methods, etc. that was introduced with Max 4.5.

#include "ext.h"             // Max Header
#include "ext_strings.h"     // String Functions
#include "commonsyms.h"      // Common symbols used by the Max 4.5 API
#include "ext_obex.h"        // Max Object Extensions

// Data Structure for this object
typedef struct _round{
    t_object    ob;          // Must always be the 1st field; used by Max
    void        *obex;       // Pointer to Obex object
    void        *outlet;     // Pointer to outlet
} t_round;

// Prototypes for methods: need a method for each incoming message
void *round_new(long value);
void round_float(t_round *x, double value);
void round_int(t_round *x, long value);
void round_assist(t_round *round, void *b, long m, long a, char *s);

// Globals
t_class        *this_class;     // Required: Global pointing to this class 

/**************************************************************************/
// Main() Function - Object Class Definition

int main(void)
{
    long attrflags = 0;
    t_class *c;
    t_object *attr;

    common_symbols_init();

    // Define our class
    c = class_new("round",(method)round_new, (method)0L,
        (short)sizeof(t_round), (method)0L,
        A_DEFLONG, 0);

    class_obexoffset_set(c, calcoffset(t_round, obex));

    // Make methods accessible for our class:
    class_addmethod(c, (method)round_int,            "int", A_LONG, 0L);
    class_addmethod(c, (method)round_float,          "float", A_FLOAT, 0L);
    class_addmethod(c, (method)round_assist,         "assist", A_CANT, 0L);
    class_addmethod(c, (method)object_obex_dumpout,  "dumpout", A_CANT,0);
    class_addmethod(c, (method)object_obex_quickref, "quickref", A_CANT, 0);

    // Finalize our class
    class_register(CLASS_BOX, c);
    this_class = c;
    return 0;
}

/**************************************************************************/
// Object Life

// Create an instance of our object
void *round_new(long value)
{
    t_round *x;

    x = (t_round *)object_alloc(this_class);
    if(x){
        object_obex_store((void *)x, _sym_dumpout,
            (t_object *)outlet_new(x,NULL));
        x->outlet = intout(x);     // Create the outlet
    }
    return(x);                     // return pointer to the new instance
}

/**************************************************************************/
// Methods bound to input/inlets

// Method for Assistance Messages
void round_assist(t_round *x, void *b, long msg, long arg, char *dst)
{
    if(msg==1)        // Inlets
        strcpy(dst, "(int/float) number to round");
    else if(msg==2) // Outlets
        strcpy(dst, "(int) rounded number");
}

// INT input
void round_int(t_round *x, long value)
{
    outlet_int(x->outlet, value);
}

// FLOAT input
void round_float(t_round *x, double value)
{
    long out;

    if(value > 0)
        out = ((long)(value + 0.5));
    else
        out = ((long)(value - 0.5));

    outlet_int(x->outlet, out);
}
  • Now start up max and lets try it out.

A word of warning: This has nothing to do with Mach-O or Xcode, but we called our external "round". As of this writing there are a lot of people with patches called "round" or something similar. Furthermore, while Max does not currently ship with an external called "round" it is always possible that it could do so in the future. All developers are advised to name externals with a unique identifier in the name. As an example, my initials are TAP - so all of my externals start with that (such as a tap.delay~, which also makes a nice pun).

We can tell in this case that the correct object has loaded because it has two outlets. It would be pretty rare to find other rounding objects with 2 outlets. The second outlet is created for dumping out state information using the Obex/Pattr system. More on that in future writings.

In the meantime, enjoy getting to know Xcode. Even if you love CodeWarrior, Xcode is going to be in your future as Intel-based Macintoshes are not likely to run your CW-compiled externals. Now is good time to get acquainted!

by Timothy PlaceLilli Wessling Hart on October 5, 2005

Creative Commons License
Timothy Place's icon

As the author of this article, I'd like to mention that it may be helpful in understanding what is under the hood in Xcode, but that it is now somewhat out-dated. At the time of writing Max was not yet ported to Mach-O executables. Max and all major externs were compiled with CodeWarrior and the Intel Mac did not exist.

If you are interested compiling Max externs, I recommend downloading the latest Max SDK and also searching the excellent forums.