Wednesday, September 17, 2008

HOWTO: Wrap a libpurple plugin for Adium

The core of the chat client Pidgin (formerly Gaim) and its command-line cousin Finch is a library called libpurple. This library handles all of the various chat protocols so that Pidgin, Finch, and other chat clients need merely to provide a GUI. It is also used by the Macintosh chat app Adium.

In this post, I'm going to discuss how to write, compile, and install a plugin for libpurple. Plugins in libpurple can extend or modify the behaviour of the library - even up to adding additional chat protocols, though in this post we'll work with a simpler example.

There's at least one good article on how to write a C plugin for libpurple that can be used with Pidgin or Finch. Installing plugins for pidgin or finch is easy - just drop the <plugin-name>.so file in ~/.purple/plugins.

Where there's a dearth of information is how to make a libpurple plugin that can be used with Adium, which is what this post will try to cover.

The Libpurple Plugin

First, let's list our libpurple plugin code. Compiled correctly, it could be used with Pidgin or Finch. Our goal, however, is to use it in Adium.

Let's create a plugin that mutes conversation - just converts all the instant messages we receive to all lowercase. There's too much shouting in chat anyway.

mute.c
 1  #define PURPLE_PLUGINS
 2
 3  #include <glib.h>
 4
 5  #include <plugin.h>
 6  #include <version.h>
 7
 8  #include <signals.h>      // purple_signal_connect()
 9  #include <account.h>      // PurpleAccount
10  #include <conversation.h> // purple_conversations_get_handle(),
11                            // PurpleConversation
12
13  #include <ctype.h>        // tolower()
14
15  static gboolean
16  receiving_im_msg_cb(
17      PurpleAccount *       account,
18      char **               sender,
19      char **               message,
20      PurpleConversation *  conversation,
21      PurpleMessageFlags *  flags,
22      void * thunk
23  ) {
24    char *p;
25
26    // convert the message to lowercase
27    for (p = *message; *p; p++) {
28      *p = tolower(*p);
29    }
30
31    return FALSE;
32  }
33
34  static gboolean
35  plugin_load(PurplePlugin *plugin) {
36
37      // hook all im's before they're displayed
38      purple_signal_connect(
39          purple_conversations_get_handle(),
40          "receiving-im-msg",
41          plugin,
42          PURPLE_CALLBACK( receiving_im_msg_cb ),
43          NULL /* our thunk */);
44
45      return TRUE;
46  }
47  static gboolean
48  plugin_unload(PurplePlugin *plugin) {
49
50      purple_signal_disconnect(
51          purple_conversations_get_handle(),
52          "receiving-im-msg",
53          plugin,
54          PURPLE_CALLBACK( receiving_im_msg_cb ));
55
56      return TRUE;
57  }
58
59  static PurplePluginInfo info = {
60      PURPLE_PLUGIN_MAGIC,
61      PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION,
62      PURPLE_PLUGIN_STANDARD,
63      NULL, 0, NULL,
64      PURPLE_PRIORITY_DEFAULT,
65
66      "core-mute",
67      "Mute",
68      "1.0",
69
70      "Mutes incoming IMs by converting uppercase to lowercase.",
71      "Mutes incoming IMs by converting uppercase to lowercase.",
72      "Noah Easterly <noah@mailinator.com>",
73      "http://rampion.blogspot.com",
74
75      plugin_load,                   // our startup hook
76      plugin_unload,                 // our shutdown hook
77      NULL,
78
79      NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
80  };
81
82  static void
83  init_plugin(PurplePlugin *plugin) { }
84
85  // magic macro that gets us run
86  PURPLE_INIT_PLUGIN(mute, init_plugin, info)

Normally, we would compile this for Pidgin just by

  • downloading a copy of the pidgin source
  • running ./configure
  • placing a copy of mute.c in libpurple/plugins/
  • running make mute.so in libpurple/plugins/

and install it by placing mute.so in ~/.purple/plugins/

Notice that this plugin hooks into all incoming IMs by using a signal. In libpurple (and Pidgin and Finch), messages are passed around by signals. Each signal has an emitter (which is just a unique id), an id (which is just a string), and zero or more receivers (again, just a unique id).

