Matt asked me a while back if I could look at what it would take to utilize libcouchbase from node.js. When I initially designed libcouchbase one of the requirements was that it should be fully asynchronous, so from a design perspective it should fit very well for node.js. Another design goal of libcouchbase was that it should be modular so that I could swap out the underlying methods that does all of the network communications. This API isn't something the average developer would see / use, but I added it to (hopefully) make it easier to port to new systems. I've been using libevent for a couple of years now, and I have to admit that I had that mindset when I designed that API. Unfortunately it doesn't map that well to lets say IOCP or libuv, so I'd like to refactor the API. In order to get full integration with node.js I have to do this refactoring (the current version use it's own event loop and block the global event loop). Mordy implemented a version that allows you to use libuv, but it's not merged into libcouchbase yet. I think I'd prefer a refactor of the current io model before merging the patch.
Anyway. When I started to look into the details I quickly realized that this was a completely new territory for me. I’ve never done anything with Javascript before, so I had no idea what kind of API the hard-core Javascript folks would like. Instead of waiting for someone to come up with an API specification for me, I started playing around trying to figure out how to create a Javascript binding to libcouchbase. I’m a strong believer of that an API should feel natural in the language it’s been used (instead of a “port” of the underlying API), but no matter what I would need to know how to integrate it. If I had a “working skeleton” I could always refactor the API into something the user would like at a later time.
It turns out that building a basic extension for node.js in C++ isn’t hard at all. The one I’ve built got doesn’t qualify as a real extension for node.js (given that it “blocks” the global event notification loop by using it’s own), but it works as a good “proof of concept”. I don’t think looking at the API is that interesting, but given that it was so easy to build the extension I figured I could walk you through it to give you a head start if you want to build your own.
You can find the entire source code I’m talking about at https://github.com/trondn/couchnode. The code examples you'll find here will probably not match entirely to the code you'll find there because I may have removed stuff here to make the example smaller and easier to read (so the stuff you'll find here might not compile ;)).
The first thing we need to do is to set up the boilerplate code:
#define BUILDING_NODE_EXTENSION
#include
class Couchbase: public node::ObjectWrap {
static void Init(v8::Handle target);
};
static void init(v8::Handle target) {
Couchbase::Init(target);
}
NODE_MODULE(couchbase, init)
The above fragment defines the class I'm going to use for the API (Couchbase), and the NODE_MODULE() macro registers the module name “couchbase” and tells node.js that it should call the function named init() to initialize the module. As you can see in init(), I’m calling the Init method in my Couchbase class to let the class initialize itself:
void Couchbase::Init(v8::Handle target)
{
v8::HandleScope scope;
v8::Local t = v8::FunctionTemplate::New(New);
v8::Persistent s_ct;
s_ct = v8::Persistent::New(t);
s_ct->InstanceTemplate()->SetInternalFieldCount(1);
s_ct->SetClassName(v8::String::NewSymbol("Couchbase"));
NODE_SET_PROTOTYPE_METHOD(s_ct, "get", Get);
target->Set(v8::String::NewSymbol("Couchbase"), s_ct->GetFunction());
}
So what does the above code do? It defines the JavaScript API to my class and maps the JavaScript functions to a C++ function, so that when someone writes couchbase.get("foo"); in JavaScript we're calling Couchbase::Get() in C++.
Now let’s look at the Get() method. When I “defined” the API I allowed for get to be called in multiple ways. In the C version of libcouchbase you typically set up a global callback that is called whenever you receive a response for a get call. Originally I made it the same way for JavaScript as well, but that clearly isn't the "node.js-way" of doing stuff. Instead you would write the following JavaScript:
cb.get(function onGet(state, key, value, flags, cas) {
if (state) {
console.log("found \"" + key + "\" - [" + value + "]");
} else {
console.log("failed for \"" + key + "\"");
}
}, "foo", "bar");
Given that I was doing all of this as part of my learning curve (I wouldn't be defining the real API now anyway), I decided to support both ways of calling the command. So let's take a look at the method:
v8::Handle Couchbase::Get(const v8::Arguments& args)
{
if (args.Length() == 0) {
const char *msg = "Illegal arguments";
return v8::ThrowException(v8::Exception::Error(v8::String::New(msg)));
}
v8::HandleScope scope;
Couchbase* me = ObjectWrap::Unwrap(args.This());
void* commandCookie = NULL;
int offset = 0;
if (args[0]->IsFunction()) {
if (args.Length() == 1) {
const char *msg = "Illegal arguments";
return v8::ThrowException(v8::Exception::Error(v8::String::New(msg)));
}
commandCookie = static_cast(new CommandCallbackCookie(args[0], args.Length() - 1));
offset = 1;
}
int tot = args.Length() - offset;
char* *keys = new char*[tot];
libcouchbase_size_t *lengths = new libcouchbase_size_t[tot];
// @todo handle allocation failures
for (int ii = offset; ii < args.Length(); ++ii) {
if (args[ii]->IsString()) {
v8::Local s = args[ii]->ToString();
keys[ii - offset] = new char[s->Length() + 1];
lengths[ii - offset] = s->WriteAscii(keys[ii - offset]);
} else {
// @todo handle NULL
// Clean up allocated memory!
const char *msg = "Illegal argument";
return v8::ThrowException(
v8::Exception::Error(v8::String::New(msg)));
}
}
me->lastError = libcouchbase_mget(me->instance, commandCookie, tot,
reinterpret_cast (keys), lengths, NULL);
if (me->lastError == LIBCOUCHBASE_SUCCESS) {
return v8::True();
} else {
return v8::False();
}
}
In order to build the plugin you need to create a file named wscript with the following content:
def set_options(opt):
opt.tool_options("compiler_cxx")
def configure(conf):
conf.check_tool("compiler_cxx")
conf.check_tool("node_addon")
def build(bld):
obj = bld.new_task_gen("cxx", "shlib", "node_addon")
obj.cxxflags = ["-g", "-Wall"]
obj.ldflags = ["-lcouchbase"]
obj.target = "couchbase"
obj.source = "src/couchbase.cc"
And set up the build environment by running:
$ node-waf configure
You can now build and "install" your pugin by running:
$ node-waf build install
You should now be able to test the plugin by creating a JavaScript file and run:
$ node myscript.js
Happy hacking
No comments:
Post a Comment