Building a firefox plugin – part one

March 1, 2009 104 Comments by Richard

Note: For a better way to create a Browser Plugin (that will work on all browsers, not just NPAPI), check out the FireBreath project.

Getting help: For a better place to ask NPAPI-related questions, go to StackOverflow and make sure to use the “npapi” and/or “firebreath” tags.

Introduction

I have now been researching and working on a cross-platform browser plugin for several months.  By far my greatest frustration throughout this process has been the significant lack of documentation on the subject.  So, with the creation of this site, I wish to create a small series that will hopefully provide assistance to other poor developers who are trying to break into this strangely secretive field =]

I’m not going to be able to cover everything in one post (for time reasons if nothing else).  How fast I get the rest of the posts done may depend in large part on whether or not anyone seems to be reading them and what requests are made in the comments =]

Plugin architecture

In these articles I plan to focus on the basic requirements for creating an NPAPI (Netscape Plugin) style plugin, which is used by most (if not all) plugin-supporting open browsers, including: Apple Internet Plugins on Mac (Firefox, Safari, probably others); Firefox (and all other gecko-based browsers), Opera, Safari, and Chrome on windows, and at least firefox on linux.  I haven’t yet implemented a plugin on linux, but most of the same principles should apply.

Because of the minor differences between platforms, I will first cover the architecture as it is used in Windows, and then in a later post I will cover the differences between windows and other platforms, such as Mac and Linux.

Plugin API vs Scripting API

When I first began developing browser plugins, I failed to understand the difference between the scripting API and the plugin API.  They are closely related, of course, but they serve different purposes.

The Scripting API is used to provide methods callable from javascript, while the Browser API provides the interface for hosting the plugin itself in the browser.

NPAPI

You can find the Mozilla documentation for plugins here:

https://developer.mozilla.org/en/Gecko_Plugin_API_Reference

For history of the NPAPI (Netscape Plugin API), see the Wikipedia page: http://en.wikipedia.org/wiki/NPAPI

What makes up an NPAPI browser plugin?

A NPAPI browser plugin is, at it’s core, simply a DLL with a few specific entry points.  Each of these entry points is only called once by the browser for all instances of your plugin on a given page.  They are listed here in the order they should be called:

  • NP_GetEntryPoints – Called immediately after the plugin is loaded and is used by the browser (no longer just netscape) to get pointers to all of the API functions that the browser might need to call.
  • NP_Initialize – Provides global initialization of your plugin.
  • NP_Shutdown – Provides global deinitialization of your plug-in.

It is important to note that since these entrypoints are specific to NPAPI, there is no reason you can’t have a NPAPI plugin inside a DLL that also provides other services (like, for example, an ActiveX plugin).

Since these three entrypoints provide the core of the NPAPI architecture, it is worth our time to look at them a little more closely.

NP_GetEntryPoints

NPError WINAPI NP_GetEntryPoints(NPPluginFuncs* pFuncs)

This is undoubtedly the most important of the three to understand.  Because of this, it shocks me that the only actual useful documentation I have found for this method so far is found in an old, but still mostly accurate web-book written who-knows-when by Zan Oliphant.

While not as straightforward as the other two entrypoint functions, NP_GetEntryPoints is nonetheless relatively straightforward.  As you can see from the above function prototype, NP_GetEntryPoints takes a pointer to the NPPluginFuncs structure:

typedef struct _NPPluginFuncs {
    uint16 size;
    uint16 version;
    NPP_NewUPP newp;
    NPP_DestroyUPP destroy;
    NPP_SetWindowUPP setwindow;
    NPP_NewStreamUPP newstream;
    NPP_DestroyStreamUPP destroystream;
    NPP_StreamAsFileUPP asfile;
    NPP_WriteReadyUPP writeready;
    NPP_WriteUPP write;
    NPP_PrintUPP print;
    NPP_HandleEventUPP event;
    NPP_URLNotifyUPP urlnotify;
    JRIGlobalRef javaClass;
    NPP_GetValueUPP getvalue;
    NPP_SetValueUPP setvalue;
} NPPluginFuncs;

UPP in each of these type names stands for “Universal Proc Pointer”, which is essentially just a function pointer that the Gecko SDK uses in conjunction with a CallUniversalProc macro for all of its function pointer needs.  For more information, grep the Gecko SDK.

