UDI Call Marshalling and RPC

From OSDev.wiki
Jump to navigation Jump to search


Prerequisite reading

UDI Core Specification, Vol 1, Section 28.3: "Marshalling". UDI Core Specification, Vol 1, Section 28.4: "MEI Stubs".

Documentation for the following service call:

void udi_mei_call (
   udi_cb_t *gcb,
   udi_mei_init_t *meta_info,
   udi_index_t meta_ops_num,
   udi_index_t vec_idx,
   ... );

Documentation surrounding the following UDI abstract types:

typedef udi_ubit8_t udi_layout_t;

typedef const struct {
	const char *op_name;
	udi_ubit8_t op_category;
	udi_ubit8_t op_flags;
	udi_index_t meta_cb_num;
	udi_index_t completion_ops_num;
	udi_index_t completion_vec_idx;
	udi_index_t exception_ops_num;
	udi_index_t exception_vec_idx;
	udi_mei_direct_stub_t *direct_stub;
	udi_mei_backend_stub_t *backend_stub;
	udi_layout_t *visible_layout;
	udi_layout_t *marshal_layout;
} udi_mei_op_template_t;

Pay particular attention to the visible_layout and marshal_layout members of udi_mei_op_template_t for this article.

UDI Call Marshalling

Call marshalling is the act of taking a function call from a caller, and packaging it such that it can cross one or more boundaries and remain intact and actionable by the callee when it eventually reaches the callee. That is, marshalling a call is the same as a Remote Procedure Call. It is the packaging of a function call such that it can be transported to a callee that is in a different logical environment from the caller. This concept is not unique to UDI, and a Google search will yield any number of results talking about the practice.

Reasons why one might need to marshal a UDI call could be:

  • Your kernel supports a very complex form of network-based compute clustering, and computers can share the devices on other computers, and use them as their own. In such a case, calls across machines may be very common.
  • Your drivers are separate-address-space, much like a microkernel's. You may need to send calls between drivers as messages which will have to cross between address spaces.

UDI recognizes the need for marshalling in microkernels and in distributed computing environments, and standardizes the call-marshalling approach for all UDI calls. Marshalling is not usually needed by monolithic kernels since their drivers do not usually call across address spaces.

For the most part, you will be able to ignore this feature and use it only for determining the total amount of memory required to marshal a call into a message for IPC. However, for very complex Operating Systems which may perform RPC over a network, this can be used to do very cool things, such as endianness swapping of integers and pointers, etc, for maximum portability. Such a scenario may be necessary if you are on a big-endian machine, and you are sending data to a little endian machine, for example. UDI's design extends gracefully and naturally to very large, high-scaling environments.

What provisions does UDI make for Call Marshalling?

UDI standardizes the marshalling format for UDI Channel (aka, IPC) calls. In essence, the environment shall marshal calls according to the UDI specification if it expects to be sending channel calls across machines with Operating System software that does not match its own.

While it is best practice to use the UDI marshalling format for internal channel IPC between drivers within the same running Operating System, if you choose to use some other marshalling methodology, as long as your calls do not need to leave your own machine (and go across say, a distributed compute cluster), you should not encounter any problems -- but you'll have to do some extra work to call into the target driver on the callee side because of your choice. I will not be going into that in detail, because deviances from the specification are beyond the scope of the article.

How are calls to be marshalled in a UDI compliant manner?

When a microkernel or distributed compute node is marshalling IPC or RPC, it should marshal them into a block of memory whose alignment matches the natural data structure alignment of a C compiler. The Generic Control Block is expected to be marshalled first; following this should be the Metalanguage-specific portion of the control block. Next, the function call arguments should be packed in after the completed control block, and finally any inline objects should come last.

If the receiving (callee) end of the IPC/RPC transaction is also UDI compliant, it will be able to successfully unpack the call and process it.

Control Block Marshalling

If a function looks like this:

void udi_enumerate_ack(
	udi_enumerate_cb_t *cb,
	udi_ubit8_t enumeration_result,
	udi_index_t ops_idx);

And its control block looks like this:

