Passing objects over RPC by value is the default and preferred means of exchanging information. Objects passed by value have no relationship between sender and receiver. Any methods defined on the object execute locally in the process on which they were executed and do not result in an RPC call. Objects passed by value are serialized by the sender with all their member data and deserialized by the receiver.
When objects do not have a serialized representation (e.g. they contain no data) but do expose methods that should be invokable by a remote party in an RPC connection, that object may be marshaled instead of serialized. When an object is marshaled, a handle to that object is transmitted that allows the receiver to direct RPC calls to that particular object. A marshaled object's lifetime is connected between sender and receiver, and will only be released when either sender or receiver dispose the handle or the overall RPC connection.
To the receiver, a marshaled object is represented by an instance of the marshaled interface. The implementation of that interface is an RPC proxy that is very similar to ordinary dynamic proxies that may be generated for the primary RPC connection. This interface has the same restrictions as documented for these dynamic proxies.
CONSIDER: Will interfaces with events defined on them behave as expected, or should we disallow events on marshaled interfaces?
Marshaled objects may not be sent in arguments passed in a notification. An attempt to do so will result in an exception being thrown at the client instead of transmitting the message. This is because notification senders have no guarantee the server accepted and processed the message successfully, making lifetime control of the marshaled object impossible.
When preparing an object to be marshaled, only methods defined on the given interface are exposed for invocation by RPC. Other methods on the target object cannot be invoked.
See additional use cases being considered for general marshalling support.
Every marshaled object's proxy implements IDisposable
.
Invoking IDisposable.Dispose
on a proxy transmits a dispose
RPC notification to the target object and releases the proxy.
A proxy is valid until its receiver disposes it, or the JSON-RPC connection is closed. Its lifetime is not tied to the object that produced it-- disposing the object that returned it will not dispose the marshaled object or its proxy.
A marshaled object and its proxy will both continue to occupy memory while the proxy is valid. A marshaled proxy should always be disposed by invoking IDisposable.Dispose
when it is no longer needed.
Any marshaled object is encoded as a JSON object with the following properties:
__jsonrpc_marshaled
- requiredhandle
- requiredlifetime
- optionaloptionalInterfaces
- optional
The __jsonrpc_marshaled
property is a number that may be one of the following values:
Value | Explanation |
---|---|
1 |
Used when a real object is being marshaled. The receiver should generate a new proxy that directs all RPC requests back to the sender referencing the value of the handle property. |
0 |
Used when a marshaled proxy is being sent back to its owner. The owner uses the handle property to look up the original object and use it as the provided value. |
The handle
property is a signed 64-bit number.
It SHOULD be unique within the scope and duration of the entire JSON-RPC connection.
A single object is assigned a new handle each time it gets marshaled and each handle's lifetime is distinct.
The lifetime
property is a string that MAY be included and set to one of the following values:
Value | Explanation |
---|---|
"call" |
The marshaled object may only be invoked until the containing RPC call completes. This value is only allowed when used within a JSON-RPC argument. No explicit release using $/releaseMarshaledObject is required. |
"explicit" |
The marshaled object may be invoked until $/releaseMarshaledObject releases it. This is the default behavior when the lifetime property is omitted. |
The optionalInterfaces
property is an array of integers that MAY be included to specify that the marshaled object implements additional known interfaces, where each array element represents one of these interfaces.
Each element is expected to add to some base functionality that is assumed to be present for this object even if optionalInterfaces
were omitted.
These integers MUST be within the range of a signed, 32-bit integer.
Each element in the array SHOULD be unique.
A receiver MUST NOT consider order of the integers to be significant, and MUST NOT assume they will be sorted.
Consider this example where SomeMethod(int a, ISomething b, int c)
is invoked with a marshaled object for the second parameter:
{
"jsonrpc": "2.0",
"id": 1,
"method": "SomeMethod",
"params": [
1,
{ "__jsonrpc_marshaled": 1, "handle": 5, "optionalInterfaces": [1] },
3,
]
}
If the RPC server returns a JSON-RPC error response (for any reason), all objects marshalled in the arguments of the request are released immediately to mitigate memory leaks since the server cannot be expected to have recognized the arguments as requiring a special release notification to be sent from the server back to the client.
Receivers SHOULD ignore unrecognized integers in the optionalInterfaces
array.
The receiver of the above request can invoke DoSomething
on the marshaled ISomething
object with a request such as this:
{
"jsonrpc": "2.0",
"id": 15,
"method": "$/invokeProxy/5/DoSomething",
"params": []
}
In the above DoSomething
example, we might suppose that DoSomething
is always defined on this object.
We might further suppose that because optionalInterfaces: [1]
was specified, that some optional functionality is also exposed on that object.
The functionality designated by 1
would be documented by the RPC contract owner and is outside the scope of this general protocol documentation.
But supposing that 1
meant that the marshaled object also offered a DoSomethingElse
method, the receiver of this object could then invoke:
{
"jsonrpc": "2.0",
"id": 20,
"method": "$/invokeProxy/5/1.DoSomethingElse",
"params": []
}
Note the 1.
prefix added to the leaf method name.
This allows distinguishing groups of functionality on the object that may otherwise have colliding method names.
Hosts of marshaled objects MUST support this prefix.
Hosts MAY also support requests that omit the prefix, but should be prepared to handle any ambiguities that may arise.
Receivers of marshaled objects SHOULD use this prefix when invoking methods on optional functionality that comes from optionalInterfaces
.
The receiver of the marshaled object may also "reference" the marshaled object by using __jsonrpc_marshaled: 0
,
as in this example:
{
"jsonrpc": "2.0",
"id": 16,
"method": "RememberWhenYouSentMe",
"params": [
1,
{ "__jsonrpc_marshaled": 0, "handle": 5 },
3,
]
}
Either side may terminate the marshalled connection in order to release resources by sending a notification to the $/releaseMarshaledObject
method.
The handle
named parameter (or first positional parameter) is set to the handle of the marshaled object to be released.
The ownedBySender
named parameter (or second positional parameter) is set to a boolean value indicating whether the party sending this notification is also the party that sent the marshaled object.
{
"jsonrpc": 20,
"method": "$/releaseMarshaledObject",
"params": {
"handle": 5,
"ownedBySender": true,
}
}
One or both parties may send the above notification. If the notification is received after having sent one relating to the same handle, the notification may be discarded.
When an RPC request references a released marshaled object, the RPC server SHOULD respond with a JSON-RPC error message containing error.code = -32001
.
When an RPC result references a released marshaled object, the RPC client library MAY throw an exception to its local caller.
Although object B may be marshaled in RPC messages that target marshaled object A, marshaled object B's lifetime is not nested within A's. Each marshaled object must be indepenently released.