These function pointers basically tell the browser how to interact with your plugin.  Notice the naming convention here: NPP_* for all plugin functions.  There are also NPN_* functions that we will see later, and these are functions that the plugin can call on the browser.  They will be given to us in the NP_Initialize call.

So, the primary purpose of the NP_GetEntryPoints function is to give the browser pointers to all of the functions that it needs to call when creating or interacting with your plugin.

Here is a quick overview of the NPP plugin functions that your plugin must provide (and give addresses to when NP_GetEntryPoints is called).  This table is copied from Zan Oliphant’s book and updated to reflect the current practices and Gecko SDK 1.8:

API Name Description
NPP_New Creates a new instance of a plug-in.
NPP_Destroy Deletes an instance of a plug-in.
NPP_SetWindow Tells the plug-in when a window is created, moved, sized, or destroyed.
NPP_NewStream Notifies a plug-in instance of a new data stream.
NPP_DestroyStream Tells the plug-in that a stream is about to be closed or destroyed.
NPP_StreamAsFile Provides a local file name for the data from a stream.
NPP_WriteReady Determines whether a plug-in is ready for data (and the maximum number of bytes it is prepared to accept).
NPP_Write Called to write/deliver data to a plug-in.  The docs note that this might be better named “NPP_DataArrived”.
NPP_Print Requests a platform-specific print operation for an embedded or full-screen plug-in.
NPP_HandleEvent Event handler, currently only used by Windowed plugins on Mac OS; windowless plugins on all platforms use this.
NPP_URLNotify Notifies the completion of a URL request.
NPP_GetJavaClass Deprecated / No longer used. Set to NULL
NPP_GetValue Called to query the plugin for information (also used to get an instance of a NPObject/Scriptable Plugin)
NPP_SetValue This call is used to inform plugins of variable information controlled by the browser.

We will talk more about the specifics of how each of these functions works later.

NP_Initialize

NPError WINAPI NP_Initialize(NPNetscapeFuncs *aNPNFuncs) // Windows
// -or-
NPError NP_Initialize(NPNetscapeFuncs *aNPNFuncs, NPPluginFuncs *aNPPFuncs) // Linux

As noted in the documentation, NP_Initialize provides global initialization for a plug-in.  The API reference is a little confusing in that it claims that NP_Initialize is actually the first function called by the browser.  The reason for this is that on linux, there aparently is no NP_GetEntryPoints call; instead, the NPPluginFuncs struct is passed into the NP_Initialize function to be filled out.

Since this isn’t confusing enough, on Mac they have replaced both of these functions with a single “main” function that not only gets passed both function pointer structures (one with the browser functions and one to be filled out with plugin functions) but also is given a shutdown function pointer to be filled out with the address to the NP_Shutdown function.

Because of these discrepancies, I recommend that you write a separate function for filling out the NPPluginFuncs structure so that it can be called from any of the various init functions.

The NPNetscapeFuncs structure that is passed in is the complement to the NPPluginFuncs structure that we have discussed previously.  As might be guessed from the name, the NPNetscapeFuncs structure contains pointers to browser functions that can be called by the plugin.  There are a lot more of these, and we will discuss more about how to use them next time.

For now, take a look at the structure to get a general idea of what is there.  I’ve added comments on some of the more common function calls.

