miniupnpc for JavaとJNAeratorのお話

miniupnpcというCなUPnPのライブラリがあって、これは結構多くのルータをサポートしてて色々なオープンソースアプリで(もしかしたら商用アプリでも)用いられているんだけれど、Javaバインディングがない。
どーせ小さなコードなので、JNIするのもそんな面倒じゃないだろ、と思って書き始めてみた。
が、途中で、JNAという別のネイティブサポートの実装があって、これ用のバインディングJNAeratorで全自動生成出来るという事に気がついた。

これは中々優秀で、たったこれだけのシェルスクリプトバインディングが生成出来た:

#!/bin/sh

UNAME=`uname`
if [ $UNAME = "Darwin" ]; then
	LIBRARY=libminiupnpc.dylib
elif [ $UNAME = "Linux" ]; then
	LIBRARY=libminiupnpc.so
fi

java -jar jnaerator.jar -library miniupnpc miniupnpc.h declspec.h upnpcommands.h upnpreplyparse.h igd_desc_parse.h miniwget.h upnperrors.h $LIBRARY \
-package fr.free.miniupnp -o . -jar miniupnpc_$UNAME.jar -v

で、これを使うとCで書いていたminiupnpcのテストコードが簡単にJavaで書き直せた:

Cのコード
/* $Id: upnpc.c,v 1.67 2009/08/03 22:58:37 nanard Exp $ */
/* Project : miniupnp
 * Author : Thomas Bernard
 * Copyright (c) 2005-2009 Thomas Bernard
 * This software is subject to the conditions detailed in the
 * LICENCE file provided in this distribution.
 * */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WIN32
#include <winsock2.h>
#define snprintf _snprintf
#endif
#include "miniupnpc.h"
#include "upnpcommands.h"
#include "upnperrors.h"

int main(int argc, char **argv)
{
	struct UPNPDev * devlist = 0;
	char lanaddr[16];	/* my ip address on the LAN */
	int i;
	
	if (devlist = upnpDiscover(2000, NULL, NULL, 0))
	{
		struct UPNPDev * device;
		struct UPNPUrls urls;
		struct IGDdatas data;
		if(devlist)
		{
			printf("List of UPNP devices found on the network :\n");
			for(device = devlist; device; device = device->pNext)
			{
				printf(" desc: %s\n st: %s\n\n",
					   device->descURL, device->st);
			}
		}	
		i = 1;
		if(i = UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr)))
		{
			switch(i) {
			case 1:
				printf("Found valid IGD : %s\n", urls.controlURL);
				break;
			case 2:
				printf("Found a (not connected?) IGD : %s\n", urls.controlURL);
				printf("Trying to continue anyway\n");
				break;
			case 3:
				printf("UPnP device found. Is it an IGD ? : %s\n", urls.controlURL);
				printf("Trying to continue anyway\n");
				break;
			default:
				printf("Found device (igd ?) : %s\n", urls.controlURL);
				printf("Trying to continue anyway\n");
			}
			printf("Local LAN ip address : %s\n", lanaddr);
			{
				char externalIPAddress[16];
				char intClient[16];
				char intPort[6];
				int r;
	
				UPNP_GetExternalIPAddress(urls.controlURL,
										  data.servicetype,
										  externalIPAddress);
				if(externalIPAddress[0])
					printf("ExternalIPAddress = %s\n", externalIPAddress);
				else
					printf("GetExternalIPAddress failed.\n");
	
				r = UPNP_AddPortMapping(urls.controlURL, data.servicetype,
										argv[1], argv[1], lanaddr, 0, argv[2], 0);
				if(r!=UPNPCOMMAND_SUCCESS)
					printf("AddPortMapping(%s, %s, %s) failed with code %d (%s)\n",
						   argv[1], argv[1], lanaddr, r, strupnperror(r));

				r = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
													 data.servicetype,
													 argv[1], argv[2],
													 intClient, intPort);
				if(r!=UPNPCOMMAND_SUCCESS)
					printf("GetSpecificPortMappingEntry() failed with code %d (%s)\n",
						   r, strupnperror(r));
	
				if(intClient[0]) {
					printf("InternalIP:Port = %s:%s\n", intClient, intPort);
					printf("external %s:%s %s is redirected to internal %s:%s\n",
						   externalIPAddress, argv[1], argv[2], intClient, intPort);
				}
			}
			FreeUPNPUrls(&urls);
		}
		else
		{
			fprintf(stderr, "No valid UPNP Internet Gateway Device found.\n");
		}
		freeUPNPDevlist(devlist); devlist = 0;
	}
	else
	{
		fprintf(stderr, "No IGD UPnP Device found on the network !\n");
	}
}
Javaのコード
import java.nio.ByteBuffer;
import fr.free.miniupnp.*;

