以更健壮和类型安全的方式处理ASCII命令

时间:2019-02-05 22:20:05

标签: c++ serial-port embedded ascii

我有一个模块,该模块接收ASCII命令,然后对它们做出相应的反应。我想知道是否有可能以更健壮和类型安全的方式调用处理程序函数。

过去,我有类似以下的代码,它也与此答案非常相似:Processing ASCII commands via RS232 in embedded c

struct Command commands[] = {
{"command1", command1Handler}
{"command2", command2Handler}
 ...
};

//gets called when a new string has been received
void parseCmd(const char *input) {
//find the fitting command entry and call function pointer
}

bool command1Handler(const char *input) { }
bool command2Handler(const char *input) { }

我不喜欢所有处理程序函数都必须执行自己的解析。这似乎不必要地重复且容易出错。

这很酷,如果相反,我们可以通过以下方式实现,其中所有解析都是在parseCmd函数中完成的:

struct Command commands[] = {
{"command1", command1HandlerSafe}
{"command2", command2HandlerSafe}
 ...
};

void parseCmd(const char *input) {
//1. find fitting command entry
//2. check that parameter number fits the expected number for the target function
//3. parse parameters and validate the types
//4. call function with parameters in their correct types
}

bool command1HandlerSafe(bool param1, const char *param2) { }
bool command2HandlerSafe(int param1) {}

我认为使用旧的C型varargs可以在中央函数中进行解析,但这不会带来类型安全。

编辑: 同时,我想出了以下解决方案,我认为在某种程度上可以兼顾黑客性和模块化:

class ParameterSet{

struct Param{
  const char *paramString;
  bool isInt();
  int toInt();
  float toFloat();
  ..
}

ParameterSet(const char *input);
Param at(size_t index);
size_t length();
char m_buffer[100];
Param m_params[10];
}

bool command1HandlerMoreSafe(const ParameterSet *paramSet);

2 个答案:

答案 0 :(得分:0)

保持命令处理界面相同的一种方法是退回到main()收到的古老的 argv / argc 接口。假设收到的命令具有 words 的概念(可能是空格分隔),则可能是这样的:

  • 接收输入字符串。
  • 将输入解析为单词,其中第一个单词是命令的名称,其余单词是其参数。
  • 随着分析的进行,将一个指针指向数组中包含每个单词的字符串,并保持对数组中元素数量的计数。
  • 使用第一个单词,查找命令功能指针。如果命令在编译时都是已知的,则可以使用类似bsearch()的名称。哈希表也许也合适。无论您实现映射如何,结果都是一个指向函数的指针,该函数采用指向参数的指针数组和该指针数组中元素数量的计数。
  • 通过其指针调用命令函数,并传递已解析单词和计数的数组,就像启动代码调用main()一样。
  • 每个命令函数都可以从那里处理其参数的具体含义,并根据需要将字符串表示形式转换为内部形式。

答案 1 :(得分:0)

围绕它构建一个抽象层可能会使事情变得更复杂,从而容易发生错误。除非应该处理的命令数量庞大,需要维护,并且这是应用程序的主要任务之一,否则我不会这样做。

有了保持类型安全和使分析与算法分离的先决条件,您可以构建类似于以下类似于C的伪代码的东西:

typedef enum
{
  INT,
  STR
} type_t;                 // for marking which type that is supported by the command

typedef struct
{
  type_t type;
  const char* text;       // what kind of text that is expected in case of strings
} arg_t;

typedef struct
{
  const char* name;       // the name of the command
  arg_t*      args;       // list of allowed arguments
  size_t      args_n;     // size of that list
  void (*callback)(void); // placeholder for callback functions of different types
} command_t;

然后,您可以使回调处理程序函数与解析无关,而仅与它们的专用任务无关:

void cmd_branch (const char* str);
void cmd_kill   (int n);

命令数组可能看起来像这样:

const command_t commands[] = 
{
  { // BRANCH [FAST][SLOW]
    .name="BRANCH", 
    .args=(entry_t[]){ {STR,"FAST"}, {STR,"SLOW"} },
    .args_n=2,
    .callback= (void(*)(void)) cmd_branch,
  },

  { // KILL [7][9]
    .name="KILL",
    .args=(entry_t[]){ {INT, NULL} },
    .args_n=1,
    .callback= (void(*)(void)) cmd_kill,
  }
};

解析函数将执行以下操作:

  • 通过搜索以上列表来查找接收到的命令(如果列表较大,则搜索b)。
  • 检查接收到的命令支持哪种类型的参数
  • 相应地解析参数
  • 使用适当类型的参数调用相关函数

由于该示例仅使用了一些伪类型函数指针(void(*)(void)),因此您必须转换为正确的类型。可以通过例如C11 _Generic来完成:

call(commands[i], int_val);

扩展为:

#define call(command, arg) _Generic((arg), \
                                    int:         (void(*)(int))         command.callback, \
                                    const char*: (void(*)(const char*)) command.callback )(arg)