typedef struct {
	udi_cb_t gcb;
	udi_ubit32_t child_ID;
	void *child_data;
	udi_instance_attr_list_t *attr_list;
	udi_ubit8_t attr_valid_length;
	const udi_filter_element_t *filter_list;
	udi_ubit8_t filter_list_length;
	udi_ubit8_t parent_ID;
} udi_enumerate_cb_t;

The Environment might marshal the call as follows: it would calculate the amount of memory required to hold all of the parameters for the IPC/RPC call, then allocate a block of memory, or an IPC message that could hold all of the parameters. The marshalled call would end up looking like this, if the environment that marshalled it is UDI compliant:

struct
{
	udi_cb_t gcb;
	// End of the Generic Control Block.
	udi_ubit32_t child_ID;
	void *child_data;
	udi_instance_attr_list_t *attr_list;
	udi_ubit8_t attr_valid_length;
	const udi_filter_element_t *filter_list;
	udi_ubit8_t filter_list_length;
	udi_ubit8_t parent_ID;
	// End of the METALANGUAGE VISIBLE portion of the control block.
	udi_ubit8_t enumeration_result,
	udi_index_t ops_idx);
	/* End of the function call arguments.
	 *
	 * From here on the environment will copy all of the data pointed to by
	 * call. For example,
	 *	udi_instance_attr_list_t *attr_list;
	 *
	 * This "attr_list" parameter points to an array of objects which will
	 * need to be read by the callee. If this array is not also marshalled
	 * and transmitted, the callee will be lacking a part of the call.
	 * So following the function call arguments, we now begin marshalling
	 * these "inline objects" that are pointed to by the procedure call.
	 *
	 * UDI refers to such objects as "Inline objects", because they are
	 * "out of line" with the rest of the procedure call, but pointed
	 * to by pointers which are "inline" in the control block, and needed
	 * by the callee.
	 */
	<COPY HERE THE OBJECT POINTED TO BY "child_data">
	<COPY HERE THE ARRAY FOR "attr_list">
	// End of "attr_list" array.
	<COPY HERE THE ARRAY FOR "filter_list">
} marshalled_udi_enumerate_ack_call;

UDI Layout Descriptors

The key UDI data type that will be discussed in this section is udi_layout_t:

typedef udi_ubit8_t udi_layout_t;

The most obvious question that will be asked is, "How could the environment know how many arguments are in a function call? Or how large the data pointed to by it is?"

Because of the strictly defined interfaces in UDI, known as Metalanguages, all communications across UDI channels are well defined. And the Metalanguage libraries for each Metalanguage protocol also describe the function calls that are sent across them.

We will continue using the example from above, with the udi_enumerate_ack call. The udi_enumerate_ack call has:

  • A Generic Control Block (like every other function in UDI).
  • A set of Metalanguage visible members in its control block.
  • And function call arguments passed on the stack.

We saw how these were marshalled into the designated memory above. In the metalanguage library that defines the function "udi_enumerate_ack", there is an array of descriptors that gives the environment the layout of the parameters taken by udi_enumerate_ack. It looks something like this:

static udi_layout_t		udi_enumerate_ack_visible_layout[] =
{
	UDI_DL_UBIT32_T, UDI_DL_INLINE_UNTYPED,
	UDI_DL_MOVABLE_UNTYPED, UDI_DL_UBIT8_T,
	UDI_DL_MOVABLE_UNTYPED, UDI_DL_UBIT8_T,
	UDI_DL_UBIT8_T,
	UDI_DL_END
};

const static udi_layout_t		udi_enumerate_ack_marshal_layout[] =
	{ UDI_DL_UBIT8_T, UDI_DL_INDEX_T, UDI_DL_END };

The udi_enumerate_ack_visible_layout describes the Metalanguage visible portion of the control block. The udi_enumerate_ack_marshal_layout describes the stack arguments. There are no descriptors anywhere for the Generic Control block portion of every call, because it is well known and never needs to be described. Generic Control Blocks have the same structure.

In the real world, these two layout descriptors are found within the udi_mei_op_template_t entry for that function.

Each entry (UDI_DL_UBIT32_T, UDI_DL_INLINE_UNTYPED, etc) tells the environment how large the function parameter is expected to be. The entries are listed in order, so the ordering is also known to the environment. Using this information, the environment can safely marshal calls across various types of boundaries in a portable manner for any type of IPC or RPC operation.