- 1 Project Overview
- 2 How RPC Works
- 3 GPP Side Architecture
- 4 DSP Side Architecture
- 5 Function Signatures
DSP-RPC-POSIX is a component of the C6Run project which allows you to do DSP->GPP remote procedure calls - that is, you can invoke functions/code residing somewhere on the GPP side directly from the DSP as if you were accessing a local function (there are, of course, certain requirements and restrictions).
"What functions are available on the GPP side, then?" one might ask. The answer is pretty much "everything that the hardware can do" or "everything you can do in your regular operating system" or something along the lines of that. From basic tasks like accessing the file system to more sophisticated things like sending a file over FTP, there's a myriad of possibilities.
Two reasons as to why DSP->GPP RPC is desireable would be: access to otherwise (directly) inaccessible features and being able to reuse existing code. The reason why C6RunApp exists is because it's messy to write and run code for the DSP, especially if all you want to do is try out or experiment with things (prototyping). C6RunApp makes your life easier as a DSP-side developer by offering you easy compilation/running and access to console I/O (which is actually a limited form of RPC); DSP-RPC-POSIX expands this by granting you access to virtually any existing functionality you have on the GPP side.
How RPC Works
Step-by-step RPC Events
Let's start by some definitions:
- DSP-side application - this is what you're assumed to be currently working on, which you compile with the C6Run script and will work on the DSP.
- GPP-side application - sets up the DSP app and starts running it, and then "answers" the RPC requests. C6Run actually generates this for you, so you don't have to worry about anything here.
- RPC target - a function which resides somewhere in the GPP side (could be a library, a shared library, your own code, etc.)
- DSP-side stub - a little wrapper function which looks identical to the RPC target. it causes the RPC target to be executed with the parameters you passed to it, and returns you the same value it returns. this is what you actually call from the DSP-side. can be produced by the c6runapp-rpcgen tool, or written manually.
- GPP-side stub - another little wrapper function on the GPP side, this is what the GPP side application actually calls. this function "knows" how to call the RPC target itself, so it executes that call and gets the result. can be produced by the c6runapp-rpcgen tool, or written manually.
The run of events that occur when you want to do a remote procedure call are as follows:
- From inside the DSP-side application, the DSP-side stub is called (which looks identical to the RPC target)
- The DSP-side stub is executed. It initializes the RPC request, and copies all the parameters into the request package (called "marshalling"), and signals for the RPC to be performed.
- The request package is sent to the GPP-side application using the RPC transport.
- The GPP-side application receives the package, unpacks it and extracts the parameters into a buffer (called "unmarshalling"). Some extra processing such as address translation for buffer/pointer parameters may be carried out at this step.
- The GPP-side application locates the relevant GPP-side stub and executes it.
- The GPP-side stub executes the RPC target, using the provided parameters, then stores the return value into another buffer. Some extra processing regarding structures or non-shared buffer return types may be carried out at this step.
- The GPP-side application sends back the result to the DSP-side.
- The DSP-side stub receives the result in the buffer, extracts and returns it to the user code.
Structure of the RPC Package
The buffer carrying a RPC request is structured as follows:
4 NameLen 4 SignatureLen ... 1 +----------+--------+----------------+-------------+----------+---+ | NameLen | Name | SignatureLen | Signature | Params | 0 | +----------+--------+----------------+-------------+----------+---+
- NameLen: length of the function name
- Name: function name of the GPP-side stub to be executed (observe: NOT the name of the RPC target)
- SignatureLen: length of the function signature
- Signature: function signature describing how the parameters section will be unpacked
- Params: the function parameters, packed without any size promotions or alignment
- 0: the null-terminating zero signalling the end of the package
GPP Side Architecture
Relevant source code files: build/gpp_libs/rpc_server.c build/gpp_libs/rpc_server.h build/gpp_libs/cio_ipc.c rpc/gpp/*.c
DSP-RPC-POSIX's GPP-side is "heavier" compared to its DSP-side and almost completely integrated into the C6Run GPP-side library. Aside from C6Run's regular GPP-side duties such as setting up the DSP and serving C I/O requests, it is also responsible for these tasks:
- extracting/cleaning up the GPP stubs library
- receiving and responding to RPC requests
- unmarshalling received packages
- postprocessing stubs' returned data
- locating and executing stubs
- managing RPC memory
The GPP Stubs Library
All GPP stubs located inside the rpc/gpp directory are compiled into a dynamic link library (librpcstubs.so), which allows the usage of dlfcn.h functions dlsym to dynamically locate them by their names. This dynamic link library is rebuilt, converted into a C header file and included in the compilation of the final executable. Upon launch, the library is temporarily extracted into the same directory as the executable, used for locating and executing the stubs, then removed upon termination.
Receiving and Responding to RPC Requests
The important task of servicing RPC requests - that is, carrying out the recieve-unmarshal-locate-execute-return steps, is currently done inside the C I/O service routines (since the RPC transport is carried out via the C I/O transport), located inside build/gpp_libs/cio_ipc.c.
DSP Side Architecture
Relevant source code files: rpc/core/dsp_core.c rpc/core/dsp_stubs_base.h rpc/dsp/*.c
DSP-RPC-POSIX's DSP-side is very small and rather simple - in fact, none of it currently resides in the C6Run DSP-side libraries but are compiled alongside the user sources every time (thus, modifications to the DSP-side code never need a re-build of the C6Run libraries - just the re-execution of c6runapp-cc script). This is mainly because there isn't all that much to do on the DSP-side: we have a buffer of a certain size (see RPC_BUFSZ in dsp_stubs_base.h) into which every DSP-side stub is responsible for copying its function name, function signature and parameters ("marshaling"), which is then sent to the GPP side using the transport, then the reply obtained and passed back to the DSP stub.
Aside from the information transmitted in the RPC request, there is one more piece of information given to the GPP side which is vital to the servicing of the RPC call - the message identifier, which describes the nature of the RPC message and defined as follows:
RPC_MSG_REQUEST generic RPC function call request
RPC_MSG_RESPONSE sent by the GPP side as a reply to every RPC request, both generic and specialized
RPC_MSG_MALLOC specialized RPC function call request, for memory allocation
RPC_MSG_FREE specialized RPC function call request, for memory dellocation
RPC_MSG_TRANSLATE specialized RPC function call request, for address translation
The reason why specialized call requests exist is because these particular functions should not be called from inside the GPP stubs library, but be handled directly inside the RPC server. Since all regular stubs would automatically use RPC_MSG_REQUEST, the specialized functions are defined manually in dsp_core.c - they also don't obey the structural conventions defining the RPC packages (no function name, signature or null terminator is given).
Observe that these identifiers are NOT used as the MSGQ MSG identifier - since the RPC transport is carried out via C6Run's existing C I/O transport, those are always CIO_TRANSFER. These RPC identifiers are carried on the command byte during writemsg for requests and the first byte of parm for responses.
Currently, the RPC transport is completely carried out via C6Run's existing C I/O transport system - that is, the functions writemsg and readmsg.
The definitions of writemsg and readmsg, and the usage conventions for parameters are as follows:
void writemsg(unsigned char command, const unsigned char *parm, const char *data,unsigned int length)
- command - the RPC message identifier
- parm - unused, any 8-element char array
- data - the RPC buffer
- length - length of the RPC buffer
void readmsg(register unsigned char *parm, register char *data)
- parm - char array whose first element should contain RPC_MSG_RESPONSE
- data - buffer that contains the RPC response
The function signature is a string of characters describing the data type of a function's return value and parameters. The reason why this data is needed is threefold:
- the unmarshaller needs to know how many bytes each parameter takes while extracting them from the buffer
- GPP and DSP address spaces aren't the same, and in order to know when to perform address translation the unmarshaller needs to know which parameters are pointers (should be translated) and which parameters are regular values (should be left untouched)
- special treatment (such as copying into a shared buffer) may be needed for some functions' return values
The signature is composed of ASCII characters, starting with the character representing the return type and continuing with characters representing each parameter in order. Its length always has to be nonzero (the return type is always needed).
|Signature||Data Type||Size (bytes)||Allowed Positions||Description|
|v||void||0||all||void data - can be omitted for parameters but always needs to be explicitly specified for return types!|
|c||char||1||all||char data, or any other 1-byte data|
|s||short int||2||all||short int data, or any other 2-byte data|
|i||int||4||all||int data, or any other 4-byte data|
|f||float||4||all||float data, i can also be used instead, provided for convenience|
|d||double||8||all||double data, or any other 8-byte data|
|@||pointer||4||all||pointer that needs address translation - see "Pointers and Shared Memory" section for details|
|a||pointer||4||all||pointer which won't be dereferenced and doesn't need address translation - see "Pointers and Shared Memory" section for details|
|$||pointer||4||return||manually copy a number of bytes from the returned pointer into a shared buffer and return the shared buffer - see "Returning Non-Shared Buffers" section|
|#||struct||*||return||manually copy a number of bytes from the returned pointer into the result buffer and return the result normally - see "Returning structures" section|
|>||*||*||param||variadic parameter block: previous parameter is assumed to be containing a printf-style format string, all variadic pointer parameters are address translated using the info from the format string|
|<||*||*||param||variadic parameter block: previous parameter is assumed to be containing a scanf-style format string, all variadic parameters are pointers and are subjected to address translation using the info from the format string (i.e, how many variadic parameters there are)|
Observe that indirect pointers (double/triple pointers such as char**, void**) are not fully supported - the contained direct pointers won't be translated, and neither will be pointers hidden inside struct's