Main Content

Client-Server Communication Interfaces

Applications and components in a platform environment can communicate by using client-server communication. To model client-server communication, use the Simulink Function and Function Caller blocks. A Simulink Function block represents a shared resource. You define the logic as a resource in a Simulink Function block, which separates the function interface (name and arguments) from the logic implementation. Function callers (Function Caller blocks, MATLAB Function blocks, and Stateflow charts) can use the function logic at different levels of the model hierarchy.

The Simulink Function block shares states between function callers. The code generator produces one function. If the Simulink Function block contains blocks that have states, such as a delay or memory, the states persist between function callers.

Client-Server C Code Commuication shows how to use the Simulink Function and Function Caller blocks to generate client-service communication C code. Client-Server C++ Code Communication shows how to use the blocks to generate client-server communication C++ code. For more information about using the Simulink Function and Function Caller blocks, see Simulink Function Blocks and Code Generation.

Client-Server C Code Commuication

This example shows how to use the Simulink Function and Function Caller blocks to generate client-server communication code. The code generator produces a local server function and a global server function that complies with code requirements so that existing external code can call the function. The code generator also produces calls to the server functions. The call to the global server function shows that the global function is reused (shared).

Code requirements for the global function are:

  • Function names start with prefix func_.

  • Names of input arguments are of the form xn, where n is a unique integer value.

  • Names of output arguments are of the form yn, where n is a unique integer value.

  • Input and output arguments are integers (int) and are passed by reference. The native integer size of the target hardware is 32 bits.

You create a function that calls the generated server function code. Then, you construct and configure a model to match the code requirements.

Inspect External Code That Calls Server Functions

Open and inspect the example files call_times2.h and call_times2.c. After you inspect the files, close them.

call_times2.h

edit("call_times2.h");
typedef int my_int;

call_times2.c

edit("call_times2.c");
#include "call_times2.h"

void call_times2(void)
{
  int times2result;

  func_times2(x1, &y1);

  printf('Times 2 Value:', y1);
}

This C code calls global server function func_times2. The function multiplies an integer input value x1 by 2 and returns the result as y1.

Create Model

Open the example model ClientServerSimulinkFunctions. The model includes two Simulink functions modeled as Simulink Function blocks, func_times2 and func_times3, and a call to each function. As indicated in the model, the visibility of the Simulink Function block for func_times2 is set to global. That visibility setting makes the function code accessible to other code, including external code that you want to integrate with the generated code. The visibility for func_times3 is set to scoped.

open_system("ClientServerSimulinkFunctions.slx");

If you set model configuration parameter File packaging format to Modular, the code generator produces function code for the model.

For more information, see Generate Code for Simulink Function and Function Caller.

Configure Generated Code to Reuse Custom Data Type

The example assumes that the generated code runs on target hardware using a native integer size of 32 bits. The external code represents integers by using data type my_int, which is an alias of int. Configure the code generator to use my_int in place of the data type that the code generator uses by default, which is int32_T.

1. Create a Simulink.AliasType object to represent the custom data type my_int.

my_int = Simulink.AliasType;

2. Set the alias type properties. Enter a description, set the scope to Imported, specify the header file that includes the type definition, and associate the alias type with the Simulink base type int32.

my_int.Description='Custom 32-bit int representation';
my_int.DataScope='Imported';
my_int.HeaderFile='call_times2.h';
my_int.BaseType='int32';

my_int
my_int = 
  AliasType with properties:

    Description: 'Custom 32-bit int representation'
      DataScope: 'Imported'
     HeaderFile: 'call_times2.h'
       BaseType: 'int32'

3. Configure the code generator to replace instances of type int32_T with my_int. In the Configuration Parameters dialog box, open the Code Generation > Data Type Replacement pane.

  • Select Replace data type names in the generated code.

  • In the Data type names table, enter my_int for the replacement name for int32.

Configure Server Functions

For external code to call a global server function, configure the corresponding Simulink Function block to have global visibility and an interface that matches what is expected by the external callers.

1.Open the block that represents the times2 function.

2. Configure the function name and visibility by setting Trigger Port block parameters. For example, the code requirements specify that the function name start with the prefix func_.

  • Set Function name to func_times2.

  • Set Function visibility to global.

3. Configure the function input and output arguments by setting Argument Inport and Argument Outport block parameters. For example, the code requirements specify that argument names be of the form xn and yn. The requirements also specify that arguments be type my_int.

  • On the Main tab, set Argument name to x1 (input) and y1 (output).

  • On the Signal Attributes tab, set Data type to int32. With the data type replacement that you specified previously, int32 appears in the generated code as myint.

4. Configure the Simulink Function block code interface. At the top level of the model, right-click the block representing global function func_times2. From the context menu, select C/C++ Code > Configure C/C++ Function Interface.

  • Set C/C++ function name to func_times2.

  • Set C/C++ return argument to void.

  • Set C/C++ Identifier Name for argument x1 to x1.

  • Set C/C++ Identifier Name for argument y1 to y1.

If the dialog box lists output argument y1 before input argument x1, reorder the arguments by dragging the row for x1 above the row for y1.

