Есть ли способ «синхронизировать» идентификаторы в перечислении и именах функций, вызываемых массивом указателей функций?

Я пишу код C на STM32 (в частности, STM32756-EVAL), и я создал перечисление, которое считывает входящий массив символов и присваивает ему перечисление. Затем значение этого перечисления помещается в качестве индекса для массива указателей функций.

Причина, по которой у меня есть этот код, заключается в том, чтобы иметь возможность решить, какую функцию вызывать на основе полученного массива символов, не полагаясь на гигантский стек if-else, считывающий массивы символов один за другим.

Соответствующий код:

enum cmd cmd_Converter(unsigned char* inputCmd){//Converts the input cmd from uint8_t to an enum.
    switch (inputCmd[0]){   //Currently we are using a switch-case. I expect this list to expand to something like 50.
    case 'F':
        if (memcmp(inputCmd, "FIRMV", COMMAND_LENGTH) == 0) return FIRMV;
        else return INVAL;
        break;
    case 'V':
        if (memcmp(inputCmd, "VALCN", COMMAND_LENGTH) == 0) return VALCN;
        else return INVAL;
        break;
    default:
        return INVAL;
    }
}

void process_Message(uint8_t* message, uint16_t Len){
    unsigned char inputCmd[COMMAND_LENGTH];
    unsigned char inputData[DATA_LENGTH];   
    unsigned char outputCmd[COMMAND_LENGTH];
    unsigned char outputData[DATA_LENGTH];

    //Function that separates message, inputCmd, and inputMessage.
    memcpy((char*) inputCmd, (const char*)message + COMMAND_CHAR, COMMAND_LENGTH);
    memcpy((char*) inputData, (const char*)message + DATA_CHAR, DATA_LENGTH);
    enum cmd enumCmd = cmd_Converter(inputCmd);
    void (*cmd_Function_Pointer[])(unsigned char* inputData) = {FIRMV_F, VALCN_F, INVAL_F}; //Is this even needed?
    (*cmd_Function_Pointer[enumCmd])(inputData);

//  message_Received(message, Len);
//  send_Message(outputCmd, outputData);
}

void FIRMV_F(unsigned char *inputData){
    //Do thing
}

void VALCN_F(unsigned char *inputData){
    //Do thing
}

void INVAL_F(unsigned char *inputData){
    //Do thing
}

Перечисление предназначено для улучшения читаемости кода, так что любой, кто читает код, может увидеть перечисление и указатель на функцию и сказать: «enum FIRMV вызовет FIRMV_F из (*cmd_Function_Pointer[enumCmd])(inputData)». Одна из выявленных мной слабых сторон заключается в том, что он зависит от идентичности последовательности enum cmd и cmd_Function_Pointer[], и если список перечислений станет слишком длинным, поддерживать эту идентичную последовательность будет сложно.

Мне интересно, есть ли в C какие-либо методы, которые позволили бы «синхронизировать» идентификаторы в перечислении и именах функций, вызываемых указателем функции?

Полный код: usbd_cdc_if.c

/**
  * @brief  Data received over USB OUT endpoint are sent over CDC interface
  *         through this function.
  *
  *         @note
  *         This function will issue a NAK packet on any OUT packet received on
  *         USB endpoint until exiting this function. If you exit this function
  *         before transfer is complete on CDC interface (ie. using DMA controller)
  *         it will result in receiving more data while previous ones are still
  *         not sent.
  *
  * @param  Buf: Buffer of data to be received
  * @param  Len: Number of data received (in bytes)
  * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
  */
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);

  memset (buffer, '\0', 64);  // clear the buffer
  uint8_t len = (uint8_t)*Len;  //Converts Len as uint32_t to len as uint8_t
  memcpy(buffer, Buf, len);  // copy the data to the buffer
  memset(Buf, '\0', len);   // clear the Buf also
  //Code used to send message back

  process_Message(buffer, len);
  return (USBD_OK);
  /* USER CODE END 6 */
}

/**
  * @brief  CDC_Transmit_FS
  *         Data to send over USB IN endpoint are sent over CDC interface
  *         through this function.
  *         @note
  *
  *
  * @param  Buf: Buffer of data to be sent
  * @param  Len: Number of data to be sent (in bytes)
  * @retval USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY
  */
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
  uint8_t result = USBD_OK;
  /* USER CODE BEGIN 7 */
  USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
  if (hcdc->TxState != 0){  //If TxState in hcdc is not 0, return USBD_BUSY.
    return USBD_BUSY;
  }
  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);    //SetTxBuffer sets the size of the buffer, as well as the buffer itself.
  result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);  //USBD_CDC_TransmitPacket(&hUsbDeviceFS) transmits
  /* USER CODE END 7 */
  return result;
}

