view mcabber/doc/HOWTO_modules.txt @ 1754:d8442bcb33b7

Reorder fields in module info struct
author Myhailo Danylenko <isbear@ukrpost.net>
date Sat, 13 Mar 2010 12:30:30 +0200
parents 4a7c7900f600
children 7befbd8e932d
line wrap: on
line source


===========================================

    Mcabber module writing brief howto

===========================================

To obtain information on module mcabber uses struct
module_info_t, that module should provide in public
variable with name info_<modulename>. If module name
contains any extra symbols except [a-z0-9_] they should
be replaced with '_'.

--------------------------------------------------------
  #include <mcabber/modules.h>

  typedef void (*module_init_t)(void);
  typedef void (*module_uninit_t)(void);

  typedef struct module_info_struct module_info_t;
  struct module_info_struct {
    const gchar      *branch;
    guint             api;
    const gchar      *version;
    const gchar      *description;
    const gchar     **requires;
    module_init_t     init;
    module_uninit_t   uninit;
    module_info_t    *next;
  };
--------------------------------------------------------

Callbacks init and uninit will be called after module
and it's dependencies loading. 'requires' can contain a
NULL-terminated list of module names, that should be loaded
before this. 'branch' and 'api' are required and should
contain mcabber branch and api version, that this module is
designed to work with. For these values see ChangeLog.api.
'version' and 'description' fields are optional and just
provide user with additional information about module.
'description' field can contain newlines. 'next' field can
contain pointer to the next struct with another branch of
mcabber, if your module can work with multiple branches.

To load modules, mcabber uses glib's GModule, thus, in your
module you can also use functions

--------------------------------------------------------
  #include <glib.h>
  #include <gmodule.h>

  const gchar* g_module_check_init (GModule *module);
  void         g_module_unload (GModule *module);
--------------------------------------------------------

to do something before any version/dependency checks will
be performed when module is loaded/unloaded. On success
g_module_check_init should return NULL, and error message
otherwise.

As module is loaded, you can use mcabber functions,
declared in mcabber's header files (though you should
consider, that they may change their calling conventions
some day).

I will not explain them all, there are too much of
them, but will provide description for those, provided
especially for module writers.

--------------------------------------------------------
  #include <mcabber/modules.h>

  const gchar *module_load (const gchar *name,
			    gboolean manual,
			    gboolean force);
  const gchar *module_unload (const gchar *name,
			      gboolean manual,
			      gboolean force);
--------------------------------------------------------

These functions load and unload modules respectively.
You can use them to handle optional dependencies. What
happens, when module is loaded:
 - check if module is present, and if present just
   increase it's reference count
 - load .so via glib (and call g_module_check_init, if
   present)
 - check for information structure presence
 - find suitable branch and check api version
   compatibility
 - load modules, that this module requires (note, that
   dependency problems will be reported as error
   invariably, force flag have no effect on this check)
 - module placed into a list of modules
 - module init routine is called
And when unloaded:
 - check if module is present
 - decrease reference count, if it is not zero, return
 - run module uninit routine
 - unload modules, that were loaded as dependencies for
   this
 - remove from modules list
They return error message or NULL in case of success.
'manual' flag indicates, that module will be loaded by
direct user request. It serves the purpose of tracking
user and automatic references (user can have only one).
'force' flag on module loading causes mcabber to ignore
most of the loading errors. On unload it forces
unloading even if reference count is not zero.

--------------------------------------------------------
  #include <mcabber/commands.h>

  void cmd_add (const char *name, const char *help,
                guint flags1, guint flags2,
		void (*f)(char*), gpointer userdata);
  void cmd_del (const char *name);
--------------------------------------------------------

These two functions are provided to declare mcabber
commands, offered by your module.
 - name is a command name.
 - help is a short description of your command, however
   for now it is not used at all and can be omitted.
 - flags are completion identifiers for first and second
   command arguments, for list of built-in completions,
   see compl.h. You can declare your own completion
   lists, using functions from compl.h, described later.
 - f is a user-provided callback function, that will be
   called upon executing mcabber command. If you will
   provide non-NULL userdata, function must be of type
   void (*f) (char *commandline, gpointer userdata).
 - userdata is a pointer to data, transparently passed
   to callback. See f description.

