C# von Perl aus benutzen

Hin und wieder wird es wichtig Fremdcode in Perl einzubinden. Für viele Sprachen gibt es Module wie z.B die Inline module wie inline::C oder ähnliches für Javascript und auch PHP. Speziell für C# gibt es keine solchen Brücken. Das bedeutet man muss sich sich selber bauen. Da sowohl C# als auch Perl interprteiert werden fallen Crosscompiler schnell weg. Der einfachste weg ist nun eine Interprozesskommunikation. Dabei Tritt das C# Programm als Server auf und das Perl Script sendet anfragen.

Nun alles Schritt für Schritt (sofern ich nichts vergessen habe)

1. Ich habe den C# Teil mit Monodevelop erstellt. Ich habe aber nur eine momo-spezifische Bibliothek benutzt (Mono::GetOptions). Ich hatte einfach keine Lust nur zum Parsen der Kommandozeilenoptionen noch eine neue Lib an zu schauen.

2. Ich nutze XML-RPC um die Daten zwischen Perl und C# zu transferieren. Auf der C#-Seite ist das CookComputing.XmlRpc; und auf der perl-Seite RPC::XML::Client.

3. Wenn kein Service (C#) läuft wird er vom Client gestartet und verwaltet. Das macht es möglich den Service auch getrennt zu starten und mehrere Clients darauf zugreifen zu lassen.

4. Ich habe nicht viel Übung in C# und dementsprechend sieht der Code auch aus. :-) Zunächst der Service in C#. Zunächst habe ich ein neues Projekt angelegt: Datei->Neu->Projektmappe... Ein Fenster geht Auf in dem man um Feld Template C#->Terminal Projekt auswählt. Nachdem man einen Projektnamen eingeben hat. (Ich wählte "xml_rpc_deamon") wird das Projekt erstellt. Man bekommt eine Datei "Main.cs" in die man folgendes eintragen sollte:

using System;
using System.Net;
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using CookComputing.XmlRpc;
using Mono.GetOptions; // hatte keine Lust mir noch ein anderes an zu schauen
namespace xml_rpc_deamon
{
  //#######################################################################
  // Klasse zum Parsen der Kommandozeielenargumente ( mono spezifisch )
  class SampleOptions : Options
  {
    [Option ("Runn as deamon", 'd')]
    public bool deamon=false;
    [Option ("set port who runns on", 'p')]
    public int port=5678;
    [Option ("set the ip to bind to", "s")]
    public string host=null;
    public SampleOptions()
    {
      base.ParsingMode = OptionsParsingMode.Both;
    }
  }
  //#######################################################################
  // Klasse zum verwalten der Serveranfragen
  // XML-RPC
  public class TestServer : MarshalByRefObject
  {
    // nachfolgend wird der name der "funktion" benannt
    // er Aufruf ist dasnn ugefähr http://127.0.0.1/test/test.Running
    [XmlRpcMethod("test.Running")]
    public void testRunning()
    { Console.Error.WriteLine("IS RUNNING"); }
    // siehe oben
    [XmlRpcMethod("test.Get")]
    public string GetTest(int Number)
    {
      if (Number < 1 || Number > test_List.Length) return "";
      return test_List[Number-1];
    }
    private static string[] test_List={ "test1", "test2", "test3", "test4", "test5" };
  }
  //#######################################################################
  // Mainklasse
  public class MainClass
  {
    public static void Main (string[] args)
    {
      // parsen der Kommadozeilenargumente
      SampleOptions options = new SampleOptions();
      options.ProcessArgs (args);
      IDictionary props = new Hashtable();
      // Name des Prozesses
      // ist hier nicht wichtig
      // kann alles drin stehen
      props["name"] = "MyHttpChannel";
      // der Port auf dem gelauscht wird
      // default 5678
      props["port"] = options.port;
      // auf eine IP begrenzen
      // default: Alle IPS
      // wenn eine ungültige IP,
      // dann localhost (meist 127.0.0.1)
      if(options.host != null)
      {
        IPAddress ip=null;
        try { ip=IPAddress.Parse(options.host); }
        catch {}
        if(ip != null)
        { props["bindTo"] = ip.ToString(); }
        else
        { props["bindTo"] = IPAddress.Loopback.ToString(); }
      }
      //Ip:Port belegen und lauschen
      HttpChannel channel=null;
      try
      {
       //könnte fehl schlagen
       // ip existiert nicht oder
       // port ist belegt
        channel = new HttpChannel(props,null,new XmlRpcServerFormatterSinkProvider());
      }
      catch
      {
        //bindTo ignorieren
        props.Remove("bindTo");
        try
        {
          // port könnte belegt sein
          channel = new HttpChannel(props,null,new XmlRpcServerFormatterSinkProvider());
        }
        catch
        {
          // aufgeben
          return;
        }
      }
      ChannelServices.RegisterChannel(channel,false);
      // Service anmelden. es wird eien Instanz von der Klasse ober erzeugt
      // und unter "http://127.0.0.1/test/" registriert
      RemotingConfiguration.RegisterWellKnownServiceType(typeof(TestServer),"test",WellKnownObjectMode.Singleton);
      // als deamon gestartet
      // in enen endlosen loop
      // oder andernfalls
      // ein paar ausgeben und aur return zum beenden warten
      if(options.deamon)
      {
        // STDIN STDOUT STDERR schließen
        Console.Out.Close();
        //Console.Error.Close();
        Console.In.Close();
        while(true) System.Threading.Thread.Sleep( 1000 );
      }
      else
      {
        Console.WriteLine("Service running as:");
        string ip="<all avaiable>";
        if(props["bindTo"] != null) ip=props["bindTo"].ToString();
        Console.WriteLine("http://{0}:{1}/test/",ip,props["port"]);
        Console.WriteLine("Press <ENTER> to shutdown");
        Console.Out.Close();
        Console.ReadLine();
      }
    }
  }
}
Die Zeile namespace xml_rpc_deamon sollte man entsprechend Anpassen. Will man das nun ausführen mosert der Compiler, dass er einige Libs nicht finden kann. Das Problem löst man indem man im Menu Projekt->Referenzen bearbeiten... auswählt.

Ein Fenster öffnen sich und kann die Pakete System.Runtime.Remoting und Mono.GetOptions zusätzlich auswählen. Für CookComputing.XmlRpc muss man erst die das Paket von http://www.xml-rpc.net/ herunter laden und entpaken. Dann wählt man im selben Fenster den Reitereintrag .Net-Assembly und selektiert dort im Entpakten Ordner bin/CookComputing.XmlRpcV2.dll und fügt es der Liste hinzu. Nun sollte sich das Programm kompilieren und starten lassen.

Im Projektordner findet man unter <projektname>/<namespace>/bin/Debug/ eine "dll" und eine "exe" die "exe" Lässt sich mit "mono <name>.exe" starten. Das ist der kompilierte Code von oben.
Bei mit sieht das so aus:

insgesamt 128
-rw-r--r-- 1 topeg topeg 118784  8. Apr 23:02 CookComputing.XmlRpcV2.dll
-rwxr-xr-x 1 topeg topeg   6144  8. Apr 23:02 xml_rpc_deamon.exe
-rw-r--r-- 1 topeg topeg    967  8. Apr 23:02 xml_rpc_deamon.exe.mdb
Halbzeit ist erreicht, der Service in C# steht und sollte sich starten lassen. Nun kommen wir zum Perl Teil: Ich habe die exe und dll in einen Ordner "mono" kopiert und dazu ein script "mono_perl_ipc.pl"
-rwxr--r-- 1 topeg topeg   5254  9. Apr 00:22 mono_perl_ipc.pl
drwxr-xr-x 1 topeg topeg     93  8. Apr 23:02 mono
-rw-r--r-- 1 topeg topeg 118784  8. Apr 23:02 mono/CookComputing.XmlRpcV2.dll
-rwxr-xr-x 1 topeg topeg   6144  8. Apr 23:02 mono/xml_rpc_deamon.exe

Der Inhalt des Scriptes: Ich hoffe der Code ist einigermaßen verständlich. mono_perl_ipc.pl:

#!/usr/bin/perl
use strict;
use warnings;
my $port="5678";
my $wait=2;
my $service=csharp_ipc_service->new($port,$wait);
print csharp_ipc_service::error()."\n" unless($service);
if($service->test_running())
{ print "test_running() erfolgreich\n"; }
else
{ print "ERROR:".$service->error()."\n"; }
print "#"x80,"\n";
if($service->test_running())
{ print "test_running() erfolgreich\n"; }
else
{ print "ERROR:".$service->error()."\n"; }
print "#"x80,"\n";
my $val=$service->test_get(1);
if(defined($val))
{ print "test_get(1) = $val\n"; }
else
{ print "ERROR:".$service->error()."\n"; }
print "#"x80,"\n";
$val=$service->test_get(4);
if(defined($val))
{ print "test_get(4) = $val\n"; }
else
{ print "ERROR:".$service->error()."\n"; }
print "#"x80,"\n";
{package csharp_ipc_service;
use strict;
use warnings;
use RPC::XML;
use RPC::XML::Client;
use FindBin;
use POSIX ":sys_wait_h";
my $ERROR=undef;
sub new
{
  my $class=shift;
  my $port=shift;
  my $wait=shift;
  my $self={};
  $self->{stop}=0;
  $self->{pid}=0;
  bless($self, $class);
  unless($self->_start($port,$wait))
  {
    $ERROR=$self->{ERROR};
    return undef;
  }
  return $self;
}
sub test_running
{
  my $self=shift;
  my $ret=$self->_runn_cmd('test.Running');
  return 1 if(defined($ret));
  return 0;
}
sub test_get
{
  my $self=shift;
  my $number=shift;
  return $self->_runn_cmd('test.Get',$number);
}
sub error
{
  my $self=shift;
  if($self && ref($self) eq __PACKAGE__)
  {
    my $err=$self->{ERROR} || '';
    $self->{ERROR}=undef if($self->{ERROR});
    return $err;
  }
  else
  {
    my $err=$ERROR;
    $ERROR=undef;
    return $err;
  }
}
sub _add_error
{
  my $self=shift;
  my $msg=shift;
  if($msg)
  {
    if($self->{ERROR})
    { $self->{ERROR}.="\n$msg"; }
    else
    { $self->{ERROR}=$msg; }
  }
}
sub _runn_cmd
{
  my $self=shift;
  my $resp=$self->{ipc}->send_request(@_);
  if(ref($resp) && ref($resp) ne 'RPC::XML::fault')
  { return $resp->value(); }
  else
  {
    if(ref($resp))
    { $self->_add_error($resp->string()); }
    else
    { $self->_add_error("no server connection ($!)"); }
  }
  return undef;
}
sub _sig_child
{
  my $self=shift;
  my $msg=waitpid($self->{pid},0);
  $self->_add_error("server died unexpected") unless($self->{stop});
}
sub _start
{
  my $self=shift;
  my $port=shift || 5678;
  my $wait=shift || 5;
  unless($self->{pid})
  {
    $self->{stop}=0;
    $self->{pid}=0;
    $self->{ipc}=undef;
    local $SIG{CHLD}=sub{ $self->_sig_child(@_); };
    my $host='http://localhost:'.$port.'/test';
    $self->{ipc}=RPC::XML::Client->new($host);
    my $resp=$self->{ipc}->send_request('x');
    unless(ref($resp))
    {
      my $cs_pid=fork();
      if(defined($cs_pid))
      {
        if($cs_pid)
        {
          $self->{pid}=$cs_pid;
          sleep($wait);
        }
        else
        {
          exec("/usr/bin/mono $FindBin::Bin/mono/xml_rpc_deamon.exe -d -s localhost -p $port");
          exit(10);
        }
      }
      else
      {
        $self->_add_error("Fork failed");
       return 0;
      }
    }
    return 1;
  }
}
sub _stop
{
  my $self=shift;
  if($self->{pid})
  {
    $self->{stop}=1;
    my $pid=$self->{pid};
    local $SIG{CHLD}='DEFAULT';
    kill('KILL',$pid) if(waitpid($pid, WNOHANG)>-1);
    eval{
      local $SIG{ALRM}={die("timeout1\n")};
      alarm(20);
      waitpid($pid,0);
      alarm(0);
    };
    if($@ && waitpid(-1, WNOHANG)>-1)
    {
      kill('TERM',$pid) if(waitpid($pid, WNOHANG)>-1);
      eval{
        local $SIG{ALRM}={die("timeout2\n")};
        alarm(5);
        waitpid($pid,0);
        alarm(0);
      };
      if($@ && waitpid(-1, WNOHANG)>-1)
      {
        $SIG{CHLD}='IGNORE';
        die("Can't kill $pid!\n");
      }
    }
  }
}
sub DESTROY
{ _stop(@_); }
1;}

Sollte alles laufen bekommt man eine Ausgabe wie diese:

./mono_perl_ipc.pl
IS RUNNING
test_running() erfolgreich
IS RUNNING
test_running() erfolgreich
test_get(1) = test1
test_get(4) = test4

Ich habe hier nur das Übertragen von Strings und Integer gezeigt, aber XML-RPC kann auch mit Arrays und Hashes umgehen, wenn man noch komplexere Datenstrukturen übertragen will kann man tiefer in die XML-RPC Kommunikation Einsteigen oder die Daten Serialisieren.