messageprocesser.c

#include "messageprocesser.h"
#include "main.h"
#include "usbd_cdc_if.h"
#include <string.h>
#include "string.h"

//Sample cmd: TOARM_FIRMV_00000000_4C\r\n
#define MESSAGE_LENGTH 25
#define COMMAND_CHAR 6  //See SW Protocol or sample cmd
#define COMMAND_LENGTH 5
#define DATA_CHAR 12
#define DATA_LENGTH 8
#define CHECKSUM_CHAR 21
#define CHECKSUM_LENGTH 2

enum cmd {FIRMV, VALCN, INVAL};

enum cmd cmd_Converter(unsigned char* inputCmd){//Converts the input cmd from uint8_t to an enum.
    switch (inputCmd[0]){
    case 'F':
        if (memcmp(inputCmd, "FIRMV", COMMAND_LENGTH) == 0) return FIRMV;
        else return INVAL;
        break;
    case 'V':
        if (memcmp(inputCmd, "VALCN", COMMAND_LENGTH) == 0) return VALCN;
        else return INVAL;
        break;
    default:
        return INVAL;
    }
}

void process_Message(uint8_t* message, uint16_t Len){
    //HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
    unsigned char inputCmd[COMMAND_LENGTH];     //These are not null-terminated strings.
    unsigned char inputData[DATA_LENGTH];           //These are just an array of chars.
    unsigned char outputCmd[COMMAND_LENGTH];
    unsigned char outputData[DATA_LENGTH];

    //Function that separates message, inputCmd, and inputMessage.
    memcpy((char*) inputCmd, (const char*)message + COMMAND_CHAR, COMMAND_LENGTH);
    memcpy((char*) inputData, (const char*)message + DATA_CHAR, DATA_LENGTH);
    enum cmd enumCmd = cmd_Converter(inputCmd);
    void (*cmd_Function_Pointer[])(unsigned char* inputData) = {FIRMV_F, VALCN_F, INVAL_F};
    (*cmd_Function_Pointer[enumCmd])(inputData);
}

void FIRMV_F(unsigned char *inputData){
    //HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
    unsigned char outputCmd[COMMAND_LENGTH];
    unsigned char outputData[DATA_LENGTH];
    memcpy(outputCmd, "FIRMV", COMMAND_LENGTH);
    memcpy(outputData, "01050A00", DATA_LENGTH);
    send_Message(outputCmd, outputData);
}

void VALCN_F(unsigned char *inputData){
    //HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
    unsigned char outputCmd[COMMAND_LENGTH];
    unsigned char outputData[DATA_LENGTH];
    memcpy(outputCmd, "VALCN", COMMAND_LENGTH);
    memcpy(outputData, "00000000", DATA_LENGTH);
    send_Message(outputCmd, outputData);
}

void INVAL_F(unsigned char *inputData){
    HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
    unsigned char outputCmd[COMMAND_LENGTH];
    unsigned char outputData[DATA_LENGTH];
    memcpy(outputCmd, "REEEE", COMMAND_LENGTH);
    memcpy(outputData, "99999999", DATA_LENGTH);
    send_Message(outputCmd, outputData);
}

void send_Message(uint8_t* cmd, uint8_t* data){
    uint8_t outputMessage[MESSAGE_LENGTH] = "TOWST_";
    memcpy((char*) outputMessage + COMMAND_CHAR, (const char*) cmd, COMMAND_LENGTH);
    outputMessage[COMMAND_CHAR + COMMAND_LENGTH] = '_';
    memcpy((char*) outputMessage + DATA_CHAR, (const char*) data, DATA_LENGTH);
    outputMessage[DATA_CHAR + DATA_LENGTH] = '_';

    //Deal with checksum
    int outputCheckSum = checkSum_Generator(outputMessage);
    char outputCheckSumHex[2] = {'0', '0'};
    itoa (outputCheckSum, outputCheckSumHex, 16);
    if (outputCheckSum < 16) { //Adds a 0 if CS has fewer than 2 numbers
        outputCheckSumHex[1] = outputCheckSumHex[0];
        outputCheckSumHex[0] = '0';
    }
    outputCheckSumHex[0] = toupper (outputCheckSumHex[0]);
    outputCheckSumHex[1] = toupper (outputCheckSumHex[1]);

    memcpy((char*) outputMessage + CHECKSUM_CHAR, (const char*) outputCheckSumHex, CHECKSUM_LENGTH);
    outputMessage[23] = '\r';
    outputMessage[24] = '\n';

    //return a processed message array

    CDC_Transmit_FS(outputMessage, sizeof(outputMessage));
}

int checkSum_Generator(uint8_t* message){
      int checkSum = 0;
      for (int i = 0; i < CHECKSUM_CHAR; i++){  //Gives the cs of TOARM_COMND_DATA0000_.
          checkSum ^= message[i];
      }
      return checkSum;
}

