?

Log in

To The Side

RPC::Any - Simple, Effective XML-RPC and JSON-RPC for Perl

I just uploaded the first stable release of a new library to CPAN, called RPC::Any. It's a simple server framework that presents an object-oriented, unified interface for both XML-RPC and JSON-RPC. It has the following features:

  • You can use both JSON-RPC and XML-RPC with the same backend code. (That is, you only have to write your functions once--you never have to do anything special for each individual protocol in your code.)
  • It fully supports Unicode, proven with extensive Unicode tests in the test suite.
  • If it gets tainted input, it will send your functions tainted parameters.
  • It can take input from STDIN, from a string, or from an HTTP::Request.
  • Everything is written as a series of methods whose names and parameters will stay stable, so it's very easy to subclass for your own application, and make it behave completely differently.
  • It gives your functions the ability to call a type() method to explicitly type return values correctly for the protocol in use. (So you can do $class->type('int', $some_value) within your WebService methods and have that value properly returned as an int instead of a string.)
  • It doesn't try to start its own server process or send its own output, so you can control exactly how it is used and how the output is sent.
  • It supports operation in a CGI environment (in addition to almost any other environment you can come up with).
  • The test suite of RPC::Any has nearly 100% (about 98%) code coverage over the entire library.

More or less, it's everything that I ever wanted in an RPC library, and had hacked into Bugzilla using some very complex code (more on that below). It's also extremely simple to use, for most cases:

use RPC::Any::Server::JSONRPC::HTTP;
my $server = RPC::Any::Server::JSONRPC::HTTP->new(
  dispatch => { 'Foo' => 'My::Methods' });
print $server->handle_input();

package My::Methods;
use strict;

sub hello {
  my ($class) = @_;
  return $class->type('string', 'Hello World!');
}

The server above will read from STDIN, parse HTTP headers and JSON-RPC input, and return proper HTTP headers and JSON-RPC output (for any version of JSON-RPC, no less! 1.0, 1.1, or 2.0). Sending JSON-RPC input with Foo.hello as the method name would call My::Methods->hello() and return the string "Hello World!", explicitly typed as a string. You'll notice that My::Methods calls $class->type even though it never defines a "type" method--that's a magic method that RPC::Any gives the class immediately before it calls the method (and removes after the method is done being called).

To get XML-RPC support instead of JSON-RPC support, just replace every instance of the string "XMLRPC" with "JSONRPC" in the above code (so, in two places) and you have an XML-RPC server instead! No changes whatsoever required to My::Methods or any other code.

A Little Background

So, you might be wondering--why did I write a whole new WebServices module when there are so many options available on CPAN? Well, in fact, I'm actually using two of them as the backends for RPC::Any--RPC::XML and JSON::RPC::Common. So I'm not re-inventing the wheel for RPC--I'm just giving it all a clean, consistent interface.

Here's the whole story behind what motivated me to start writing RPC::Any:

Many years ago, for Bugzilla 3.0, we added a WebServices interface to Bugzilla. When we started off, it was pretty simple. We were using SOAP::Lite for XML-RPC, and everything seemed to be working okay.

Then, we realized that Unicode support was broken in SOAP::Lite (at the time), and so we had to write a workaround for that. Then we wanted to change how we dealt with null values, and we had to write a complex workaround for that. Then we changed how we dealt with input values--more complex, hackish workaround code. Oh, and then we realized that we wanted to preserve taint on incoming values, for security in Perl's taint mode (because otherwise we might accidentally send unchecked input to the database).

All of this was really tough, because SOAP::Lite was basically a SOAP module, with XML-RPC support tacked on. So all our workarounds involved modifying very convoluted SOAP code, when all we really wanted to do was modify how our XML-RPC support worked. Our XML-RPC Server module became quite a monster. (You might ask--okay, so why not switch to a pure XML-RPC module from CPAN? Ah, I tried--but more on that later.)

And then, we added JSON-RPC support, and we wanted to do it with the same backend code--that is, I didn't want to maintain one whole Bug.get function for JSON-RPC and another, separate Bug.get function for JSON-RPC. I just wanted to have one Bug.get function, with two separate frontends.

Well, this was far, far harder than I anticipated. The biggest problem was that SOAP::Lite called methods like Bugzilla::WebService::Bug->get(@params), but JSON::RPC called methods like $server->Bugzilla::WebService::Bug::get(@params);. (So under XML-RPC, the first argument to our methods was the name of our class, like "Bugzilla::WebService::Bug", but under JSON-RPC the first argument was an instance of JSON::RPC::Server.) So in order to be able to explicitly type return values using $self->type within our WebService code, I had to have one "type" implementation in Bugzilla::WebService::Server (which was the base class for both our XML-RPC and JSON-RPC servers) and another "type" implementation in Bugzilla::WebService (which is the base class for our WebService itself--that is, the base of things like Bugzilla::WebService::Bug and so on).

Then, there were numerous bugs in JSON::RPC--so many that I practically ended up writing my own JSON-RPC implementation on top of JSON::RPC, just to get it working properly for Bugzilla. Oh, and also, remember, I had to do all the same things I did for XML-RPC above, like get taint mode working right and testing/fixing Unicode. Oh, and then we added HTTP GET support to our JSON-RPC interface (which JSON::RPC didn't really support natively) and then we added JSONP support to it. It was crazy--just look at the old Bugzilla::WebSevice::Server::JSONRPC.

So along then comes the day when I have a bit of free time, and I'm looking for a project. I think, "Oh, I know, I've always wanted to move away from SOAP::Lite for our XML-RPC service. Let's see what else I can find on CPAN!" So I looked around at all of the available options, and I found RPC::XML, which, though not exactly what I needed, it was well-maintained, XML-RPC specific, and the maintainer had been really helpful with my requests.

So, I started to write a patch to move Bugzilla from SOAP::Lite to RPC::XML...and started to write a whole other series of workarounds--this time not for bugs (because the RPC::XML maintainer was very responsive about fixing those) but just because RPC::XML worked in a different way than we needed it to work, for Bugzilla.

Finally, at this point, I decided it was time to take the engineer attitude and solve the problem the right way--that is, not just fix the problem with our XML-RPC server, but fix all architectural problems of our WebServices by refactoring.

I looked around CPAN to see if there were any good, unified RPC frameworks that supported both the XML-RPC and JSON-RPC protocols, and didn't find anything. So, I rolled up my virtual sleeves and put the time in to do it really right--to write a module that would be the best, simplest, and most powerful RPC framework available for XML-RPC and JSON-RPC. One that fit the (very complex) needs of the Bugzilla Project, but could be used by anybody. A few weeks later, and I had RPC::Any.

It's really been a surprising amount of work, particularly in the test suite. But I think it was worth it--the new Bugzilla WebServices code is way cleaner, simpler, and actually much more powerful than it was before the RPC::Any.

-Max

Comments