1. Method Parameters ==================== 1.1 Overview ============ The writeability, sharedness, and lifetime of memory passed by reference as a parameter (in or out) depends on the parameter attributes, as well as whether the method is asynchronous. The implementation of the semantics will be different for in-process and remote methods, and not all restrcitions are enforced as strictly for in-process methods, but the semantics when the rules are followed must be the same for both. The data type of parameter also affects how it is handled. Built-in datatypes, bitfields, enums, and inline structs and arrays are passed directly by value (or, depending on the ABI, simple reference-valid- until-end-of-method for out parameters), and thus do not require memory management. Object references are reference counted both at the ORB and process level, and are unique in that the client data structures are read-only and uncopyable (because a unique object needs a unique address) within an address space; most of this section does not apply to them. That leaves non-inline structs and arrays. The ideal way of treating these depends on how they're being used. Simple, small structs and arrays would benefit from being passed as a simple pointer (plus a length field in the case of arrays), with the method copying anything it wants to keep and/or change. For async methods, the caller would need to avoid modifying referenced memory until it knows the method has been executed (in the case of remote methods, it only matters until the ORB has COWed or copied the memory, but client code should not depend on this, or it will not work with an in-process server). This is the default behavior if no parameter attributes are specified. With this type of parameter, there are no restrictions on what allocator the memory being passed came from for in parameters. "Out" and inout parameters will be discussed later. The only major complexity here is in how the duplicate is made. Struct duplications will need to do a deep copy without being confused by reference loops; the stubs will need to provide a "duplicate" function that does this (possibly using a hash table or other associative array to identify reference loops if IDLC determines that they are possible with that particular type of struct). To avoid a double-copy, out-of-process methods may want to merely ask the ORB to give it full, permanent access (via COW if not already copied) to the memory. This may be hard to implement, though, as it requires the server to know that the data came from an out-of-process implementation (even if it's been passed to other functions, some of which may be in-process object-model methods). This optimization may only be useful for large amounts of data that is not likely to have most of its pages modified; if it is likely to be heavily modified, then COW doesn't help much, and may hurt (and if it's large, it probably hasn't already been copied). It may be better to not implement this optimization, and instead recommend that the user use a parameter attribute to deal with the problem. 1.2 Parameter Attributes ======================== With larger arrays and complex data structures, one would often benefit from being able to avoid the copy altogether. This can be accomplished by altering the interface semantics. All of the parameter attributes but "copy" do this, and thus cannot be changed without breaking interface compatibility. None of the current parameter attributes can be combined with any other current parameter attribute. 1.2.1 Default Semantics ======================= If no attribute is specified, "in" parameters are visible only until the method returns, and are read-only. There will be a "duplicate" function provided by the language binding for the server to use if it wants to retain and/or write to the data. For "small" data (the threshold needs to be empirically determined), it just makes a copy. For "large" data, the pages will be copy-on-write mapped (unless the caller asks for an immediate copy). The only real reason not to use the immediate flag for small data (as determined by the programmer) as well (rather than have a threshold) is so that the threshold can be tunable based on the relative performance of copies versus traps on a given system. It'd also be nice if the programmer could ask a profiler to determine whether large data should be COWed or copied immediately on a per-call basis. When the "duplicate" function is called, a copy-on-write mapping of the data will be created. Edge data will be overmapped regardless of page type, but the overmap status will be retained (so that edge pages will not be overmapped into some other address space), though read overmap will be promoted to read/write overmap, as the extra data in the copy will not be used anymore. There will be an option to the "duplicate" function to create fully overmappable pages by copying the edge data and zeroing the rest of the edge pages (in case the caller wants to share the data). 1.2.2 Copy ========== The "copy" attribute affects only the implementation; changing it does not break interface compatibility (and thus require a new GUID). As such, the use of this attribute is specified in the CDL rather than the IDL. Language bindings that do not require a CDL file will provide some way of specifying copy semantics directly from the implementation language. This attribute directs the ORB to automatically make a copy (possibly via COW, but no read-only or shared mappings) of the parameter. For in-process invocation, methods with any "copy" parameters will need to go through a stub to do the copy before calling the real method. 1.2.3 Shared ============ The "shared" attribute declares that the method implementation and caller will treat the data as a read/write shared memory region, lasting beyond the end of the method. The caller must ensure that all segments of data provided (including every nested struct, or struct/array in an array of structs/arrays) either begins and ends exactly on a page boundary, or has all partial pages marked for read/write overmap. For out-of-process methods, an exception will be thrown if this is not the case. For in-process methods, you'll merely go to hell for writing bugs that won't show up until someone hands your code a reference to an out-of-process implementation. All data must be under the management of the ORB memory manager. The shared region is terminated when the caller or callee frees the memory using the ORB memory manager. This requires calling some function that knows whether to actually free it (in the out-of-process case), or release a reference or something (in the in-process case). For arrays, where we're already using a struct with pointer and length, adding a reference count pointer wouldn't be a big deal. Struct pointers, OTOH, are currently plain pointers, and adding an indirection struct with pointer and reference pointer would be ugly, but doable. The alternative is to have the memory manager look up the memory fragment and find the refcount in its internal data structures, which would require a lookup for every reference count operation. 1.2.4 Push ========== The "push" attribute transfers the memory region to the destination, unlinking it from the source address space. The source memory region will be unmapped for out-of-process invocations; for in-process invocations, the memory region will simply belong to the callee, and the caller must not reference it any more. Like "shared", all data fragments must either begin and end on a page boundary, or be in a page with read/write overmap enabled. It is also required that every page being pushed be under the management of the ORB allocator. 1.2.5 Inline ============ When used as a parameter attribute (either explicitly or as part of the type definition), "inline" specifies that the struct or array will be passed directly as a value parameter. A NULL reference cannot be passed, and for out parameters, the method cannot return a reference to an existing struct or array, but instead must fill in the reference given. The "inline" attribute is similar to "copy", except that no in-process stub is required because it is part of the interface (though a stub may be required in certain languages if they do not provide automatic copying of value-passed structs/arrays). An inline array cannot be of variable length, and may be treated differently from other arrays in the language binding (e.g. in C++, inline arrays are bare language arrays, and do not use the Array or MutableArray wrappers). The "inline" attribute can also be used on members of a struct, to indicate that the member is embedded directly in the outer struct, rather than linked with a pointer. 1.2.6 Immutable =============== The "immutable" attribute is similar to "const" in C/C++ ("const" in IDL is used only for compile-time constants), and specifies that the array or struct cannot be modified through this particular reference. It is the default for parameters when neither "shared" nor "push" is used ("copy" and "inline" parameters will accept immutable references on the caller side, but will produce a mutable copy for the server side). It may be specified without effect when it is the default. Immutable "shared"/"push" parameters will result in read-only mappings in the destination address space, though for "push" parameters, the process will have permission to enable writes (this is intended for use by the ORB memory manager when the memory is freed). The "immutable" attribute may also be used on members of a struct. 1.3 Asynchronous methods ======================== Most of the semantics are the same for asynchronous methods; however, the points at which the method begins and ends are less clear. As far as the ORB is concerned, an async method begins when it processes the message. When invoking an async method, there should be a mechanism to get a handle to track the progress of the invocation. This can be used by the caller to try to cancel the method on a timeout, which can only be done if the message has not yet been accepted by the recipient. Once the message has been accepted, in the in-process, non-"copy" case, the caller must not touch the data after this point until it receives a message from the callee that it is finished. In the out-of-process case, if a caller loses patience with the callee, it can free the memory (thus making it exist only in the callee's address space, non-shared). 1.4 Out parameters ================== When a struct or array is being returned via an out or inout parameter, there is no end of method on which to base reference lifetime. As such, if neither "shared" nor "inline" is specified, an out parameter is treated as "push". The default of "push" only applies to the out half of an inout parameter; in general, use of inout should be probably limited to value types and parameters that use "push" in both directions so as to avoid potentially confusing semantics. To return static data that does not need to be freed, out parameters can use the "copy" implementation attribute. The interface semantics will still be "push", but the ORB (or a wrapper function for in-process calls) will allocate a pushable buffer and copy the data into it. If the static data is managed by the ORB memory manager, it will reference count the page rather than make a copy if the buffer is of sufficient size. 1.5 Exceptions ============== Exceptions are thrown as copy/push out parameters. This will often mean unnecessary copies at in-process IDL method boundaries, but exceptions should be small and infrequently thrown, and usually not managed by the ORB memory manager except across method boundaries. 1.6 Unmet needs =============== It is currently impossible to specify attributes (other than "immutable" and "inline") on individual struct members. This would be useful to pass a struct normally that contains a status code or other metadata along with a pushed or shared buffer, without making the outer struct also pushed or shared. It would also be useful to pass a pushed buffer through some fixed superstruct such as a NotifierInfo. 2. The ORB Memory Manager (ORBMM) ================================= The ORB memory manager keeps track of memory allocated by the ORB during an out-of-process method invocation. It is also used for allocations made by user code for memory that may need to be freed by another component in the same address space (such as when using the shared or push attributes). A reference count is kept on each page, so that shared-within-a-process mappings must be released by both caller and callee before the memory is freed. Passing a chunk of data through a "shared" parameter to in-process method increments the page's reference count; this requires a memory manager lookup for every such parameter. Because of this, consider using "push" instead if sharing is not required. In the out-of-process case, each mapping is freed separately, and the kernel handles reference counting the physical page. 2.1 Methods =========== Each language binding shall provide a mechanism by which code may call the following functions on a given type of array or struct. The prototypes are for C++; other language bindings express these methods in whatever form is most appropriate. In C++, the ORB memory manager is at System::RunTime::orbmm, which is a pointer to an instance of System::RunTime::ORBMM. 2.1.1 alloc =========== void *ORBMM::alloc(size_t size, ORBMM::AllocGroup *group = NULL); Allocate the memory required for the given type (and the given array size, if the type is an array). A group handle may be passed; if not, no page will contain more than one allocation. The reference count on the page is incremented if a page has been reused and per-object refcounts are not supported; otherwise, the object's reference count is one. If the allocation spans multiple pages, it will be tracked as an "object", so that each page will have its reference count incremented and/or decremented when appropriate. The implementation may, but is not required to, track reference counts on a per-page basis rather than per-object. The former will generally be more efficient, but will preclude the reuse of an object's memory upon release until the entire page is released. Alternative forms: Type *obj = new(orbmm) Type; Type *obj = new(orbmm) Type[]; Type *obj = new(orbmm, group) Type; Type *obj = new(orbmm, group) Type[]; 2.1.2 retain ============ void ORBMM::retain(Region region); Increment the reference count on the specified object. The region must refer to only one ORBMM object; the implementation may, but is not required to, throw an exception if this rule is violated. If a region smaller than the object is retained, it will not prevent other pages in the region from being freed. 2.1.3 release ============= void ORBMM::release(Region region); Decrement the reference count on the specified object, freeing it if it reaches zero. It is allowed, but not required, that space in multi-object groups be reused when freed, if the same group is used to allocate new objects. This is only possible if reference counts are kept on a per-object basis rather than per-page. The region must refer to only one ORBMM object; the implementation may, but is not required to, throw an exception if this rule is violated. If a region smaller than the object is released resulting in a reference count of zero, portions may be freed prior to the rest of the region's reference count reaching zero. 2.1.4 super_retain ================== void ORBMM::super_retain(Region region); Increment the reference and super-reference counts on the specified object. If the reference count ever goes below the super-reference count, an exception is thrown. This mechanism is intended to ease debugging reference count problems, by turning memory corruption into an exception. It would typically be used when a given object is not intended to be released until the program exits (or some well-defined cleanup procedure is done), such as program and module code and static data. It should also be used when a mapping is created using mmap() or other higher-level function, so as to be able to detect if such a reference is released through release() rather than through the high-level mechanism. The region must refer to only one ORBMM object; the implementation may, but is not required to, throw an exception if this rule is violated. 2.1.5 super_release =================== void ORBMM::super_release(Region region); Decrement the reference and super-reference counts on the given object. The region must refer to only one ORBMM object; the implementation may, but is not required to, throw an exception if this rule is violated. 2.1.6 create_group ================== ORBMM::AllocGroup *ORBMM::create_group(); Create a group handle that can be passed to the alloc function to pack multiple allocations into the same page(s). 2.1.7 destroy_group =================== void ORBMM::destroy_group(ORBMM::AllocGroup *group); Free the memory associated with the group handle returned by create_group. The allocations made under the group are unaffected, and must be released separately. 2.1.8 add_region ================ void *ORBMM::add_region(System::Mem::Region region); The ORB memory manager can manage reference counts of pages that were not allocated using ORBMM. This can be used to refcount non-anonymous mappings (and thus make them usable with parameters that require ORBMM memory). It can also be used on static pages that will never be freed until the program exits. The add_region method places a non-ORBMM controlled region under ORBMM control. The ORBMM may use the existing mapping, or it may remap the pages into its own region of the virtual address space. The address that it uses will be returned. The entire region will be one ORBMM object. Upon a reference count of zero, the pages will be unmapped using System.AddrSpace.unmap().