Поделиться через


Contracts and IContract

In my post What is an Addin? I said that a "contract" is just that: "a previously decided upon method of communication with rules and limitations." Here is where we discuss the rules and limitations for .NET Addin contracts. In my post Addins in .NET I noted that we need to solve issues of versioning, unloadability and isolation for security. Our definition of a .NET Addin contract helps in all three areas.

The key tenet for solving the versioning issue is separation of "integration types" from "implementation types." In .NET, of course, the Type is the basic unit. For this article I assume you know the difference between Reference Types (class in C#), Value Types (struct in C#) and interfaces.

What we want to do is allow the implementations to version freely and independently of each other. In other words host v3 should be able to load addins that targeted host v1, v2 and v3. We call this "backward compatibility" in the host, meaning it is compatible with past versions. Ideally we also want host v1 to be able to load addins that targeted host v1, v2 and v3. We call this "forward compatibility" in the host, meaning it is compatible with *future* versions. Obviously this is harder to attain. Additionally, of course, a particular host version should be able load any addin version.

The way to attain this versioning, ironically, is to define integration types that *never* version. It is these types that become the "previously decided upon method of communication" or "contracts." These communication types are necessarily abstract -- they are separate from implementation types. We have chosen to use .NET interfaces as the kind of Type for Contracts. Now, technically, we could have used abstract classes, I'm sure you are thinking. However, abstract classes are *allowed* to have implementation, interfaces are not. But also using interfaces allows a particular implementation to implement multiple contracts. The absense of multiple inheritance in the .NET Type system makes abstract classes unworkable here. And finally, using interfaces divorces the type from any implied or explicit underlying remoting infrastructure. We'll go into why this is important below.

So, a contract is an interface, so far so good. The next thing one must realize is that contracts must be defined in separate assemblies from integration types -- because the assembly is the unit of versioning in .NET. Since contracts never version, they must be defined apart from things that *do* version. Which brings us to the next point: Contracts must define a closed system within themselves. The transitive closure of all types exposed by the system must be defined within the system. This must be true to make sure that all of the system maintains its lack of versioning as a unit. As soon as a versioning type breaks into the system, the entire system is compromised. As we will see the other problems require this restriction as well.

So what is legal? How may I define the signatures of the methods of my contracts? We have to start somewhere, and the primitive or intrinsic types defined in .NET are the basic building blocks. We usually refer to these as "strings and ints" but these can include any of the types with an explicit TypeCode value (other than TypeCode.Object) in System.TypeCode. These primitive types are basic enough that we can count on them never changing in future versions of .NET: a string will always be a string, an Int64 will always be an Int64.

Next, other Contracts are legal. Contracts, by definition, don't version, so we can use them. Contracts serve as the Reference Types of the contract type system. Any type that acts as an object with behavior should be passed as a contract.

And finally it *is* legal to define simple structs that are compositions of the primitive types. MAF does this as you will see. These structs *must* be marked as [Serializable] to make sure that they remote. And they must be defined *with* the contracts that they support, not just any serializable struct is OK, only structs defined *within* the contract system will work. Think of these as a way to conveniently pass chunks of data through contracts. You should *not* add behavior to these structs.

Now it could be argued, and indeed we do this in a limited way, that *any* enum, serializable struct or class or MarshalByRef class defined in mscorlib should be legal to be used. Why? Because the intrinisic types are all defined there -- if they are legal why not any type in mscorlib? Surely if the intrinsic types don't break the versioning, these others won't either.

While that line of argument may be true -- if you explore MAF you will see that we do use a few enums defined in mscorlib in our signatures -- it should be used only with EXTREME caution. And other considerations impose additional rules on what is legal, or more aptly illegal.

The first and most important illegal type is "Object." You may *never* have a parameter or return value typed as System.Object. Why? Hopefully this is obvious, but "object" represents "any type" or some type that may *not* be defined within the closed system. The system is no longer closed if Object is exposed.

But there is another reason Object is illegal that has to do with the other issues we need to solve: unloadability and isolation for security. As noted in Addins in .NET, both of these issues are solved with AppDomain isolation. In other words, we load the addins into separate AppDomains so that they can be unloaded -- with the appdomain -- and so that we can set a different Application Base directory to isolate the types as well as sandbox the addin, if desired, with a different, presumably more restricted, set of permissions.

So Object is illegal, along with System.Type, because Object and Type represent arbitrary types that may not be accounted for in the closed system. At best passing an arbitrary object or type across the domain boundary will simply fail -- the type won't be visible to the host AppDomain and fail to load. At worst it can be an elevation of privilege security hole. If a malicious addin can cause an arbitrary type to be loaded in a less restricted AppDomain, static constructors and initializers can run and run arbitrary, perhaps malicious, code.

There are a few other types specifically forbidden. MarshalByRefObject, obviously, for the same reason as Object. Any of the types in the System.Reflection namespace (with the exception of the enums) because they carry with them references to arbitrary types. And, as a catchall here, anything else that can cause any arbitrary type to be loaded is illegal.

Now, as soon as you begin to design any closed contract system of any complexity you realize you need something. You realize you need a base contract that you can use as the "object" of your system. It serves three purposes. The first purpose is to give you the "is a" semantic. You can easily test if an interface "is a" contract if it implements this base contract. The next purpose is to provide an "any contract" idea. You will find that you will have methods that may take or return one of several possible contracts. We've already said it can't be System.Object, so you need a type, this base contract, that can represent them all. And finally, you will find there are a set of basic operations that all contracts require. The base contract can be defined to provide those.

Of course, MAF provides such a base contract, which we call IContract. I noted in my earlier post that IContract provides the analog of both IUnknown and IMarshal from COM. IUnknown is, of course, *the* base "contract" from COM. Marshalling was optional in COM, but in MAF, while the act of marshalling is optional, the ability to marshal is not -- it is a requirement that if any piece of the system must be marshalled then the definition of the system must support marshalling. Of course contracts don't *implement* marshalling -- they don't implement anything by definition -- but IContract contains the definition of methods that help with lifetime and identity management required by marshalled objects. The fact that contracts are themselves interfaces allow the implementations to be marshallable (but the implementations must do work as well to make sure that they marshal).

So let's look at IContract. The current interface definition follows. It is unlikely to change at this point, but I make no promises.

    public interface IContract
{
IContract QueryContract(string contractIdentifier);
        int GetRemoteHashCode();
        bool RemoteEquals(IContract contract);
        string RemoteToString();
        int AcquireLifetimeToken();
        void RevokeLifetimeToken(int token);
    }

The first method is QueryContract. Gee, seems and awful lot like QueryInterface on IUnknown, no? It is there to serve the same purpose. The "contractIdentifier" parameter is just a string here. The convention in the implementation will be that this string contains the AssemblyQualifiedName of the contract being queried for (just use: typeof(IContract).AssemblyQualifiedName for IContract). This is as unique of an identifier as you can get in managed code, more verifiably unique that attaching an arbitrary GUID to an interface definition. Note that we are already using IContract as "any contract." So if I have some contract reference, I can call QueryContract with the AssemblyQualifiedName of any other contract to see if the object implementing the first contract implements the second. It is legal, then, to cast the returned IContract to the desired new contract. By convention, this method should simply return null if the desired contract is *not* implemented. We toyed with the idea of throwing a ContractNotFoundException or some such thing instead of returning null, but in practice this became too unwieldy with try\catches all over the place. And it is really *not* an exceptional situation anyway. Implementations should simply account for null being a possible return -- or else you risk your code throwing a NullReferenceException instead....

The next two methods help to establish and manage identity. The idea of GetRemoteHashCode is that it should always return the hash code of the underlying object to the remote caller. This ensures that the same remote object hashes the same every time it is used. But hash codes are not definitively unique -- one cannot simply compare hash codes to know if this IContract is implemented on the same object identity as that IContract. That is, in general, what RemoteEquals is for. By default Object.Equals maintains identity for reference types. It is overridden in a few cases to allow different object identities to compare as Equal -- identical strings are equal even if they are physically different strings, and delegates are equal if the are delegates for the same function. In general, though, this is the kind of behavior you would want remotely as well. So, in general, RemoteEquals should delegate to the Object.Equals of the underlying object and honor its idea of "Equal."

We debated whether RemoteToString was necessary or even desireable on the base interface. Its intention is to simply delegate to the underlying Object.ToString. It turned out to be useful enough in enough situations that we left it.

The final methods have to do with lifetime management, as their names indicate: AcquireLifetimeToken and RevokeLifetimeToken. The idea here is that if a client of a contract wants to guarantee that the object will stay alive for a certain amount of time, it should call AqcuireLifetimeToken. The client the hangs on to the returned token until it is done with the contract and then calls RevokeLifetimeToken, at which point the object may be eligble for garbage collection.

"Why is this even necessary?" you may ask. Well, garbage collection exists at the AppDomain level. As soon as an object it marshalled out of an AppDomain using .NET Remoting, its lifetime is subject to the .NET Remoting leasing system; .NET Remoting keeps a local reference to it to keep it alive until its "lease" runs out. Once the lease has expired, the object is given back to the GC of the owning AppDomain and it can go away. So remote clients of the object may get an exception when calling an object whose lease has expired. The default lease is 5 minutes. That's right, 5 actual minutes. The lease *is* renewed, though, every time a remote call comes through, so you actually have to let the object lie dormant for 5 consecutive minutes for the default lease to expire. However, this is likely a common thing to do in Addin scenarios: a host may load a bunch of addins, but have no reason to talk to them for hours, if not days, in many situations. Fortunately, the .NET remoting system is extensible. One can "sponsor" a lease, meaning the lease remains active as long as the sponsor (or a sponsor, a particular lease can have any number of sponsors) says to keep renewing it. It is also possible to give an object an infinite lease -- meaning it will never be GC'd, it will live as long as the AppDomain lives. Giving an object an infinite lease is the easiest to implement (though entirely inappropriate in many situations); one simply overrides MarshalByRefObject.InitializeLifetimeService to return null. Sponsors are more complicated to implement of course, and I will discuss how the VSTA implementation of MAF uses sponsors in a future article.

One might imagine that one possible implementation of AcquireLifetimeToken is to add a sponsor to the lease of the contract and of RevokeLifetimeToken is to revoke the same sponsor. So why not just build this in to the interface? Why is it abstracted? One simple reason is that the sponsorship model is not the *only* possible implementation, indeed there are occasions where an infinite lease is desireable. But the main reason is that we do not expect .NET Remoting to remain the underlying Remoting infrastructure for MAF for the forseeable future. As noted in other posts, WCF, nee Indigo, will eventually replace .NET Remoting at every level. The Contracts in general and IContract specifically cannot be tied directly to any Remoting infrastructure in general, nor .NET Remoting in particular. We think the Acquire\Revoke abstraction, though, is going to be flexible enough to support us going forward over different implementations.

So, hopefully, we see why we need contracts, what they are, and why IContract is itself useful. And when I discuss the MAF AddinLoader you will see that we actually require that contracts derive from IContract through a generic constraint. We do this for the "is a" reason above: we know a contract "is a" contract if it derives from IContract.

In future posts I will explore some of the other contracts in MAF, as well as discuss their specific implementations in VSTA.

Comments

  • Anonymous
    July 31, 2006
    You may be asking, "Is thisproxy layer stuff really necessary? Why do I have togenerate proxy types