--------------------------------------------------------
  #include <mcabber/compl.h>

  guint compl_new_category (void);
  void  compl_del_category (guint id);

  void    compl_add_category_word (guint categ,
                                   const char *command); 
  void    compl_del_category_word (guint categ,
                                   const char *word); 
  GSList *compl_get_category_list (guint cat_flags,
                                   guint *dynlist); 
--------------------------------------------------------

These functions allow you to define and manage word
lists for completion categories, used by your commands.
First you need to obtain handle for completion type,
that you later will supply as flags, when declaring
your commands. For that use function compl_new_category.
It returns new category id or zero, if mcabber runs
out of completion ids (for now there are only 32 ids
available, and 20 of them are already taken by builtin
commands). compl_del_category allows you to delete
user-defined category, deleting all words in it too.

Now, that you have a completion category, you can at any
time add or delete words from it's completion list.
For that use functions compl_add_category_word and
compl_del_category_word. You can obtain current contents
of category by using gompl_get_category_list. If after
execution dynlist is TRUE, you should free obtained
list of words (both, words and list).

--------------------------------------------------------
  #include <mcabber/hooks.h>
 
  typedef struct { 
    const char *name; 
    const char *value; 
  } hk_arg_t; 
  
  typedef void (*hk_handler_t) (guint32 hookid,
                                hk_arg_t *args,
                                gpointer userdata); 
   
  void hk_add_handler (hk_handler_t handler,
                       guint32 flags,
                       gpointer userdata); 
  void hk_del_handler (hk_handler_t handler,
                       gpointer userdata); 
--------------------------------------------------------

These functions allow your module to react to events,
such as incoming and outgoing messages, buddy status
changes and sever connection establishment or breakup.
Flags field specifies mask of events, upon which this
handler should be called. Flags, that comprise this
mask can be found in hooks.h. You can specify not yet
used flags in mask, if you need to handle all events.
Handler can determine, which event is occured by
hookid argument and by a "hook" field in args, that
may provide more precise information in some cases.
Args argument is a list of hk_arg_t structures,
terminated by structure, whose name field is set to
NULL. Usually the "hook" field is in the first
structure of the list, however it is not guaranted,
that this will be so forever.

Currently there are next events possible:
 - hook-message-in (HOOK_MESSAGE_IN) with parameters
   * jid - sender of the incoming message
   * message - message body, converted to locale
     charset
   * groupchat ("true" or "false")
 - hook-message-out (HOOK_MESSAGE_OUT) with parameters
   * jid - recipient of the outgoing message
   * message - message body, converted to locale
     charset
 - hook-status-change (HOOK_STATUS_CHANGE) with
   parameters
   * jid - buddy, whose status has changed
   * resource - resource, whose status has changed
   * old_status - old status of the buddy, one-char
     string, representing mcabber status letter -
     one of 'ofdna?_'.
   * new_status - new buddy status. The same as
     old_status.
   * message - new status message. Old one should be
     still available to module as the current buddy's
     message.
 - hook-my-status-change (HOOK_MY_STATUS_CHANGE) with
   parameters
   * new_status - user's new status, see
     hook-status-change. Old one should still be
     available as the current status of the user.
   * message - new status message
 - hook-post-connect (HOOK_POST_CONNECT) with no parameters
 - hook-pre-disconnect (HOOK_PRE_DICSONNECT) with no
   parameters

--------------------------------------------------------
  #include <mcabber/xmpp_helper.h>

  void xmpp_add_feature (const char *xmlns);
  void xmpp_del_feature (const char *xmlns);
--------------------------------------------------------

These functions may be useful, if your module implements
some additional functionality to mcabber, that should be
advertised in a client's discovery features list.

=====================

   Example: hello

=====================

Now, let's write a simple module, called "hello", that
will do no more than just print something on loading
and unloading.

--------------------------------------------------------
#include <glib.h>
#include <gmodule.h>

/* We will use scr_LogPrint mcabber function,
   that does mcabber's messages output */
#include <mcabber/logprint.h>