/**
 *
 * @author syuu
 */
public class JavaBridgeTest {
    public static void main(String[] args) {
        int UPNP_DELAY = 2000;
        MiniupnpcLibrary miniupnpc = MiniupnpcLibrary.INSTANCE;
        UPNPDev devlist = null;
        UPNPUrls urls = new UPNPUrls();
        IGDdatas data = new IGDdatas();
        ByteBuffer lanaddr = ByteBuffer.allocate(16);
        ByteBuffer intClient = ByteBuffer.allocate(16);
        ByteBuffer intPort = ByteBuffer.allocate(6);
        int ret;
        int i;

        devlist = miniupnpc.upnpDiscover(UPNP_DELAY, (String) null, (String) null, 0);
        if (devlist != null) {
            System.out.println("List of UPNP devices found on the network :");
            for (UPNPDev device = devlist; device != null; device = device.pNext) {
                System.out.println("desc: " + device.descURL.getString(0) + " st: " + device.st.getString(0));
            }
            if ((i = miniupnpc.UPNP_GetValidIGD(devlist, urls, data, lanaddr, 16)) != 0) {
                switch (i) {
                    case 1:
                        System.out.println("Found valid IGD : " + urls.controlURL.getString(0));
                        break;
                    case 2:
                        System.out.println("Found a (not connected?) IGD : " + urls.controlURL.getString(0));
                        System.out.println("Trying to continue anyway");
                        break;
                    case 3:
                        System.out.println("UPnP device found. Is it an IGD ? : " + urls.controlURL.getString(0));
                        System.out.println("Trying to continue anyway");
                        break;
                    default:
                        System.out.println("Found device (igd ?) : " + urls.controlURL.getString(0));
                        System.out.println("Trying to continue anyway");

                }
                System.out.println("Local LAN ip address : " + new String(lanaddr.array()));
                ByteBuffer externalAddress = ByteBuffer.allocate(16);
                miniupnpc.UPNP_GetExternalIPAddress(urls.controlURL.getString(0),
                        new String(data.servicetype), externalAddress);
                System.out.println("ExternalIPAddress = " + new String(externalAddress.array()));
                ret = miniupnpc.UPNP_AddPortMapping(
                        urls.controlURL.getString(0), new String(data.servicetype),
                        args[0],
                        args[0],
                        new String(lanaddr.array()), null,
                        args[1], null);
                if (ret != MiniupnpcLibrary.UPNPCOMMAND_SUCCESS)
                    System.out.println("AddPortMapping() failed with code " + ret);
                ret = miniupnpc.UPNP_GetSpecificPortMappingEntry(
                        urls.controlURL.getString(0), new String(data.servicetype),
                        args[0], args[1], intClient, intPort);
                if (ret != MiniupnpcLibrary.UPNPCOMMAND_SUCCESS)
                    System.out.println("GetSpecificPortMappingEntry() failed with code " + ret);
                System.out.println("InternalIP:Port = " +
                        new String(intClient.array()) + ":" + new String(intPort.array()));
                ret = miniupnpc.UPNP_DeletePortMapping(
                        urls.controlURL.getString(0),
                        new String(data.servicetype),
                        args[0], args[1], null);
                if (ret != MiniupnpcLibrary.UPNPCOMMAND_SUCCESS)
                    System.out.println("DelPortMapping() failed with code " + ret);
                miniupnpc.FreeUPNPUrls(urls);
            } else {
                System.out.println("No valid UPNP Internet Gateway Device found.");
            }
            miniupnpc.freeUPNPDevlist(devlist);
        } else {
            System.out.println("No IGD UPnP Device found on the network !\n");
        }
    }
}

まぁ、全然JavanizedされてないAPIだけど、Binding書かなくても良いのは大きい。
但し、ぬるぽするとVMごと落ちたりするので、そういう注意は必要。
何がGCされて何がGCされないのか?とか、どれがポインタで実体で…とか、色々面倒くさい事はある。
でも、少なくとも取り敢えず動く。

ちなみに、JRubyはJNAを使ってPOSIXサポートを実装したらしい。

で、出来上がったのでminiupnpcの作者にマージさせろボケがってメール送った。
このままじゃ拒否されるかもしれんが、まぁ、何か議論するのはいいんじゃないカナ。

追記

あっさり「I think I'll add that to the Makefile :)」という返事が返ってきたので、次のsnapshotからJavaが使えるようになるんじゃないかな?と思うです。