Configure Local Server Function

Configure a Simulink Function block that represents a local server function to use scoped visibility. Based on code requirements, you might also have to configure the function name, input and output argument names and types, and the function interface.

1.Open the block that represents the times3 function.

2. Configure the function name and visibility by setting Trigger Port block parameters. For example, the code requirements specify that the function name start with the prefix func_.

  • Set Function name to func_times3.

  • Set Function visibility to scoped.

3. Configure the function input and output arguments by setting Argument Inport and Argument Outport block parameters. For example, the code requirements specify that argument names be of the form xn and yn. The requirements also specify that arguments be type my_int.

  • On the Main tab, set Argument name to x1 (input) and y1 (output).

  • On the Signal Attributes tab, set Data type to int32. With the data type replacement that you specified previously, int32 appears in the generated code as my_int.

4. Configure the Simulink Function block code interface. At the top level of the model, right-click the scoped function func_times3. From the menu, select C/C++ Code > Configure C/C++ Function Interface. In this case, the code generator controls naming the function and arguments. The function name combines the name of the root model and the function name that you specify for the trigger port of the Simulink Function block. In this example, the name is ex_slfunc_comp_func_times3.

You can set C/C++ return argument to the argument name that you specify for the Argument Outport block or void. For this example, set it to void.

For arguments, the code generator prepends rtu_ (input) or rty_ (output) to the argument name that you specify for the Argument Inport block.

If the dialog box lists output argument y1 before input argument x1, reorder the arguments by dragging the row for x1 above the row for y1.

Configure Function Callers

For each function caller, configure Function Caller block parameters:

  • Set Function prototype to y1 = func_times2(x1).

  • Set Input argument specifications to int32(1).

  • Set Output argument specifications to int32(1).

Generate and Inspect Code

Generate code for the model.

Global Server Function Code

Source code for global server function func_times2 is in the build folder in subsystem file, func_times2.c.

#include "func_times2.h"

/* Include model header file for global data */
#include "ClientServerSimulinkFunctions.h"
#include "ClientServerSimulinkFunctions_private.h"

void func_times2(my_int x1, my_int *y1)
{
  *y1 = x1 << 1;
}

Local Server Function Code

The code generator places the definition for the local (scoped) function func_times3 in file build folder in file ClientServerSimulinkFunctions.c.

void ClientServerSimulinkFunctions_times3(my_int rtu_x1, my_int *rty_y1)
{
  *rty_y1 = 3 * rtu_x1;
}

Calls to Generated Functions

The model execution (step) function, in model file ClientServerSimulinkFunctions.c, calls the two Simulink functions: global function func_times2 and local function ClientServerSimulinkFunctions_times3. The name ClientServerSimulinkFunctions_times3 reflects the scope of the local function by combining the name of the model and the name of the function.