typedef struct _NPNetscapeFuncs {
    uint16 size;
    uint16 version; // Newer versions may have additional fields added to the end
    NPN_GetURLUPP geturl; // Make a GET request for a URL either to the window or another stream
    NPN_PostURLUPP posturl; // Make a POST request for a URL either to the window or another stream
    NPN_RequestReadUPP requestread;
    NPN_NewStreamUPP newstream;
    NPN_WriteUPP write;
    NPN_DestroyStreamUPP destroystream;
    NPN_StatusUPP status;
    NPN_UserAgentUPP uagent;
    NPN_MemAllocUPP memalloc; // Allocates memory from the browser's memory space
    NPN_MemFreeUPP memfree; // Frees memory from the browser's memory space
    NPN_MemFlushUPP memflush;
    NPN_ReloadPluginsUPP reloadplugins;
    NPN_GetJavaEnvUPP getJavaEnv;
    NPN_GetJavaPeerUPP getJavaPeer;
    NPN_GetURLNotifyUPP geturlnotify; // Async call to get a URL
    NPN_PostURLNotifyUPP posturlnotify; // Async call to post a URL
    NPN_GetValueUPP getvalue; // Get information from the browser
    NPN_SetValueUPP setvalue; // Set information about the plugin that the browser controls
    NPN_InvalidateRectUPP invalidaterect;
    NPN_InvalidateRegionUPP invalidateregion;
    NPN_ForceRedrawUPP forceredraw;
    NPN_GetStringIdentifierUPP getstringidentifier; // Get a NPIdentifier for a given string
    NPN_GetStringIdentifiersUPP getstringidentifiers;
    NPN_GetIntIdentifierUPP getintidentifier;
    NPN_IdentifierIsStringUPP identifierisstring;
    NPN_UTF8FromIdentifierUPP utf8fromidentifier; // Get a string from a NPIdentifier
    NPN_IntFromIdentifierUPP intfromidentifier;
    NPN_CreateObjectUPP createobject; // Create an instance of a NPObject
    NPN_RetainObjectUPP retainobject; // Increment the reference count of a NPObject
    NPN_ReleaseObjectUPP releaseobject; // Decrement the reference count of a NPObject
    NPN_InvokeUPP invoke; // Invoke a method on a NPObject
    NPN_InvokeDefaultUPP invokeDefault; // Invoke the default method on a NPObject
    NPN_EvaluateUPP evaluate; // Evaluate javascript in the scope of a NPObject
    NPN_GetPropertyUPP getproperty; // Get a property on a NPObject
    NPN_SetPropertyUPP setproperty; // Set a property on a NPObject
    NPN_RemovePropertyUPP removeproperty; // Remove a property from a NPObject
    NPN_HasPropertyUPP hasproperty; // Returns true if the given NPObject has the given property
    NPN_HasMethodUPP hasmethod; // Returns true if the given NPObject has the given Method
    NPN_ReleaseVariantValueUPP releasevariantvalue; // Release a MNVariant (free memory)
    NPN_SetExceptionUPP setexception;
    NPN_PushPopupsEnabledStateUPP pushpopupsenabledstate;
    NPN_PopPopupsEnabledStateUPP poppopupsenabledstate;
} NPNetscapeFuncs;

In addition to saving the function pointers given so that browser calls can be made, any memory that is to be shared by all instances of your browser plugin should be initialized here.

NP_Shutdown

This is the simplest of the three entrypoints.  Free any shared memory and release any shared resources.  This is called when the browser has already destroyed all instances of your plugin (by calling NPP_Destroy) and does not expect to create any more in the near future.

Next Time

Next time we will go into greater detail on implementing the NPP functions and also cover some of the most commonly used NPN functions.

Building a firefox plugin – part one

Building a firefox plugin – part two

Building a firefox plugin – part three

Building a firefox plugin – part four

Getting more help

Update May 14, 2011: Many people have been asking questions in the comments; while I don’t mind that, it would probably be more useful if you ask your question on the FireBreath forums. There is a forum there for those just using NPAPI as well!