Попытки решения этого вопроса

Еще один вопрос, связанный с «связыванием указателей функций и перечисления», который я изучил, но предложенное решение, похоже, не решает проблему, о которой я упоминал, а только обходит за счет меньшего кода.

До сих пор я пытался изменить имена функций, чтобы они были идентичны их аналогам перечисления, переименовав FIRMV_F() в FIRMV(). Неудивительно, что я получил это:

../Core/Src/messageprocesser.c:59:6: error: 'FIRMV' redeclared as different kind of symbol

Я также попытался присвоить массив указателей функций аналогично обычным массивам:

void process_Message(uint8_t* message, uint16_t Len){
    //HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
    unsigned char inputCmd[COMMAND_LENGTH];     //These are not null-terminated strings.
    unsigned char inputData[DATA_LENGTH];   //These are just an array of chars.
    unsigned char outputCmd[COMMAND_LENGTH];
    unsigned char outputData[DATA_LENGTH];

    //Function that separates message, inputCmd, and inputMessage.
    memcpy((char*) inputCmd, (const char*)message + COMMAND_CHAR, COMMAND_LENGTH);
    memcpy((char*) inputData, (const char*)message + DATA_CHAR, DATA_LENGTH);
    enum cmd enumCmd = cmd_Converter(inputCmd);
    void (*cmd_Function_Pointer[INVAL + 1])(unsigned char* inputData);
    (*cmd_Function_Pointer[FIRMV]) = FIRMV_F;
    (*cmd_Function_Pointer[VALCN]) = VALCN_F;
    (*cmd_Function_Pointer[INVAL]) = INVAL_F;
    (*cmd_Function_Pointer[enumCmd])(inputData);    //What is going on here?
}

Я получил следующие ошибки.

../Core/Src/messageprocesser.c:52:33: error: lvalue required as left operand of assignment
   52 |  (*cmd_Function_Pointer[FIRMV]) = FIRMV_F;
      |                                 ^
../Core/Src/messageprocesser.c:53:33: error: lvalue required as left operand of assignment
   53 |  (*cmd_Function_Pointer[VALCN]) = VALCN_F;
      |                                 ^
../Core/Src/messageprocesser.c:54:33: error: lvalue required as left operand of assignment
   54 |  (*cmd_Function_Pointer[INVAL]) = INVAL_F;

Это имеет смысл, так как я понимаю, что функции FIRMV_F не имеют lvalue, но я не знаю, как это исправить, предполагая, что это возможно.

Пожалуйста, дайте мне знать, если требуется больше деталей или ясности.

Для менее решительных читателей: использование enum в качестве индекса в массиве указателей функций для удобства чтения. В текущем коде последовательность перечисления должна быть идентична последовательности указателя функции. Это кажется уязвимым. Хотите, чтобы последовательность перечисления оставалась идентичной последовательности указателя функции.

У вас есть двухэтапный процесс, который преобразует строку в перечисление, а затем перечисление в указатель на функцию. Было бы проще просто преобразовать строку в указатель функции (в cmd_Converter()). Просто идея.

pmacfarlane 09.02.2023 12:22

Моей первой попыткой было использовать гигантский стек if-else, преобразующий 50 строк в функции. Это выглядело довольно уродливо и яндре-деви, поэтому я поговорил с стековой биржей разработки программного обеспечения, которая предложила switch-case. Чтобы s-c работал, мне нужны были перечисления. Конечно, если есть строка для указателя на функцию, которая не является гигантским стеком if-else, я был бы рад услышать об этом.

HFOrangefish 09.02.2023 12:26

Как насчет подхода X Macro в последних ответах на вопрос, который вы связали?

nielsen 09.02.2023 12:27
(*cmd_Function_Pointer[FIRMV]) должно быть cmd_Function_Pointer[FIRMV]
user253751 09.02.2023 12:54

Макросы @nielsen X должны быть последним средством. Очень полезно при сопровождении старого хлама, в котором мало что можно изменить, но я не рекомендую использовать их в новых программах. Тем не менее, для полноты картины я разместил здесь версию своего ответа для X-макроса.

Lundin 09.02.2023 12:55

Вы также можете написать ... = {[FIRMV] = FIRMV_F, [VALCN] = VALCN_F, ...}

user253751 09.02.2023 12:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
86
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы можете сделать так, чтобы ваша функция cmd_Converter() просто возвращала требуемый указатель функции. Вы по-прежнему можете использовать оператор switch, чтобы избавиться от необходимости иметь длинную цепочку if...else if.... Таким образом, вам не нужны ни перечисления, ни массивы указателей на функции, и нечего синхронизировать.

