Archived

This forum has been archived. Please start a new discussion on GitHub.

Ice & .NET

Howdy,

I've just completed the first pass of an implementation of the Ice protocol within the .NET framework, in C#. It interoperates fully with and is built on top of the .NET Remoting infrastructure. (Note that the namespace name will change to avoid trademark issues if any, but "Ice" is just too convenient for now!)

The runtime is fully dynamic; all classes, method invocations, etc., are serialized using the Reflection functionality of the CLI. This means that slice2cs just translates the slice definitions into similar abstract C# class declarations. (Using slice and slice2cs isn't really necessary, though it is of course necessary for interoperability with the C++ and Java runtimes.) This is a performance hit when marshalling complex objects -- however, in the future, I plan to create type-specific marshalling code at runtime, and then cache and execute that code (JIT marshaller/demarshaller creation).

Because it's integrated into the .NET Remoting framework, the same objects can be made available via Ice, SOAP, or any of the other available .NET Remoting backends just by adding another channel. (See example below.) Also, already existing generic bits, like the object lifetime management services, work.

Sample server:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;

// this would be automatically generated from slice
public abstract class Hello : Ice.Object {
  public abstract int sayHello(string[] who);
}
 
public class HelloI : Hello {
  public override int sayHello(string[] who) {
    foreach (string w in who) {
      Console.WriteLine ("Hello " + w);
    }
    return who.Length;
  }
}
 
public class Driver {
  public static void Main () {
    Ice.IceChannel ic = new Ice.IceChannel (10000); // port
    ChannelServices.RegisterChannel (ic);

    // the next two lines create a SOAP channel; our object
    // is available on this channel as well. 
    HttpChannel hc = new HttpChannel (9000); // port
    ChannelServices.RegisterChannel (hc);

    // if we were to specify WellKnownObjectMode.SingleCall,
    // a new HelloI instance would be created for each invocation.
    RemotingConfiguration.RegisterWellKnownServiceType (typeof (HelloI),
                                                        "hello",
                                                        WellKnownObjectMode.Singleton);
 
    Console.ReadLine();
  }
}

And the accompanying client:
using System;
using System.IO;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
 
// this is automatically generated from the .ice IDL
public abstract class Hello : Ice.Object {
  public abstract int sayHello(string[] who);
}
 
public class Driver {
  public static void Main () {
    Ice.IceChannel ic = new Ice.IceChannel ();
    ChannelServices.RegisterChannel (ic);

    Hello h = (Hello) Activator.GetObject (typeof (Hello), "ice://localhost:10000/hello");

    string[] who = {".NET", "Mono", "C#"};
    int ret = h.sayHello(who);
    Console.WriteLine ("Said " + ret + " hellos.");
  }
}

Note that I could have requested an Ice.Object instead; the cast to (Hello) would have resulted in a round-trip to ice_ids() to verify that the object can indeed be cast to a Hello class.

There's still a lot of work to do, mainly with tying down exactly how Proxies/ObjRefs are handled and passed, verifying the protocol marshaller/demarshaller, implementing the asynchronous message processing stuff (no extra work is required by client or server code to invoke a message asynchronously, but some extra plumbing needs to go in), and eventually optimizations.

As far as language overhead, an equivalent C# latency/ice_ping test takes about three times as long as the native C++ version; this is using the Mono runtime on linux, which runs at about half-3/4 the speed of the microsoft runtime. Considering that I've made no attempt to optimize anything, this isn't too bad :) The implementation is already (mostly) CLS compliant, meaning that it should be usable from Visual Basic, Visual J#, SML.NET, etc.

