All user-authored attributes here do not have arguments unless explicitly specified.
@rpc:Interfacecan be used on an interface.interfacein this document does not include generic interface:$interface IDerivbed:IBased<TYPE>;.
@rpc:Ctorcan be used only when@rpc:Interfaceis on that type.@rpc:Byvaland@rpc:Byrefon a property, method, parameter.@rpc:Cachedand@rpc:Dynamicon a property.@rpc:IdString(string)and@rpc:IdNumber(int)on interfaces, events and methods.- These attributes are generated into RPC Workflow metadata.
- User-authored
@rpc:IdStringand@rpc:IdNumberon RPC interfaces, methods and events are ignored when metadata is regenerated. IdStringstores the generated full text id, andIdNumberstores the generated numeric id from the sorted RPC id list.
All types below count as serializable types, but serialization itself is optional:
- Predefined primitive types.
- Struct types.
- If it is reflection serializable, use the serialization.
- Otherwise, trigger the fallback serialization (generated).
- Enum types, including flags.
- Serialized to its interger value.
- Nullable types.
- Nullable could be applied to primitive types, struct types and enum types.
- Also all nullable types should just be serializable.
- Strong typed collection types, its element type, key type, value type should all be serializable.
- Strong typed collection types here mean the type uses Workflow's collection type syntax.
T{},T[],V[K]andobserve T[].
- Weak typed collection types, like
system::Enumerableand all others, are just trivial interfaces, they are not serializable because they do not have@rpc:Interface.
- Strong typed collection types here mean the type uses Workflow's collection type syntax.
- Interface with
@rpc:Interface. Only shared pointerT^is acceptable,T*is not.
system::RpcObjectReference and system::RpcException are internal RPC transport structs. Generated infrastructure serializes them when needed, but user-authored RPC interfaces cannot use them in function return values, function arguments, or event arguments.
- There are multiple types of property definitions, all count as properties.
FULL-NAMEmeans a full name of a type.- If the target is a member, it becomes
type.member. - If the target is a parameter, it becomes
type.method(parameter).
- If the target is a member, it becomes
- A method is serializable only when all argument types and the return type are serializable.
- If the return type is
void, it is also serializable.
- If the return type is
- When generating helper functions for an error, multiple error messages with the same format can share one helper function. Only text in
xxxbecomes a parameter (or ifxxxhas only one choice in this helper function, it should not be a parameter).- The error code should be
H\d+. - Each helper function will be assigned with a unique error code.
- The error code should be
- (AST) does not belong to the error message, it is a hint that this error should be checked by traversing the AST, otherwise it should be checked against
ITypeDescriptor.
- (AST)
@rpc:Interfacecan only apply to an interface definition, but notFULL-NAME.- Triggered when it is applied to anything else.
@rpc:Interfacecannot be applied to interfaceFULL-NAMEbecause its base typeFULL-NAMEis not serializable.- Triggered when any base type does not apply with
@rpc:Interface. - Triggered for each inqualify base type.
- Triggered when any base type does not apply with
@rpc:Interfacecannot be applied to interfaceFULL-NAMEbecause its memberMEMBER-NAMEis not serializable.- Triggered when the interface type has unserializable members.
- Triggered for each inqualify member.
@rpc:Interfacecannot be applied to interfaceFULL-NAMEbecause its memberMEMBER-NAMEuses reserved RPC typeFULL-NAMEin a return value.- Triggered when a function return value or property value contains
system::RpcObjectReferenceorsystem::RpcException.
- Triggered when a function return value or property value contains
@rpc:Interfacecannot be applied to interfaceFULL-NAMEbecause its function argumentMEMBER-NAMEuses reserved RPC typeFULL-NAME.- Triggered when a function argument contains
system::RpcObjectReferenceorsystem::RpcException.
- Triggered when a function argument contains
@rpc:Interfacecannot be applied to interfaceFULL-NAMEbecause its event argumentMEMBER-NAMEuses reserved RPC typeFULL-NAME.- Triggered when an event argument contains
system::RpcObjectReferenceorsystem::RpcException.
- Triggered when an event argument contains
- (AST)
@rpc:Ctorcan only apply to an interface definition with@rpc:Interface.- Triggered when it is applied to anything else.
- Triggered when it is applied to an interface without
@rpc:Interface.
- (AST)
@rpc:Byvalcan only apply to a property, a method or a parameter.- Triggered when it is applied to anything else.
- (AST)
@rpc:Byvalcan only be used inside an interface type with@rpc:Interface.- Not triggered any of the above error is triggered on the same attribute.
- Triggered when the member it applies to is not in an interface with
@rpc:Interface.
@rpc:Byvalcannot be used on memberMEMBER-NAMEbecause it does not have a strong typed collection types.- For property or parameter, it means the type.
- For method, it means the return type.
- Not triggered any of the above error is triggered on the same attribute.
- Triggered when the member is inqualify.
- (AST)
@rpc:Byrefcan only apply to a property, a method or a parameter.- Triggered when it is applied to anything else.
- (AST)
@rpc:Byrefcannot be used on memberMEMBER-NAMEbecause it already has@rpc:Byval.- Not triggered any of the above error is triggered on the same attribute.
- Triggered when it is applied to a member with
@rpc:Byval.
- (AST)
@rpc:Byrefcan only be used inside an interface type with@rpc:Interface.- Not triggered any of the above error is triggered on the same attribute.
- Triggered when the member it applies to is not in an interface with
@rpc:Interface.
@rpc:Byrefcannot be used on memberMEMBER-NAMEbecause it does not have a strong typed collection types.- For property or parameter, it means the type.
- For method, it means the return type.
- Not triggered any of the above error is triggered on the same attribute.
- Triggered when the member is inqualify.
- (AST)
@rpc:Cachedcan only apply to a property.- Triggered when it is applied to anything else.
@rpc:Cachedcan only be used inside an interface type with@rpc:Interface.- Not triggered any of the above error is triggered on the same attribute.
- Triggered when the member it applies to is not in an interface with
@rpc:Interface.
- (AST)
@rpc:Dynamiccan only apply to a property.- Triggered when it is applied to anything else.
- (AST)
@rpc:Dynamiccannot be used on memberMEMBER-NAMEbecause it already has@rpc:Cached.- Not triggered any of the above error is triggered on the same attribute.
- Triggered when it is applied to a member with
@rpc:Cached.
@rpc:Dynamiccan only be used inside an interface type with@rpc:Interface.- Not triggered any of the above error is triggered on the same attribute.
- Triggered when the member it applies to is not in an interface with
@rpc:Interface.
- Interface with this attribute could have implementation exposed as a singleton.
- RPC interface only allow registering singleton implementing
@rpc:Ctormarked interfaces. - RPC interface has dedicated methods for acquiring such singleton.
Compile Errorsalready limit these attributes on properties / return values / parameters of strong typed collection.- If none is offered, the following collection types will by default
@rpc:Byref, others will by default@rpc:Byval:observe T[].- Strong typed collections whose element or value is an interface, directly or through nested strong typed collections.
- During generating RPC Workflow metadata,
@rpc:Byvaland@rpc:Byreffrom the property will be copied to getter's return value and setter's parameter. - Event arguments will pick the default value, and no customization could apply (because there is no syntax for putting attributes on event arguments).
- The whole collection is sent to a client, and it doesn't keep track on changes from the other side.
- All nested collections in this collection share the same behavior.
- The collection is treated as remote object.
- All nested collections in this collection share the same behavior.
- This is the default option, for any property.
- When property value is not cached, trigger actual RPC action and cache the result.
- When property value is cached, return the result immediately.
- Cached will be cleared if the associate event (if exists) is triggered.
- Client could proactivaly send property values to cache.
- The getter will trigger actual RPC action immediately.
The RPC runtime has three local concepts and one cross-client concept:
IRpcLifecycleowns object/reference conversion for one client.IRpcControllerowns local objects, local wrappers, local ops, and local suppression state for one client.IRpcOperationsexposes the local client's list, object, list-event, and object-event operation objects. List operations and list-event operations are local runtime adapters; cross-client list traffic is transported through object operations and object-event operations with predefined negative ids.IRpcDispatcheris the only object that knows how to send a message from one client to another client.
Lifecycle and controller implementations should not use another lifecycle's objects directly. When a message needs to leave the current client, it goes through IRpcDispatcher.
IRpcLifecycle owns service lookup state. A local service is stored in the service-owning lifecycle as a map from RPC type id to the local service object. A remote service is stored in each receiving lifecycle as a map from RPC type id to the full RpcObjectReference declared by the service owner.
IRpcLifecycle::RegisterLocalService(typeId, service) records a local service before lifecycle initialization. It converts the service object to a local RpcObjectReference, adds the owner hold, stores the local service object, and calls IRpcDispatcher::DeclareLocalService(ref) so the dispatcher implementation can transmit the declaration. Registering the same local type id twice is a recoverable service-registration error, and registering after IRpcLifecycle::Initialize() is also an error.
IRpcDispatcher::DeclareLocalService(ref) is a data-transmission hook, not service storage. The receiving lifecycle handles the declaration through IRpcLifecycle::DeclareRemoteService(ref). Declaring a remote service with the same type id overwrites the earlier remote declaration and stores the full reference, including objectId.
IRpcObjectOps::RequestService should not exist. IRpcLifecycle::GetTypeIdFromName(typeName) resolves the type name through the lifecycle RPC id map and returns RpcTypeId_NotFound when the name is unknown. IRpcLifecycle::RequestService(typeName) uses this function, returns the local registered service when that type id exists locally, or finds the stored remote RpcObjectReference and calls RefToPtr(ref).
RefToPtr(ref) uses ref.clientId to decide whether the ref belongs to the current client:
- If the ref is local, it returns the local object.
- If the ref is remote, it returns or creates a wrapper for the remote object.
Refcounting tracks interested clients for local objects. It is not the same concept as Ptr<T> or std::shared_ptr ownership. A hold means a client has a remote wrapper or an equivalent durable interest in the local object.
IRpcLifecycle::LocalObjectHold(ref, remoteClientId)records thatremoteClientIdis interested in the local object identified byref.IRpcLifecycle::LocalObjectUnhold(ref, remoteClientId)removes that interest.- These functions only track local objects. If
refdoes not belong to the current lifecycle, this is an implementation error. - The counter should track interested clients, not wrapper instances. Wrapper construction and destruction should happen once for each object/client pair, so duplicate holds or unholds for the same
(ref, remoteClientId)should not silently create a different ownership meaning.
PtrToRef(ptr) only converts a local object to a RpcObjectReference. It should allocate a new local object id when needed and attach the object-to-ref internal property, but it must not increase the interested-client counter.
For a normal non-service object, the counter becomes 1 only after the first remote client actually receives the ref and creates its wrapper:
- The remote wrapper constructor calls
IRpcDispatcher::SendToClient_ObjectOps(ref.clientId)->ObjectHold(ref, currentClientId, true). - Generated interface wrappers and predefined container wrappers follow the same rule.
- The owner lifecycle receives this through
IRpcObjectOps::ObjectHoldand callsIRpcLifecycle::LocalObjectHold(ref, remoteClientId). - The wrapper destructor calls
ObjectHold(ref, currentClientId, false)if it has not been disconnected from the lifecycle, and the owner lifecycle callsIRpcLifecycle::LocalObjectUnhold(ref, remoteClientId).
When the interested-client counter decreases to 0, the lifecycle should remove all resources and tracking for that local object reference. After this removal, a later PtrToRef(ptr) on the same object should allocate a new object id.
There is a special case when PtrToRef(ptr) is called but no remote client ever receives the returned ref, so no wrapper constructor sends ObjectHold(..., true). The object-to-ref internal property is the final fallback for this case: if the local object is deleted while the lifecycle is still alive, the property cleanup removes the tracking resource. During lifecycle finalization, implementations should remove these internal properties from all tracked local objects, so objects deleted after the lifecycle is gone do not call back into a finalized lifecycle.
Services use the same local-object tracking, but RegisterLocalService adds an owner hold:
- An object registered by
IRpcLifecycle::RegisterLocalServicehas an initial interested-client count of 1. - This hold represents the owner client being in the interested-client list. It is not created by a remote wrapper.
- Remote clients still send normal hold and unhold messages when they create or destroy wrappers for the service object.
- When all remote clients unhold the service object, the owner hold remains, so the service is not unregistered by ordinary refcounting.
- Ordinary service unregistration is not part of the RPC interface. Finalization clears lifecycle local-object tracking and local/remote service registration state.
When a wrapper performs a method call or list operation, the message is sent to the lifecycle that owns ref.clientId.
- Object method calls use
IRpcDispatcher::SendToClient_ObjectOps(ref.clientId). - List operations use
IRpcDispatcher::SendToClient_ObjectOps(ref.clientId)->InvokeMethod(...)with predefined list method ids throughRpcCallerListOps. - Array resize uses its own predefined method id through
IRpcListOps::ArrayResize; list-only mutation ids such as clear and remove-at should not be accepted as array resize shortcuts.
The returned ops object is the target client's local operation object. The caller should not know or store the target lifecycle directly.
Events are broadcast from the client that observed or raised the event. The broadcast excludes that caller client and sends to all other clients.
- Object events use generated strong typed caller-side ops, which call
IRpcDispatcher::BroadcastFromClient_ObjectEventOps(selfClientId)->InvokeEvent(...). - List events use
IRpcDispatcher::BroadcastFromClient_ObjectEventOps(selfClientId)->InvokeEvent(...)with the predefined observable-list event id throughRpcCallerListEventOps.
This rule applies whether the event is raised from a local object or from a wrapper. If a wrapper raises an event, the owner client is still just another client in the broadcast target set unless it is the caller client.
Event broadcasts return object so the same ops interface can carry either direct reflected values or JSON serialized values. After deserialization, the value is system::RpcException[int] or null, keyed by the lifecycle client id that caught an event handler exception. A broadcast should attempt every target lifecycle even when earlier targets report exceptions, then aggregate all returned maps. The lifecycle that triggered the event receives the aggregate map internally; generated object-event send-side code and predefined container-event send-side code deserialize the returned value and call system::IRpcLifecycle::ReadEventException, which raises a normal Workflow exception message when the aggregate is non-empty.
In the dual-client test implementation, broadcasting can be implemented by returning event ops from the lifecycle whose client id is not selfClientId.
Event suppression is local controller state. It is not dispatcher state.
Suppression prevents an event replayed from a remote message from being forwarded again by the normal locally attached event handlers.
IRpcController exposes these suppression APIs:
SetEventSuppressedFlag(ref, eventId, bool)GetEventSuppressedFlag(ref, eventId)SetItemChangedSuppressedFlag(ref, bool)GetItemChangedSuppressedFlag(ref)
Although the setter takes a bool, implementations should store a counter for each suppression key. Setting true increments the counter. Setting false decrements it. The getter returns true when the counter is greater than zero. Decrementing below zero should be treated as an implementation error.
Object event suppression is keyed by (RpcObjectReference, eventId). List ItemChanged suppression is keyed by RpcObjectReference.
When IRpcObjectEventOps::InvokeEvent(ref, eventId, arguments) receives a remote event, generated rpc_IRpcObjectEventOps should:
- Call
lc.Controller.SetEventSuppressedFlag(ref, eventId, true). - Unbox arguments and raise the local event.
- Catch exceptions into a returned
system::RpcException[int]map keyed bylc.ClientId. - Call
lc.Controller.SetEventSuppressedFlag(ref, eventId, false)in afinallyblock. - Treat unknown event ids as local dispatch errors that are raised directly and are not inserted into the returned exception map.
Generated rpclistener_* handlers should check lc.Controller.GetEventSuppressedFlag(ref, eventId) before forwarding the event. If the flag is set, the handler returns immediately. Otherwise it calls the generated strong typed caller-side ops event method, letting those ops handle boxing, optional JSON serialization, broadcasting, deserialization, and exception raising.
List events use the same shape with the predefined observable-list event id. When receive-side OnItemChanged(ref, index, oldCount, newCount) replays a remote list notification locally, it should set SetItemChangedSuppressedFlag(ref, true), raise the local list notification, catch exceptions into a system::RpcException[int] map keyed by lc.ClientId, return that map as object, and clear the flag in a finally block. The locally attached native list event handler should check GetItemChangedSuppressedFlag(ref) before broadcasting through the object-event dispatcher path, deserialize the returned value when a serializer exists, and call system::IRpcLifecycle::ReadEventException on the resulting map.
When a method return value is marked with @rpc:Byval, the generated IRpcObjectOps::InvokeMethod result is not the boxed collection itself. It is a system::RpcByvalReturnValue object:
valuestores the boxed returned collection, or the JSON serializedJsonNodewhen JSON object ops are used.slotstores an incremental index allocated by the callee-side object ops object.
The generated object ops object owns _slot : int and _byvalReturnValues : object[int]. For each byval collection return, it first calls system::IRpcLifecycle::RpcCopyByval on the real returned collection. This recursively copies every nested collection layer, so later mutation of the original collection cannot affect the value being transported. The copied collection is stored in _byvalReturnValues[slot], and then the copied collection is boxed or serialized for RpcByvalReturnValue.value.
The callee caches the copied collection instead of the boxed or JSON value. This keeps interface elements alive for both non-JSON and JSON transport: boxed byval collections may contain only RpcObjectReference values, and JSON values contain no interface objects at all. Holding the recursive copy keeps the actual interface objects alive until the caller has received the result and created the needed wrappers.
Generated caller-side ops know from metadata whether a return value uses this path. They cast the InvokeMethod result directly to system::RpcByvalReturnValue^, deserialize and unbox value into a local return variable, call EndInvokeMethod(slot) on the same object ops object, and then return the local variable. EndInvokeMethod removes the cached copied collection from _byvalReturnValues.
Non-byval returns do not use RpcByvalReturnValue, _slot, _byvalReturnValues, or EndInvokeMethod for result cleanup.