/* Print something on module loading */
const gchar* g_module_check_init (GModule *module)
{
	scr_LogPrint (LPRINT_NORMAL, "Hello, World!");
	return NULL;
}

/* ... and unloading */
void g_module_unload (GModule *module)
{
	scr_LogPrint (LPRINT_NORMAL, "Bye, World!");
}

/* The End */
--------------------------------------------------------

Now, compile this file (hello.c) with

libtool --mode=compile gcc `pkg-config --cflags glib-2.0 \
  gmodule-2.0 mcabber` -c hello.c
libtool --mode=link gcc -module -rpath /usr/lib/mcabber/ \
  `pkg-config --libs glib-2.0 gmodule-2.0 mcabber` \
  -o libhello.la hello.lo

(you should substitute /usr/lib/mcabber to directory, where
your modules are located) and then install obtained module with

libtool --mode=install install libhello.la \
  /usr/lib/mcabber/libhello.la

Note, that you, most likely need not run suggested by libtool
finish action, as we're working with module object, not system-
wide library, but maybe some systems require that.

Now, set modules_dir mcabber variable to point to your modules
dir, and try to run /module -f load hello. If all goes well,
you should see in status buffer message "Hello World!" (as
well as some complaints, as we forced module loading).
Now unload module by running command /module unload hello,
that should bring up message "Bye, World!".

That's it, you just created very simple dynamically loadable
mcabber module. But, as you noticed, it needs force to be
loaded. Now, let's add information structure, that mcabber
wants.

==========================

   Example: info struct

==========================

--------------------------------------------------------
#include <mcabber/logprint.h>
/* module_info_t definition */
#include <mcabber/modules.h>

/* Print something on module loading */
void hello_init (void)
{
	scr_LogPrint (LPRINT_NORMAL, "Hello, World!");
}

/* ... and unloading */
void hello_uninit (void)
{
	scr_LogPrint (LPRINT_NORMAL, "Bye, World!");
}

module_info_t info_hello = {
	.branch          = "dev",
	.api             = 1,
	.requires        = NULL,
	.init            = hello_init,
	.uninit          = hello_uninit,
	.version         = "0.0.1",
	.description     = "Hello world module\n"
		"(as well as bye world module)",
	.next            = NULL,
};

/* The End */
--------------------------------------------------------

Here we now do not use glib nor gmodule, so, we can omit
them in compilation lines:

libtool --mode=compile gcc `pkg-config --cflags mcabber` \
  -c hello.c
libtool --mode=link gcc -module -rpath /usr/lib/mcabber/ \
  `pkg-config --libs mcabber` -o libhello.la hello.lo

Again compile it, copy, and try to load, now without -f
flag. As you may notice, when loading previous example,
mcabber first printed "Hello, World!", and only then
complaint about module not having information struct.
That's because g_module_check_init is called right
after module loading, before mcabber even have a chance
to look at module, while .init from info struct is
called afterwards by mcabber itself. You can try to
introduce some error (eg too high or missing target
mcabber version) and see the difference.

=======================

   Example: command

=======================

Now, let's allow our module to do some real work.

--------------------------------------------------------
#include <mcabber/logprint.h>
#include <mcabber/commands.h>
#include <mcabber/modules.h>

/* Handler for command */
void do_hello (char *args)
{
	/* args contains command line with command
         * name and any spaces after it stripped */
	scr_LogPrint (LPRINT_NORMAL, "Hello, %s!", 
                      *args != '\0' ? args : "World");
}

/* Register command */
void hello_init (void)
{
	cmd_add ("hello", "", 0, 0, do_hello, NULL);
}

/* Unregister command */
void hello_uninit (void)
{
	cmd_del ("hello");
}

module_info_t hello_info = {
	.branch          = "dev",
	.api             = 1,
	.requires        = NULL,
	.init            = hello_init,
	.uninit          = hello_uninit,
	.version         = "0.0.2",
	.description     = "Hello world module\n"
		"Provides command /hello",
	.next            = NULL,
}

/* The End */
--------------------------------------------------------

Now, compile it and try to load and run /hello with some
arguments.

Note, that we used one-argument version of command
handler, as we specified no userdata.

==========================

   Example: completion

==========================