Since the signals documentation is still in progress, I'll take a moment to talk about them. Here's the complete list of signals in pidgin 2.4.2. If you're using a later version, you can find this list by just grepping through the source for purple_signal_register(), which is how signals are registered by their emitters.

Each call to purple_signal_register() specifies:

  • the emitter,
  • the signal id,
  • the type of the callback function,
  • the type of the return value of the callback function,
  • the number of arguments to pass to the callback function (other than the thunk, which I'll discuss in a second),
  • the types of the arguments to pass to the callback function.

Each callback gets one extra argument - a thunk. This thunk is just a (void *) pointer that was specified when you connected to the signal. It's how you can maintain state between calls without using globals.

For example, compare the signature of the "receiving-im-msg" signal:

purple_signal_register(handle, "receiving-im-msg",
           purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
           purple_value_new(PURPLE_TYPE_BOOLEAN), 5,
           purple_value_new(PURPLE_TYPE_SUBTYPE,
                  PURPLE_SUBTYPE_ACCOUNT),
           purple_value_new_outgoing(PURPLE_TYPE_STRING),
           purple_value_new_outgoing(PURPLE_TYPE_STRING),
           purple_value_new(PURPLE_TYPE_SUBTYPE,
                  PURPLE_SUBTYPE_CONVERSATION),
           purple_value_new_outgoing(PURPLE_TYPE_UINT));

with our callback function:

static gboolean
receiving_im_msg_cb(
    PurpleAccount *       account,
    char **               sender,
    char **               message,
    PurpleConversation *  conversation,
    PurpleMessageFlags *  flags,
    void * thunk
) { ... }

It's a little tricky to figure out how we got all those types from that signature. The easy way to do it is of course to cheat. Grep through the source, and look for purple_signal_emit() calls with the desired signal id - that will tell you exactly what's getting passed in (except for the thunk).

Wrapping it for Adium

There is some documentation on how to write Adium plugins, but it's certainly not as complete as it needs to be, or could be.

To create an Adium plugin, we're going to need a copy of the Adium source. Adium and Adium plugins are written in Objective C. If you're not familiar with Objective C, and you don't feel like learning it right now, be not afraid. It's really just a couple concepts tossed on top of regular C.

  • classes are declared in @interface ... @end blocks
  • and implemented in @implementation ... @end blocks
  • methods are called using [object method:arg1 with:arg2 and_with:arg3] notation (where the instance method is method:with:and_with:).
  • classes can have super classes and can implement protocols (abstract collections of declared but undefined methods).

So, we'll need to fire up XCode. First, we'll open the Adium project from our Adium source (in adium-1.2.5/Adium.xcodeproj), and build that (for Development). This will give us the necessary headers and such that our plugin will need. Then we'll create a new project (File→New Project...), in this case a Cocoa bundle:

cocoa-bundle

We'll go ahead and stick our new project in the same root directory as our adium source:

mute-directory

(So in this case our adium source is in ~/Projects/adium-1.2.5).

First, we'll create our wrapper code. We'll create a new cocoa class (File→New File):

cocoa-class

We'll be using this new class to invoke our existing C code, as a wrapper, so name it appropriately

mute-wrapper

This code is mostly skeletal. In the .h file we'll be declaring our MuteWrapper class - it's subclass of AIPlugin (Adium Plugin) that implements the AILibpurplePlugin protocol. What that does is provide hooks for Adium to run the plugin when libpurple is ready.

MuteWrapper.h
1  #import <Adium/AIPlugin.h>
2  #import <AdiumLibpurple/AILibpurplePlugin.h>
3
4  @interface MuteWrapper : AIPlugin <AILibpurplePlugin> {}
5  @end

In our .m file, we need to implement the necessary hooks expected: installLibpurplePlugin (which is called before libpurple is fully ready - so we can't register for signals yet) and loadLibpurplePlugin (which is called after libpurple is ready).

MuteWrapper.m
 1  #import "MuteWrapper.h"
 2
 3  @implementation MuteWrapper
 4  - (void) installLibpurplePlugin
 5  {
 6  }
 7  - (void) loadLibpurplePlugin
 8  {
 9      purple_init_mute_plugin();
10  }
11  @end

The function purple_init_mute_plugin() was defined in mute.c by the PURPLE_INIT_PLUGIN(mute, init_plugin, info) call at the end when PURPLE_STATIC_PRPL is defined at compile time.

The last piece of code we'll need is mute.c - which we already wrote, so we'll just import that into the project (Project→Add to Project):

import-file

Now, we need to add a bunch of Frameworks to our project, so we they're available to link with:

add-existing-frameworks

From adium-1.2.5/build/Developement/ add

  • Adium.framework
  • AdiumLibpurple.framework
  • AIUtilities.framework

and from adium-1.2.5/Frameworks/ add

  • FriBidi.framework
  • libglib.framework
  • libgmodule.framework
  • libgobject.framework
  • libgthread.framework
  • libintl.framework
  • libmeanwhile.framework
  • libpurple.framework

The last bit we'll need to do is a bit strange - in order to link correctly, we need to hack in a change to the linker paths. We can do this by adding a new script phase to our target:

add-script-phase

The script we need to add is fairly simple. Just copy and paste the following. However, be certain to update it if your version of Adium uses a version of libpurple besides 0.4.1, as this is hardcoded into the script

script.sh
#Make this change for every new version of libpurple in Adium source
/usr/bin/install_name_tool -change "@executable_path/../Frameworks/libpurple.framework/Versions/0.4.1/libpurple" "@executable_path/../Frameworks/libpurple.framework/libpurple" "$TARGET_BUILD_DIR/$TARGET_NAME.$WRAPPER_EXTENSION/Contents/MacOS/$TARGET_NAME"

Next, we'll need to change some project settings, so go to Project→Edit Active Target:

edit-active-target

First we'll need to set some information in the Properties tab. The important things to do here is to

  • set Creator to AdIM - not sure why yet, but if I figure out why, I'll let you know.
  • set Principal Class name as MuteWrapper, the class we be used to wrap our libpurple plugin.
target-info

Now we move over to the Build tab, and change more settings (beware trailing spaces):

  • add a new User-defined setting for ADIUM as ../adium-1.2.5
    - we'll be using this a bunch, so it's convenient.
  • set Architectures (ARCHS) as $(NATIVE_ARCH_32_BIT)
    - I get bugs otherwise, not sure why.
  • set SDK Path (SDKROOT) as $(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk
    - Adium is a universal binary, so we need to use the right sdk.
  • set Header Search Paths (HEADER_SEARCH_PATHS) as $(ADIUM)/Frameworks/libpurple.framework/Versions/0.4.1/Headers $(ADIUM)/Frameworks/libglib.framework/Headers/glib-2.0
    - most of our headers are in adium.
  • set Framework Search Paths (FRAMEWORK_SEARCH_PATHS) as $(SDKROOT)/System/Library/Frameworks $(ADIUM)/build/Development $(ADIUM)/Frameworks
    - same for our frameworks.
  • set Wrapper extension (WRAPPER_EXTENSION) as AdiumLibpurplePlugin
    - different than the normal AdiumPlugin extension, this loads us after libpurple is ready, and makes sure the AILibpurplePlugin hooks are called.
  • set Installation Directory (INSTALL_PATH) as "$(HOME)/Library/Application Support/Adium 2.0/PlugIns/"
    - this is where Adium plugins live.
  • set Other C Flags (OTHER_CFLAGS) as -DPURPLE_STATIC_PRPL
    - this makes mute.c generate purple_init_mute_plugin(), so we can call it from MuteWrapper.m.
build-settings

That's it. Now we just build, and double-click on our completed product to install the Mute.AdiumLibpurplePlugin in Adium.

3 comments:

Evan Schoenberg said...

Great article!

One comment: Rather than linking against all those frameworks, a less fragile solution is to use the following (assuming you put all the frameworks in Frameworks):

OTHER_LDFLAGS = -undefined dynamic_lookup
FRAMEWORK_SEARCH_PATHS = $(inherited) "$(SRCROOT)/Frameworks"
HEADER_SEARCH_PATHS = "$SRCROOT/Frameworks/libglib.framework/Headers"

(Set these in the appropriate places in the target's Build settings).

You can then remove the shell script which modifies the linker path, and all you actually need are the appropriate headers, not the framework binaries themselves.

Noah said...

@evan - Great tip, thanks a lot. I'll have to try it out.

Jeff B said...

Just curious, but how would I go about doing this with Xcode 4? Many of the steps have changed, and it's quite difficult to puzzle through. Thanks!