46 Comments

  1. cygnl7
    9 years ago

    Good stuff! Thanks for pulling this information together.

    More detail on what the differences are (especially in the initialization) on the three OSes would be useful. For example, what do you mean by a “main” function for MacOS?

  2. cygnl7
    9 years ago

    Good stuff! Thanks for pulling this information together.

    More detail on what the differences are (especially in the initialization) on the three OSes would be useful. For example, what do you mean by a “main” function for MacOS?

  3. taxilian
    9 years ago

    I think part 4 or 5 will probably have more of that detail; I haven’t actually finished the Mac and Linux ports of my implementation (though I know in theory what needs to be done), so I want to wait until I’ve completely worked through all the cross-platform issues before I blog about them. =]

  4. Richard
    9 years ago

    I think part 4 or 5 will probably have more of that detail; I haven’t actually finished the Mac and Linux ports of my implementation (though I know in theory what needs to be done), so I want to wait until I’ve completely worked through all the cross-platform issues before I blog about them. =]

  5. Juri
    9 years ago

    Great post, by far the most adequate on this topic. Thanks.

  6. Juri
    9 years ago

    Great post, by far the most adequate on this topic. Thanks.

  7. Marius
    9 years ago

    Great post. Most relevant info I’ve found so far on the subject!
    Thanks a bunch!

  8. Marius
    9 years ago

    Great post. Most relevant info I’ve found so far on the subject!
    Thanks a bunch!

  9. sara
    9 years ago

    hi, Your work on plugin is great. I am very new to plugin. I am sorry for any stupid question. i am confused that how plugin interact with application. i am developing a plugin for minisip application. how plugin will interact with that application…sorry if i am asking question at wrong place…but i need help…please

  10. sara
    9 years ago

    hi, Your work on plugin is great. I am very new to plugin. I am sorry for any stupid question. i am confused that how plugin interact with application. i am developing a plugin for minisip application. how plugin will interact with that application…sorry if i am asking question at wrong place…but i need help…please

  11. taxilian
    9 years ago

    I'm not sure if I understand what you mean by “how does the plugin interact with the application”. It depends on what you are trying to do;

    Think of a plugin as an application that lives in a browser and interacts with the web page as a DOM element. Before developing a plugin, you should be familiar already with javascript and DHTML.

    Beyond that, the plugin will essentially live in a window most of the time (I haven't actually made a windowless plugin yet, so I can't speak to the details of that version), and you can communicate with it using javascript calls (see the “part three” post that is linked to at the bottom of this article)

  12. taxilian
    9 years ago

    I'm not sure if I understand what you mean by “how does the plugin interact with the application”. It depends on what you are trying to do;

    Think of a plugin as an application that lives in a browser and interacts with the web page as a DOM element. Before developing a plugin, you should be familiar already with javascript and DHTML.

    Beyond that, the plugin will essentially live in a window most of the time (I haven't actually made a windowless plugin yet, so I can't speak to the details of that version), and you can communicate with it using javascript calls (see the “part three” post that is linked to at the bottom of this article)

  13. sara
    9 years ago

    thanks a lot…I try to explain to you. I have minisip.exe file from minisip.org website. Now I want to make plugin for minisip. Do I need to bring all functions of minisip application in plugin? I mean is minisip.exe is used separately by plugin or the whole minisip is embedded inside plugin functions. I am sorry if I am confusing you.
    regards

    sara

  14. taxilian
    9 years ago

    minisip.exe would not work just by itself; it probably is possible for a plugin to interact with other processes, though you could run into some problems if you wanted to make an IE compatible ActiveX control that way, and it would require modifying both sides to be aware of the communication.

    Much better would be to either build a minisip library (static or DLL) that you could link to and then write a plugin wrapper around it. The plugin itself is the only process that would be running, and your minisip library could run inside it. Be a little careful what APIs you expose to the web page so that it doesn't become a security risk.

  15. sara
    9 years ago

    thanks a lot…I try to explain to you. I have minisip.exe file from minisip.org website. Now I want to make plugin for minisip. Do I need to bring all functions of minisip application in plugin? I mean is minisip.exe is used separately by plugin or the whole minisip is embedded inside plugin functions. I am sorry if I am confusing you.
    regards

    sara

  16. taxilian
    9 years ago

    minisip.exe would not work just by itself; it probably is possible for a plugin to interact with other processes, though you could run into some problems if you wanted to make an IE compatible ActiveX control that way, and it would require modifying both sides to be aware of the communication.

    Much better would be to either build a minisip library (static or DLL) that you could link to and then write a plugin wrapper around it. The plugin itself is the only process that would be running, and your minisip library could run inside it. Be a little careful what APIs you expose to the web page so that it doesn't become a security risk.

  17. sara
    9 years ago

    thanks a lot

  18. sara
    9 years ago

    thanks a lot

  19. sara
    9 years ago

    hi

    I am again writing you. I am running npruntime sample in ubuntu. I call a function in plugin.cpp. The function is declared in external library. While comipling plugin sample I want to link that external library so that function can be called. I am having linking problems.Please help me

  20. sara
    9 years ago

    hi

    I am again writing you. I am running npruntime sample in ubuntu. I call a function in plugin.cpp. The function is declared in external library. While comipling plugin sample I want to link that external library so that function can be called. I am having linking problems.Please help me

  21. taxilian
    9 years ago

    It sounds like your problem has nothing directly to do with npapi or npruntime, but rather that you simply aren't linking your project correctly.

    When you compile, make sure you specify the library that you need to link against; it won't find it automatically, you have to specifiy the .so or .a file that contains showtime()

  22. taxilian
    9 years ago

    It sounds like your problem has nothing directly to do with npapi or npruntime, but rather that you simply aren't linking your project correctly.

    When you compile, make sure you specify the library that you need to link against; it won't find it automatically, you have to specifiy the .so or .a file that contains showtime()

  23. lucka
    9 years ago

    Hi, I really liked your tutorial and thought you might be able to help. I have an NPAPI plugin that works in firefox, but does not work in google chrome – more specifically calling plugin.myPluginMethod() from javascript fails because plugin.myPluginMethod is undefined.
    Do you have any idea what might have gone wrong?

  24. taxilian
    9 years ago

    Without additional information about what you're trying to do and what technologies you are using, I would guess that you're still trying to use XPCOM and an .idl file to define your interface. This won't work in Chrome, and reportedly it won't work in Firefox starting with 3.6

  25. lucka
    9 years ago

    Hi, I really liked your tutorial and thought you might be able to help. I have an NPAPI plugin that works in firefox, but does not work in google chrome – more specifically calling plugin.myPluginMethod() from javascript fails because plugin.myPluginMethod is undefined.
    Do you have any idea what might have gone wrong?

  26. taxilian
    9 years ago

    Without additional information about what you're trying to do and what technologies you are using, I would guess that you're still trying to use XPCOM and an .idl file to define your interface. This won't work in Chrome, and reportedly it won't work in Firefox starting with 3.6

  27. Guybrush
    9 years ago

    I am trying to learn how to build Firefox plugins and want to share my experience to run the sample npruntime plugin

    Follow the guide at: https://developer.mozilla.org/en/Compiling_The_

    NB: As for Gecko SDK use Gecko 1.9 (Firefox 3.0) as the Gecko 1.9.1 (Firefox 3.5) gave several errors which I couldnt Solve

    When you compile several Depreciated warnings will show up replace them with the suggestions that the VS will give you but some memcpy wont be replaceable by memcpy_s cause of number of parameters so I included _CRT_SECURE_NO_WARNINGS with the preprocessor

    also rename #include “afxres.h” to #include “windows.h” in nprt.rc as it will not be found and should work

  28. Guybrush
    9 years ago

    I am trying to learn how to build Firefox plugins and want to share my experience to run the sample npruntime plugin

    Follow the guide at: https://developer.mozilla.org/en/Compiling_The_

    NB: As for Gecko SDK use Gecko 1.9 (Firefox 3.0) as the Gecko 1.9.1 (Firefox 3.5) gave several errors which I couldnt Solve

    When you compile several Depreciated warnings will show up replace them with the suggestions that the VS will give you but some memcpy wont be replaceable by memcpy_s cause of number of parameters so I included _CRT_SECURE_NO_WARNINGS with the preprocessor

    also rename #include “afxres.h” to #include “windows.h” in nprt.rc as it will not be found and should work

  29. Guybrush
    9 years ago

    Hi,

    I am not sure about my previous post how to setup the development enviroment. Disabling warnings isn't a great idea because it might produce unexpected behaviors and errors. So I kindly ask for a description to setup VS2008 with Gecko 1.9.1 (Firefox 3.5) and maybe a “hello world” plugin in complaince to the latest API.

    I can't understand how updates and changes are done to the APIs and descriptions/samples stay put this will not help new developers start experimenting with it.

    I tried to follow this but doesn't work with latest Gecko.
    https://developer.mozilla.org/en/Compiling_The_

    Regards

  30. Guybrush
    9 years ago

    Hi,

    I am not sure about my previous post how to setup the development enviroment. Disabling warnings isn't a great idea because it might produce unexpected behaviors and errors. So I kindly ask for a description to setup VS2008 with Gecko 1.9.1 (Firefox 3.5) and maybe a “hello world” plugin in complaince to the latest API.

    I can't understand how updates and changes are done to the APIs and descriptions/samples stay put this will not help new developers start experimenting with it.

    I tried to follow this but doesn't work with latest Gecko.
    https://developer.mozilla.org/en/Compiling_The_

    Regards

  31. taxilian
    9 years ago

    Try pulling down the Firebreath source and building it; that has a basic functional NPRuntime scripting plugin up and working (though it's not well documented yet).

    http://firebreath.googlecode.com

  32. taxilian
    9 years ago

    Try pulling down the Firebreath source and building it; that has a basic functional NPRuntime scripting plugin up and working (though it's not well documented yet).

    http://firebreath.googlecode.com

  33. pineapple_monkey
    9 years ago

    I'm using NPRUNTIME for my plugin and it works perfectly fine in Firefox. But I'm experiencing the same problem with Chrome as lucka.
    “Uncaught TypeError: Object #<an HTMLEmbedElement> has no method 'SmartCardListReaders'”. I´ll return with more info after debugging :)

  34. pineapple_monkey
    9 years ago

    I'm using NPRUNTIME for my plugin and it works perfectly fine in Firefox. But I'm experiencing the same problem with Chrome as lucka.
    “Uncaught TypeError: Object #<an HTMLEmbedElement> has no method 'SmartCardListReaders'”. I´ll return with more info after debugging :)

  35. black spider
    9 years ago

    Pls. can u help me. I want to know how to add a simple function in plugin like for example a function the has a param int and will just return its value like:
    int retval(int i)
    {
    return i;
    }

    i want to know how this retval function in plugin can be called in javascript. I would really appreciate help.

  36. taxilian
    9 years ago

    You need to create a NPObject and implement Invoke for a method, GetProperty and SetProperty for a property.

    There are details on how all that works in part three: http://colonelpanic.net/2009/08/building-a-fire

  37. black spider
    9 years ago

    i had used the npruntime. is it ok? sorry i'm new to this plugin so i'm kind of mind bubbling but a little i understand the code. but i hardly dont know how to add this even simple like function.

  38. taxilian
    9 years ago

    Start with the npruntime sample from the mozilla source code. Create an NPObject. Implement the NPP_Invoke method. It'll take some research on your own.

    Check out the firebreath project to see some examples, but it's a little more complicated than your question requires. By the end of the year, there should be a release candidate of Firebreath for windows only.

  39. black spider
    9 years ago

    ok. thanks for your help.

  40. black spider
    9 years ago

    Pls. can u help me. I want to know how to add a simple function in plugin like for example a function the has a param int and will just return its value like:
    int retval(int i)
    {
    return i;
    }

    i want to know how this retval function in plugin can be called in javascript. I would really appreciate help.

  41. black spider
    9 years ago

    Pls. can u help me. I want to know how to add a simple function in plugin like for example a function the has a param int and will just return its value like:
    int retval(int i)
    {
    return i;
    }

    i want to know how this retval function in plugin can be called in javascript. I would really appreciate help.

  42. taxilian
    9 years ago

    You need to create a NPObject and implement Invoke for a method, GetProperty and SetProperty for a property.

    There are details on how all that works in part three: http://colonelpanic.net/2009/08/building-a-fire

  43. taxilian
    9 years ago

    You need to create a NPObject and implement Invoke for a method, GetProperty and SetProperty for a property.

    There are details on how all that works in part three: http://colonelpanic.net/2009/08/building-a-fire

  44. black spider
    9 years ago

    i had used the npruntime. is it ok? sorry i'm new to this plugin so i'm kind of mind bubbling but a little i understand the code. but i hardly dont know how to add this even simple like function.

  45. black spider
    9 years ago

    i had used the npruntime. is it ok? sorry i'm new to this plugin so i'm kind of mind bubbling but a little i understand the code. but i hardly dont know how to add this even simple like function.

  46. taxilian
    9 years ago

    Start with the npruntime sample from the mozilla source code. Create an NPObject. Implement the NPP_Invoke method. It'll take some research on your own.

    Check out the firebreath project to see some examples, but it's a little more complicated than your question requires. By the end of the year, there should be a release candidate of Firebreath for windows only.

4 Trackbacks

  1. […] Last time, I talked about the fundamentals of implementing a NPAPI plugin.  Today, I’m going to go into a little more detail on how to use the strange callback architecture that firefox exposes. […]

  2. By pre dev wiki: Browser Plugins on July 1, 2009 at 11:48 pm

    […] Building a FireFox plugin: Part One Building a FireFox plugin: Part Two […]

  3. […] you are new to NPAPI plugins, you may want to go back and read part one and part two of this series, which cover the basic architecture and lifecycle of an NPAPI […]

  4. By Building a firefox plugin « HeejinDev on September 8, 2009 at 6:41 pm

    […] Building a firefox plugin – part one Building a firefox plugin – part two Building a firefox plugin – part three […]

Post a Comment

Your email is never published or shared. Required fields are marked *