Download JointCode.Shuttle and samples
Introduction
This article briefly describes a new technology to improve the functions and performance while marshaling across application domains.
Background
All .net/mono code runs within AppDomain
boundry under the hood, yet AppDomain
is not a common technology to use when developing these applications, this is because usually our code runs in one AppDomain
(the default AppDomain
), and we don't create it ourselves, the runtime system creates it for us instead.
However, sometimes you just needs to create a secondary AppDomain
and run code in it. For example, when you needs to load and unload assemblies at runtime without stopping the application.
In these cases, you needs to make cross-AppDomain
calls. So, what's the problem? Well, the problem is not that you make the calls, it’s how you make these calls.
Generally, most people uses the MarshalByrefObject
subclassing based mechanism provided by the runtime to achieve the cross-AppDomain
communication. This is the easiest and most convenient way to go, and everything will be fine, as long as the service to be operated across AppDomain
s inherits from MarshalByrefObject
, like this:
namespace JoitCode.Shuttle.SimpleSample
{
public class MyService : MarshalByRefObject
{
public void Do() { }
}
class Program
{
static void Main(string[] args)
{
var serviceDomain = AppDomain.CreateDomain("ServiceDomain", null, null);
var myService = (MyService)serviceDomain.CreateInstanceAndUnwrap
(typeof(MyService).Assembly.FullName,
"JoitCode.Shuttle.SimpleSample.MyService");
myService.Do();
Console.Read();
}
}
}
Simple as it is, there are some limitations in using this approach.
- AppDomain is a single point accessing infrastructure, that is, a child
AppDomain
can only be accessed by its parent (or host) AppDomain
that created it, and no other AppDomains, even the parent AppDomain
of the host or sibling AppDomain
s created by the same host, can access it. - Lack of flexibility, because the service must inherit from the
MarshalByrefObject
, this limits the flexibility. - Performance concerns, tests show that cross-
AppDomain
method calls using this way is hundreds to a thousand times slower than normal method calls within a same AppDomain
. - Two-way communication, no two-way communications can be achieved this way.
In addition, there are Remoting, WCF, even message queuing, and other IPC mechanisms can be used to achieve cross-AppDomain
communication as well, and these approaches eliminate more or less the above restrictions. However, we can imagine that the performance loss is bound to be more serious than MarshalByrefObject
, and its learning costs will be higher, the implementation will be more complex.
So, it seems that there is no good solution. Is that right?
Solution
The solution is simple, find another way! That’s why i created JointCode.Shuttle.
JointCode.Shuttle is a fast, flexible and easy-to-use service-oriented framework for cross-AppDomain
communication. It's aimed to be a replacement for MarshalByrefObject
provided by the runtime libraries.
JointCode.Shuttle provides the same cross-AppDomain
communication function as MarshalByrefObject
, and features:
- Service-oriented.
- Access to any
AppDomain
from one AppDomain
(the MarshalByrefObject
only allow access to child AppDomain
s from the parent AppDomain
). - Better performance: 60 ~ 70 times faster than
MarshalByrefObject
. - Services are manageable: dynamically register/unregister services at runtime without having to restart the application, or even restart
AppDomain
. - Strong type, easy to use (while the
MarshalByrefObject
way relies on magic string to find the service type). - Built-in IoC functionality for automatic service dependencies management.
- Supports for lazy type / assembly loading.
- The remote service lifetime can be managed by leasing, or on demand (the
MarshalByrefObject
way does not provide remote service life management). - Simple and quick to get started.
- Support .net 2.0.
How to use it
In the JointCode.Shuttle distribution package, it contains two files: JointCode.Shuttle.dll and JointCode.Shuttle.Library.dll, where JointCode.Shuttle.dll is a library written in managed language, and JointCode.Shuttle.Library.dll is a component written in non-managed language, which is depended by the former.
Prepare
To use JointCode.Shuttle, we first need to refer to JointCode.Shuttle.dll in our project, and copy the JointCode.Shuttle.Library.dll to the folder where JointCode.Shuttle.dll is compiled (for example, after the project is compiled, if the JointCode.Shuttle.dll is copied to the c:/projects/sampleproject folder, you need to manually copy the JointCode.Shuttle.Library.dll to this folder).
Using the code
As the JointCode.Shuttle is interface-oriented, so we first need to create a service interface (also called service contract), and apply ServiceInterface
attribute to it.
[ServiceInterface]
public interface ISimpleService
{
string GetOutput(string input);
}
Then we create a service class that implements the contract and apply the ServiceClass
attribute to it.
[ServiceClass(typeof(ISimpleService), Lifetime = LifetimeEnum.Transient)]
public class SimpleService : ISimpleService
{
public string GetOutput(string input)
{
return string.Format
("SimpleService.GetOutput says: now, we are running in AppDomain: {0}, and the input passed from the caller is: {1}",
AppDomain.CurrentDomain.FriendlyName, input);
}
}
Because we want to make cross-AppDomain
calls, we need to write a class for starting/stopping the remote services and let it inherit from MarshalByRefObject
.
public class ServiceProvider : MarshalByRefObject
{
ShuttleDomain _shuttleDomain;
public void RegisterServices()
{
var guid = Guid.NewGuid();
_shuttleDomain.RegisterServiceGroup(ref guid,
new ServiceTypePair(typeof(ISimpleService), typeof(SimpleService)));
}
public void CreateShuttleDomain()
{
_shuttleDomain = ShuttleDomainHelper.Create("domain1", "domain1");
}
public void DisposeShuttleDomain()
{
_shuttleDomain.Dispose();
}
}
Now, we are ready to use JointCode.Shuttle:
namespace JoitCode.Shuttle.SimpleSample
{
public static class ShuttleDomainHelper
{
public static ShuttleDomain Create(string assemblySymbol, string assemblyName)
{
return Create(assemblySymbol, assemblyName, null);
}
public static ShuttleDomain Create(string assemblySymbol, string assemblyName, ServiceContainer svContainer)
{
var dynAsmOptions = new DynamicAssemblyOptions
{
AccessMode = AssemblyBuilderAccess.Run,
AssemblyName = new AssemblyName(assemblyName)
};
var options = new ShuttleDomainOptions
{
DynamicAssemblySymbol = assemblySymbol,
DynamicAssemblyOptions = dynAsmOptions,
DefaultLeaseTime = 10.Seconds(),
PollingInterval = 5.Seconds()
};
try
{
return ShuttleDomain.Create(ref options, svContainer);
}
catch (Exception e)
{
if (e.InnerException != null)
Console.WriteLine(e.InnerException.Message);
else
Console.WriteLine(e.Message);
return null;
}
}
}
class Program
{
static void Main(string[] args)
{
ShuttleDomain.Initialize();
var serviceEnd1Domain = AppDomain.CreateDomain("ServiceEndDomain1", null, null);
var serviceProvider = (ServiceProvider)serviceEnd1Domain.CreateInstanceAndUnwrap
(typeof(Program).Assembly.FullName, "JoitCode.Shuttle.SimpleSample.ServiceProvider");
serviceProvider.CreateShuttleDomain();
serviceProvider.RegisterServices();
var str = Guid.NewGuid().ToString();
var shuttleDomain = ShuttleDomainHelper.Create(str, str);
ISimpleService service;
if (shuttleDomain.TryGetService(out service))
{
try
{
Console.WriteLine("Currently, we are running in AppDomain {0} before calling the remote service method...",
AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine();
var output = service.GetOutput("China");
Console.WriteLine(output);
Console.WriteLine();
Console.WriteLine("Tests completed...");
}
catch
{
Console.WriteLine();
Console.WriteLine("Failed to invoke the remote service method...");
}
}
else
{
Console.WriteLine();
Console.WriteLine("Failed to create remote service instance...");
}
shuttleDomain.ReleaseService(service);
serviceProvider.DisposeShuttleDomain();
Console.Read();
}
}
}
Here is the screenshot of result :
Future
JointCode.Shuttle is still a young project, and it lacks some features, such as service registering/unregistering notification, cross-AppDomain
events, etc. The author will continue to improve existing functions, as well as introduce more new features in the future, but there are some limitations right now, including:
- Only supports 32-bit applications (x86 target platforms)
- Only supports Windows (only supports for .net framework, no mono support at this time)
- No events supported in the service interface
- No support for cross-
AppDomain
events - Not thoroughly tested