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
  2. Copy the c74support folder into the /Library/Application Support folder.
  3. 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.
  4. 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.
  5. You should now see Xcode’s main interface window.
  6. 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.
  7. 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

    .
  8. 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)
  9. 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.
  10. Now switch to the Properties tab in the Active Target editor. Change the type to iLaX.

    Then we can close the Active Target editor.
  11. 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.

  12. 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);
    }
  13. Now we can try to compile it again, but we get an error.

    This error is caused because in our source we included Max 4.5′s helper utilities for working with symbols. We included the header file (commonsyms.h) but we also need to add the actual C code to our project. Locate the file commonsyms.c and drag it into the source folder in your project.

    It present a sheet with some options. We can just click the Add button.

    This results in the file being present in the source folder, where Xcode can access it.
  14. Try compiling again now – click the hammer. If you’ve followed all of the directions, you should see that it is a success!
  15. Before we pat ourselves on the back too hard though, we better test the object in Max… Back in the Finder, we need to locate our built external. It should be in the build directory of our project folder, as shown in the following screenshot.

    We can either add this folder to our Max searchpath, or copy the external into Max’s searchpath. Just make sure that Max can find it.
  16. 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!

Writing Externals with Xcode 2.2

Dec 19, 2009 at 1:44am

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.

#261418

You must be logged in to reply to this topic.