#include <string.h>
#include <stdlib.h>

#define COMMAND_LENGTH 8

typedef void (*handler_t)(const unsigned char *arg);

void FIRMV_F(const unsigned char *inputData);

handler_t cmd_Converter(const unsigned char *inputCmd)
{
    handler_t result = NULL;

    switch (inputCmd[0])
    {
        case 'F':
            if (memcmp(inputCmd, "FIRMV", COMMAND_LENGTH) == 0)
                result = FIRMV_F;
            break;

        // more cases here.

        default:
            break;
    }

    return result;
}

Редактировать: Если вам нужен чисто управляемый данными подход, который полностью избегает оператора switch, вы можете иметь массив структур, отображающих имена команд на указатели функций. Это по-прежнему устраняет необходимость в перечислениях — отображение явное:

#include <string.h>
#include <stdlib.h>

#define COMMAND_LENGTH 8

typedef void (*handler_t)(const unsigned char *arg);

typedef struct
{
    const char *name;
    handler_t fn_p;
} cmd_t;

void FIRMV_F(const unsigned char *inputData);
void VALCN_F(const unsigned char *inputData);

static const cmd_t commands[] =
{
    {"FIRMV", FIRMV_F},
    {"VALCN", VALCN_F}
};

handler_t cmd_Converter(const unsigned char *inputCmd)
{
    handler_t result = NULL;
    
    for (size_t i = 0; i < sizeof commands / sizeof commands[0]; ++i)
    {
        if (strcmp((const char *)inputCmd, commands[i].name) == 0)
        {
            result = commands[0].fn_p;
            break;
        }
    }

    return result;
}

Для этого используется линейный поиск. Если у вас не так много команд, этого может быть достаточно. Если их много, вы можете отсортировать массив и выполнить двоичный поиск.

Тем не менее, вам все равно придется поддерживать большой старый switch со всеми ветвлениями, которые он генерирует. Создание пар перечисление/функция — довольно разумный и полезный дизайн для многих сценариев, например, при создании конечных автоматов.

Lundin 09.02.2023 12:48

@ Лундин Согласен. Если бы я делал это, у меня был бы массив структур, содержащих имя команды и указатель функции, и поиск по нему. Но я отвечал на заданный вопрос.

pmacfarlane 09.02.2023 12:54

@Lundin Обновлено, чтобы добавить версию, управляемую данными, без переключателя.

pmacfarlane 09.02.2023 13:07

Structs — это нормальное решение, оно удобочитаемо и имеет хороший дизайн, но преимущество наличия таблицы указателей enum и функций заключается в том, что вы получаете прямой доступ. Никаких циклов, никаких переключателей, никакого поиска или strcmp, только просмотр таблицы. Что касается вашего примера здесь, его нужно переписать, используя bsearch, когда общее количество элементов составляет около 50+.

Lundin 09.02.2023 14:49

@Lundin Если они изменят свой дизайн, чтобы получать номер через USB, это сработает. Как бы то ни было, они получают строку через USB. Где-то должен быть strcmp() или что-то подобное.

pmacfarlane 09.02.2023 14:55
Ответ принят как подходящий

Существует распространенная рекомендуемая практика, предполагающая, что значения перечисления просто последовательны. Сделайте перечисление следующим образом:

typedef enum
{
  INVAL,
  FIRMV,
  VALCN,
  CMD_N // number of items in the enum
} cmd_t;

И такой шаблон функции:

typedef void cmd_func_t (unsigned char *inputData);

Затем вы можете создать массив указателей на функции, где каждый индекс соответствует соответствующему перечислению, используя назначенные инициализаторы:

static cmd_func_t* const cmd_list[] =   // size not specified on purpose
{
  [INVAL] = INVAL_F,
  [FIRMV] = FIRMV_F,
  [VALCN] = VALCN_F,
};

Проверьте целостность с помощью:

_Static_assert(sizeof cmd_list/sizeof cmd_list[0] == CMD_N,
               "cmd_list has wrong size compared to cmd_t");

Использование вызова функции, например:

cmd_list[FIRMV](param);

Кроме того, просто для полноты картины мы можем полностью отказаться от «не повторяться» и сгенерировать многое из этого с помощью X-макросов: https://godbolt.org/z/zY1nh5M5T. На самом деле не рекомендуется, так как это делает код неясным, но довольно мощным. Например, такие строки, как "FIRMV", могут генерироваться во время компиляции, как показано в этом примере.

Хотя X-макросы в этом случае излишни, есть вопрос масштаба. Если бы мы говорили о 50+ элементах, я бы рассмотрел подход X-macro и написал объяснение в коде. Кстати, _Static_assert не нужен в версии X-macro.

nielsen 09.02.2023 13:38

Другие вопросы по теме