January 19, 2010

Using the Perforce C++ API on Windows

Integration

I recently had the urge to write a Perforce plug-in for the popular free source code editor Notepad++, the result of which you can find in the public depot here: NppPerforcePlugin.

Not wanting to wrap a call to "p4.exe", I decided to use the Perforce C++ API. This turned out to be surprisingly simple, so I want to share what I learned from using the API to encourage you, my esteemed readers, to write your own plug-ins and tools.

Preparations

First, you will need the P4 API, which you can find on the Perforce ftp server. For Windows, there are several choices, depending on which compiler you want to use, whether you want to link in the libraries statically or dynamically, and if you are creating a debug build. Download your choice and unpack it. You will find three directories: include, lib and sample; the latter contains a simple example on how you can use the API.

Using Visual Studio 2008, I pointed the include and library paths to the Perforce API directories and added the symbol "OS_NT" to the defined pre-compiler symbols; this symbol is needed if you want to use the Enviro class, otherwise your application will crash the first time you instantiate an Enviro object (more on that later).

The interface to Perforce in my plugin is encapsulated in a class called PerforceClient. Its main attribute is a ClientApi instance, the central object for the communication with Perforce; here it is simply named "client". To run any commands on a Perforce server, you need to set the environment and then call ClientApi::Init( Error * ):

ClientApi client;
client.SetPort( "perforce:1666" );
client.SetUser( "sven" );

Error e;
client.Init( &e );

Note that the Perforce API does not use exceptions; instead error states are set in an error object, which you need to instantiate yourself.

If you look through the code of the plug-in you will notice that I do not set the port or any other parameters besides the current working directory (with ClientApi.SetCwd). ClientApi::Init() will search the environment for P4PORT, P4USER and P4CLIENT following the usual rules you know for the command line client. This includes P4CONFIG, which is why I set the current working directory in my plug-in.

To actually run a command on the Perforce server and to process the output, we need to create a subclass of ClientUser. This class defines a set of callbacks for the actual output. An example will make this easier to understand:

#include <p4/clientapi.h>
#include <iostream>

class OutputClient : public ClientUser
{
public:
  virtual void OutputInfo( char level, const char * data ) {
    std::cout << data << std::endl;
  }
};

void main()
{
  ClientApi client;
  Error e;

  client.Init( &e );

  OutputClient ui;
  client.Run( "info", &ui );

  client.Final( &e );
}

Compile and link this simple application with the following libraries:

libclient.lib librpc.lib libsupp.lib wsock32.lib ws2_32.lib oldnames.lib

Now you have a little application that shows the connection information. Not very useful I admit, but expandable.

The ClientUser class has more virtual methods that can be overwritten if necessary. In my plug-in I also overwrote

void ClientUser::OutputError( const char *errBuf);
void ClientUser::OutputStat( StrDict *varList);

The former is useful to catch error messages and present them in a useful form. The latter is used for tagged output, in my case for presenting the output of "p4 fstat" in a useful way:

virtual void OutputStat( StrDict *varList ) {
  StrRef var, val;

  for( int i = 0; varList->GetVar( i, var, val ); i++ ) {
   if( var == "func" || var == P4Tag::v_specFormatted ) continue;

   std::cout << var << " ... " << val << std::endl;
  }
}

StrDict is a Perforce class that represent key-value pairs, in other words, a dictionary.

The only thing missing to create a fully fledged application is the ability to add options and file names to the command. This is accomplished with ClientApi::SetArgv(int argc, char *argv[]):

int argc = 2;
char * argv[] = { "-l", "//depot/myproject/myfile.cpp" };

client.setArgv( argc, argv );

OutputClient ui;
client.Run( "fstat", &ui );

Now, about the Enviro object I mentioned above. The Enviro class is a clever bit of code that lets you investigate the environment from the Perforce point of view. This includes, on Windows, the registry, which is the reason you need to define the symbol "OS_NT" when compiling code that uses the Enviro class. In the plug-in I use the Enviro class to detect the existence of the environment variable P4CHARSET. This is not automatically picked up by the ClientApi class. If you want to talk to an Unicode enabled Perforce server and use the P4CHARSET defined in the environment, you need a bit of code like this:

#include <p4/i18napi.h>
#include <p4/enviro.h>
// ...
enviro = new Enviro;

enviro->Config( StrRef(cwd) );

const char *lc;
if( ( lc = enviro->Get( "P4CHARSET" )) ) {
 CharSetApi::CharSet cs = CharSetApi::NOCONV;

 cs = CharSetApi::Lookup( lc );
 if( cs >= 0 )
 {
   client.SetTrans( cs, cs, cs, cs );
 }
}

This will work for all 8-bit character set like UTF8 (therefore not for UTF16).

Now you have all the building blocks to create a Windows Perforce Application using the C++ API. I hope to see more Perforce client applications out there in the future.

Happy Hacking.

Sven Erik