Now let's investigate how to provide custom completion to
your commands. You can as well use built-in completions,
their IDs are listed in compl.h.

--------------------------------------------------------
#include <mcabber/logprint.h>
#include <mcabber/commands.h>
#include <mcabber/modules.h>
#include <mcabber/compl.h>

static guint hello_cid = 0;

/* hello command handler */
void do_hello (char *args)
{
	/* If argument is provided, add it to
	 * completions list. */
	if (hello_cid && *args != '\0')
		compl_add_category_word (hello_cid,
		                         args);
	scr_LogPrint (LPRINT_NORMAL, "Hello, %s!",
	              *args != '\0' ? args : "World");
}

/* Initialization */
void hello_init (void)
{
	/* Obtain handle for our completion
	 * category */
	hello_cid = compl_new_category ();
	if (hello_cid)
		/* Add known default word to
		 * completion list */
		compl_add_category_word (hello_cid,
		                         "World");
	cmd_add ("hello", "", hello_cid, 0, do_hello,
                 NULL);
}

/* Deinitialization */
void hello_uninit (void)
{
	/* Give back category handle */
	if (hello_cid)
		compl_del_category (hello_cid);
	cmd_del ("hello");
}

module_info_t hello_info = {
	.branch          = "dev",
	.api             = 1,
	.requires        = NULL,
	.init            = hello_init,
	.uninit          = hello_uninit,
	.version         = "0.0.3",
	.description     = "Hello world module"
		"Provides command /hello with completion",
	.next            = NULL,
}

/* The End */
--------------------------------------------------------

Now you can use completion for hello command. Note, that
this code have some serious simplifications, made for
simplicity reasons. For now, compl_add_category_word
does not checks, if word already exists in completions
list (although it is marked as TODO, so, some day it
will), so, we should check it ourselves.

=====================

   Example: hooks

=====================

Now let's implement our own beeper. Why anyone may wish
to do this? I am not satisfied with default mcabber's
builtin beeper flexibility. I wanted beeping on any
muc conference message, not just ones, directed to me.

--------------------------------------------------------
#include <string.h>

#include <mcabber/logprint.h>
#include <mcabber/commands.h>
#include <mcabber/compl.h>
#include <mcabber/hooks.h>
#include <mcabber/screen.h>
#include <mcabber/settings.h>
#include <mcabber/module.h>

static guint beep_cid = 0;

/* Event handler */
void beep_hh (guint32 hid, hk_arg_t *args, gpointer userdata)
{
	/* Check if beeping is enabled */
	if (settings_opt_get_int ("beep_enable"))
		/* *BEEP*! */
		scr_Beep ();
}

/* beep command handler */
void do_beep (char *args)
{
	/* Check arguments, and if recognized,
	 * set mcabber option accordingly */
	if (!strcmp (args, "enable") ||
	    !strcmp (args, "on") ||
	    !strcmp (args, "yes") ||
	    !strcmp (args, "1"))
		settings_set (SETTINGS_TYPE_OPTION,
		              "beep_enable", "1");
	else if (!strcmp (args, "disable") ||
		 !strcmp (args, "off") ||
		 !strcmp (args, "no") ||
		 !strcmp (args, "0"))
		settings_set (SETTINGS_TYPE_OPTION,
		              "beep_enable", "0");

	/* Output current state, either if state is
	 * changed and if argument is not recognized */
	if (settings_opt_get_int ("beep_enable"))
		scr_LogPrint (LPRINT_NORMAL,
		              "Beep on messages is enabled");
	else
		scr_LogPrint (LPRINT_NORMAL,
		              "Beep on messages is disabled");
}

/* Initialization */
void beep_init (void)
{
	/* Create completions */
	beep_cid = compl_new_category ();
	if (beep_cid) {
		compl_add_category_word (beep_cid, "enable");
		compl_add_category_word (beep_cid, "disable");
	}
	/* Add command */
	cmd_add ("beep", "", beep_cid, 0, do_beep, NULL);
	/* Add handler
	 * We are only interested in incoming message events
	 */
	hk_add_handler (beep_hh, HOOK_MESSAGE_IN, NULL);
}