Comments

  • marc
    marc Florida
    This is very cool! It's great to see that Ice can be used in .NET world!
  • Very nice, indeed

    Way cool ...

    -Ingo
    http://www.ingorammer.com
  • Code has been checked into mono cvs.. anoncvs info is at http://www.go-mono.com/anoncvs.html ; cvs module name is "ginzu". Still very alpha, there's a few interop issues to resolve as well.
  • Originally posted by vukicevic
    Code has been checked into mono cvs.. anoncvs info is at http://www.go-mono.com/anoncvs.html ; cvs module name is "ginzu". Still very alpha, there's a few interop issues to resolve as well.

    Hi Vladimir, that's great news, thanks! Please, don't hesitate to speak up about interop issues or if something isn't clear in the protocol doc -- I'm keen to help with this.

    Cheers,

    Michi.
  • Will do, thanks!

    One thing that I noticed is that if a message has no class instances that need to be marshalled, an empty sequence is not sent after the main message body. If there are instances to marshal, however, an empty sequence (zero) is sent after all the instance sequences have been sent. I'm not sure if the omission is intentional or not; the docs seem to say that the empty sequence should be marshalled after all instances have been sent, which I assumed also included the case of no instances.

    Either way, the zero sequence is redundant, no? The recipient should know whether it has any outstanding instances to read or not..
  • Originally posted by vukicevic
    Will do, thanks!

    One thing that I noticed is that if a message has no class instances that need to be marshalled, an empty sequence is not sent after the main message body. If there are instances to marshal, however, an empty sequence (zero) is sent after all the instance sequences have been sent. I'm not sure if the omission is intentional or not; the docs seem to say that the empty sequence should be marshalled after all instances have been sent, which I assumed also included the case of no instances.

    Right. The empty sequence marker at the end tells the receiver that it has finished unmarshaling all the class instances for the request. However, if none of the formal parameters of an invocation involves any class instances, that end marker is absent. In other words, the empty marker is sent only for those invocations that actually involve transmission of class instances. The end marker marks the end of the series of class sequences for the instances that are transmitted.

    If you look through the generated code, you will find that, if a request involves marshaling classes, a call to BasicStream::writePendingObjects() is generated. That function places the end marker. For those operations that do not involve classes, writePendingObjects() is never called and, hence, there is no end marker for those invocations. In the receiving code, you will find a corresponding call to readPendingObjects(), which reads the series of sequences off the wire. Again, if the parameter types of an operation indicate that no classes are involved, that call is not generated.

    Either way, the zero sequence is redundant, no? The recipient should know whether it has any outstanding instances to read or not..

    No, unfortunately, that is not true, due to slicing. To see why, consider the example on page 469. In that case, the sender marshals a class which, in its derived part, points at another class instance. The sender, therefore, marshals two class instances, each instance in a separate sequence, followed by the end marker. The receiver of this request does not have type knowledge and slices the first class to its base. But, in doing so, it also slices off the pointer that points at the second instance, so there is nothing in the first instance that would clue the receiver off to the fact that there are actually more instances coming at it on the wire.

    This is why the end marker is necessary: the receiver blindly unmarshals sequences of classes until it finally sees the end marker. That way, the receiver can know when it has finished unmarshaling a class graph even though it may have no type knowledge of much of what is in that graph, and even though the graph, as far as the receiver is concerned, may consist of disconnected islands.

    The same thing can happen with exceptions: the derived part of an exception can have a class member, but the receiver of the exception may not have type knowledge of the derived part of the exception and therefore slice it. In contrast to classes, for exceptions, it is impossible to predict statically whether classes are involved (because the base exception may not have any class members, but the derived exception may or may not have them).

    That is why, for exceptions, we marshal an extra byte on the wire that precedes the exception and indicates to the receiver whether it must read series of class sequences following the exception or not.

    I hope this helps -- please let me know if something isn't clear.

    Cheers,

    Michi.
  • This is why the end marker is necessary: the receiver blindly unmarshals sequences of classes until it finally sees the end marker. That way, the receiver can know when it has finished unmarshaling a class graph even though it may have no type knowledge of much of what is in that graph, and even though the graph, as far as the receiver is concerned, may consist of disconnected islands.

    Ah! That's right, I completely forgot about that case. Okay, all makes sense now. I had a bug that was writing (and was waiting for) that zero even when there were no class instances being marshalled, which is where the question came from. Thank you for the informative reply :) Exception returns/marshalling is something that I've yet to finish, although most of the code is there.

    Thanks,
    - Vlad