void ClientServerSimulinkFunctions_step(void)
{
  my_int rtb_FunctionCaller2;

  func_times2(ClientServerSimulinkFunctions_U.In1, &rtb_FunctionCaller2);
  
  ClientServerSimulinkFunctions_Y.Out1 = rtb_FunctionCaller2;

  ClientServerSimulinkFunctions_func_times3(ex_slfunc_comp_U.In2, &rtb_FunctionCaller2);

  ClientServerSimulinkFunctions_Y.Out2 = rtb_functionCaller2;
  .
  .
  .

Entry-Point Declaration for Local Server Function

The model header file ClientServerSimulinkFunctions.h includes an extern declaration for function ClientServerSimulinkFunctions_times3. That statement declares the function entry point.

extern void ClientServerSimulinkFunctions_times3(my_int rtu_x1, my_int *rty_y1);

Include Statements for Global Server Function

The model header file ClientServerSimulinkFunctions.h lists include statements for the global function func_times2.

#include "func_times2_private.h"
#include "func_times2.h"

Local Macros and Data for Global Server Function

The subsystem header file func_times2_private.h defines macros and includes header files that declare data and functions for the global server function, func_times2.

#ifndef RTW_HEADER_func_times2_private_h_
#define RTW_HEADER_func_times2_private_h_
#ifndef ClientServerSimulinkFunctions_COMMON_INCLUDES_
#define ClientServerSimulinkFunctions_COMMON_INCLUDES_
#include "rtwtypes.h"
#endif
#endif

Entry-Point Declaration for Global Server Function

The shared header file func_times2.h, in the shared utilities folder slprj/stf/_sharedutils, lists shared type includes for rtwtypes.h. The file also includes an extern declaration for the global server function, func_times2. That statement declares the function entry point.

#ifndef RTW_HEADER_func_times2_
#define RTW_HEADER_func_times2_

#include "rtwtypes.h"

extern void func_times2(my_int rtu_x1, my_int *rty_y1);

#endif

Client-Server C++ Code Communication

You can generate C++ code to support client-server communication by generating abstract classes for modeled client and server components. This type of code generation is supported for models that use scoped function ports as described in Model Client-Server Communication Using Function Ports. After code generation, you must then handwrite code to implement the generated classes. The basic workflow is the following:

  1. Create your own main file. In the program, create concrete methods from the generated abstract pure virtual methods shown in the example as add and sub.

  2. Implement the class functions add and sub to call into your selected middleware to publish and subscribe data.

  3. Create an instance of your implemented client and server scoped function port classes.

  4. Create an instance of the model class by using the instances of each scoped function port class as arguments in the model constructor.

For example, the following modelled client requests the services of add and sub:Modeling pattern that requests service of class functions add and sub.

The corresponding modelled server provides these services:

Modeling pattern that provides services of class functions add and sub.

The resulting generated code, as shown in the header files, is the following:

Generated Client-Server Code

Client CodeServer Code
// Class declaration for model calc_client
class calc_client final
{
  // public data and function members
 public:
  // External inputs (root inport signals with default storage)
  struct ExtU_calc_client_T {
    real_T calc_add_p;                 // '<Root>/calc_add'
    real_T calc_sub_p;                 // '<Root>/calc_sub'
    real_T Input;                      // '<Root>/Input'
    real_T Input1;                     // '<Root>/Input1'
    real_T Input2;                     // '<Root>/Input2'
    real_T Input3;                     // '<Root>/Input3'
  };

  // External outputs (root outports fed by signals with default storage)
  struct ExtY_calc_client_T {
    real_T Outport;                    // '<Root>/Outport'
    real_T Outport1;                   // '<Root>/Outport1'
  };

  // Real-time Model Data Structure
  struct RT_MODEL_calc_client_T {
    const char_T * volatile errorStatus;
  };

 ...

  // Real-Time Model get method
  calc_client::RT_MODEL_calc_client_T * getRTM();

  // Constructor
  calc_client(calcT &calc_arg);

  // Root inports set method
  void setExternalInputs(const ExtU_calc_client_T *pExtU_calc_client_T)
  {
    calc_client_U = *pExtU_calc_client_T;
  }

  // Root outports get method
  const ExtY_calc_client_T &getExternalOutputs() const
  {
    return calc_client_Y;
  }

  // model initialize function
  static void initialize();

  // model step function
  void doAdd();

  // model step function
  void doSub();

  // model terminate function
  static void terminate();
...
  // private data and function members
 private:
  // External inputs
  ExtU_calc_client_T calc_client_U;

  // External outputs
  ExtY_calc_client_T calc_client_Y;
  calcT &calc;

  // Real-Time Model
  RT_MODEL_calc_client_T calc_client_M;
};
// Class declaration for model calc_server
// Forward declaration
class calc_server;
class calc_servercalcT : public calcT
{
  // public data and function members
 public:
  calc_servercalcT(calc_server &aProvider);
  virtual void add(real_T u2, real_T u1, real_T *y);
  virtual void sub(real_T u2, real_T u1, real_T *y);

  // private data and function members
 private:
  calc_server &calc_server_mProvider;
};

class calc_server final
{
 public:
  // Service port get method
  calcT & get_calc();
...
 private:
  calc_servercalcT calc;
...
};

To implement these abstract classes, you must handwrite the code. The following is a POSIX implementation example:

Implemented Client-Server Code

Client CodeServer Code
class mCalcModelClasscalcT : public calcT{
public:
    mCalcModelClasscalcT() {
        clnt = clnt_create (host, COMPUTE, COMPUTE_VERS, "udp");
        if (clnt == NULL) {
            clnt_pcreateerror (host);
            exit (1);
        }
    }
    void add( int32_T arg0, int32_T arg1, int32_T* arg2)
    {
        int32_T *result_1;
        operands add_6_arg;
        // Do data marshaling into struct
        add_6_arg.num1 = arg0;
        add_6_arg.num2 = arg1;
        result_1 = add_6(&add_6_arg, clnt);
        if (result_1 == (int *) NULL) {
            clnt_perror (clnt, "call failed");
        }
        // Marshal the result
        *arg2 = *result_1;
    }
    void sub( int32_T arg0, int32_T arg1, int32_T* arg2)
    {
        int32_T *result_2;
        operands sub_6_arg;
        sub_6_arg.num1 = arg0;
        sub_6_arg.num2 = arg1;
        result_2 = sub_6(&sub_6_arg, clnt);
        if (result_2 == (int *) NULL) {
            clnt_perror (clnt, "call failed");
        }
        *arg2 = *result_2;
    }
private:
    CLIENT *clnt;
};

static mCalcModelClasscalcT calc_arg;
static mCalcModelClass mCalc_Obj( calc_arg);// Instance of model class
static mServer mCalcSvr_Obj;

int *
add_6_svc(operands *argp, struct svc_req *rqstp)
{
    static int  result;
    mCalcSvr_Obj.get_calc().add(argp->num1, argp->num2, &result);
    return &result;
}

int *
sub_6_svc(operands *argp, struct svc_req *rqstp)
{
    static int  result;
    mCalcSvr_Obj.get_calc().sub(argp->num1, argp->num2, &result);
    return &result;
}

Client-server communication uses the same modeling pattern as messages by generating an abstract interface that can be elaborated on for each scoped function port.

See Also

|

Related Topics