/* Deinitialization */
void beep_uninit (void)
{
	/* Unregister event handler */
	hk_del_handler (beep_hh, NULL);
	/* Unregister command */
	cmd_del ("beep");
	/* Give back completion handle */
	if (beep_cid)
		compl_del_category (beep_cid);
}

module_info_t beep_info = {
	.branch          = "dev",
	.api             = 1,
	.requires        = NULL,
	.init            = beep_init,
	.uninit          = beep_uninit,
	.version         = "0.0.1",
	.description     = "Simple beeper module\n"
		"Recognizes option beep_enable\n"
		"Provides command /beep",
	.next            = NULL,
}

/* The End */
--------------------------------------------------------

If you use CMake (as do I), corresponding CMakeLists.txt
snippet:

--------------------------------------------------------
cmake_minimum_required(VERSION 2.6)
project(beep C)

find_package(PkgConfig REQUIRED)
pkg_check_modules(MCABBER REQUIRED mcabber)
# this one should be before any target definitions
link_directories(${MCABBER_LIBRARY_DIRS})

add_library(beep MODULE beep.c)

include_directories(SYSTEM ${MCABBER_INCLUDE_DIRS})
target_link_libraries(beep ${MCABBER_LIBRARIES)
include_directories(${beep_SOURCE_DIR}
                    ${beep_BINARY_DIR})

install(TARGETS beep DESTINATION lib/mcabber)
--------------------------------------------------------

===========================

   Example: dependencies

===========================

I will not provide here a complete example of two
modules, one of which depends on other, only some
use cases.

Info struct for module, that depends on two other
modules:

--------------------------------------------------------
#include <mcabber/modules.h>

const gchar *a_deps[] = { "b", "c", NULL };

module_info_t info_a = {
	.branch          = "dev",
	.api             = 1,
	.requires        = a_deps,
	.init            = a_init,
	.uninit          = a_uninit,
	.version         = NULL,
	.description     = NULL,
	.next            = NULL,
};
--------------------------------------------------------

If your module needs to "authenticate" mcabber version
too, this can be done in g_module_check_init:

--------------------------------------------------------
#include <glib.h>
#include <gmodule.h>

#include <mcabber/main.h>
#include <mcabber/modules.h>

const gchar *g_module_check_init (GModule *module)
{
	char *ver = mcabber_version ();
	// ver now contains version in format
	// X.X.X[-xxx][ (XXXXXXXXXXXXX)]
	const gchar *branch = mcabber_branch;
	guint api = mcabber_api_version;
	const gchar *error = NULL;

	if (...)
		error = "Incompatible mcabber version";

	g_free (ver);
	return error;
}
--------------------------------------------------------

Also you can use glib check_init routine to modify
module information, that will be checked by mcabber,
eg. if you want your module to always pass mcabber
version check, you can assign branch information,
obtained from mcabber_... variables to corresponding
fields in your struct.
Or you can modify your module's dependencies, though
direct module_load() will have the same effect, and
can be used for optional dependencies, that your module
can still work without.

Note: remember, that g_module_check_init will be always
called, even if later module will not pass checks, thus:
 - do not use functions from other modules there;
 - provide g_module_unload to undo anything, check_init
   has done.

==============

   Further

==============

As mcabber now uses glib mainloop, you can use glib's
event sources, for example, fifo reading already uses
GIOChannels for non-blocking IO.

You can extend xmpp part of mcabber functionality by
providing lm message handlers with high priority and
allowing unhandled by your handler messages be taken
care by mcabber's handlers on normal priority level.
This is where you may need to modify set of advertised
supported disco features.

Many useful examples can be found in my modules, that
can be found at http://isbear.unixzone.org.ua/source.

If you think, that your module needs to change
something, hardcoded in current implementation - feel
free to mail me or join mcabber's MUC room and
discuss this - for now I have only implemented things,
that I found necessary for written by me modules.

Also I am not native English speaker, so, if you find
some errors or non-natural constructs in this howto,
please, inform me (I will be glad, if you also provide
a more suitable version of text in question).

  -- Myhailo Danylenko
  -- mailto:isbear@ukrpost.net
  -- xmpp:isbear@unixzone.org.ua
  -- Sat, 13 Mar 2010 12:18:17 +0200