[流畅的 C]C语言将结构体转化为字符串

[流畅的 C] C语言将结构体转化为字符串

Overview

[流畅的 C]C语言将结构体转化为字符串

思路

直接使用 memcpy 之类的是不可以的。所以最好的做法就是定义结构体的时候就实现对字符串的转换。

就像 Python 的 __str__ 一样。
(不好意思,博主雷打不动转python!信仰上帝Python)
如果不懂 python 也没有关系,我在下面会说明 C 语言的方法。

假设你有一个 packet:

struct protocol_packet { /* 假设 open source 已经帮你把字节码翻译成 ACSII / HEX 完成*/
    unsigned char Mac[16];
    unsigned char IPv4Addr[64];
};

然后你你在一个 open source 代码中,有个函数,传递了接受过来的packet:

int
I_am_open_source_handler(struct protocol_packet recv_packet){
   // ...do things...
  return 0;
}

packet to string “抽象实现”

这个时候你想把 packet 打印出来看看里面有什么。
我们可以这么做:

免责声明,因为个人时间有限,没有对以下纯手打抽象出来的代码实验,
但是思路不会错,所以具体细节还需要看官自己微调一下。

struct protocol_packet { /* 假设 open source 已经帮你把字节码翻译成 ACSII / HEX 完成*/
    unsigned char Mac[16];
    unsigned char IPv4Addr[64];

    int (*packet2str)(*dstStr, struct protocol_packet);
    int (*spacket2str)(*dstStr, struct protocol_packet, size_t);
};

int protocol_packet2str(char dstStr[], struct protocol_packet *recv_packet){
    /** why char dstStr[]?: 这个和 char *dstStr, 是完全一样的,这些写只是为了方面了解含义。实际上它不是数组,关键词:“指针退化”
    * 我会假设你知道  *recv_packet 这么传递有助于提升运行效率,如果你对 const 熟悉,我建议“像强迫症”一样给它加上 const,这样能够说明本函数不会改变 recv_packet 的内容。
     * 你无法直接拷贝整个 struct, 所以只能针对它内部的成员一个一个拷贝,
     * 因为是用来输出的,所以这个时候你可以直接添加一些字段的含义说明在 dstStr 中
     */
     unsigned char mapping_struct_Mac[16];
     unsigned char mapping_struct_IPv4Addr[64];
     memcpy(mapping_struct_Mac, recv_packet->Mac, 16); /* 假设 open source 已经将 MAC 解析成了 FF0073E4FFFF 的形式*/
     memcpy(mapping_struct_IPv4Addr, recv_packet->IPv4Addr, 64);

   /**
    * 现在你可以使用 snprintf() 定制你的字符串输出了
    * 注意, MAC 不能使用 '%s' 的方式, 你得 for 循环 使用 %x(%X), 再注意,MAC 不是 16 byte 长度!
    * 为了避免误导,因为我主要的时间都在写 Python,所以C语言没有测试会有错误,
    * 因此下面不给实例,要你自己实现 snprintf() 的具体内容,
    * 这里是思路:
    * 比如针对 MAC: 
    *                      0123456
    *     snprint(dstStr, "MAC: 0x", 7);
    *     snprint(&dstStr[7], "%X", mapping_struct_MAC[0]) /* for 循环 12 次 */
    */
   // ...do the final job....
    return 0;
}

int sprotocol_packet2str(char dstStr[], struct protocol_packet recv_packet size_t length_of_dstStr){
    /**  你可以实现一个 safe 版本,比起上面,只是多了一个 dstStr 的长度检查;    */
    return 0;
}

如何使用

这里有一处必须要注意的地方:

因为我们上面把 struct 里面的内容改了,增加了一个转化为 string 的函数指针(还有一个 option 的 safe 方法),
但是如果不给这个指针赋值,它是 NULL 的,不能乱用。
所以,使用的时候,找到 code 里面初始化 recv_packet 的地方,我假设原本 code 是这样的:

int
main(...你懂的...){
   struct protocol_packet recv_packet;
   
   // make me as daemon, come on!
   
   while(1) { /* loop until universe collapses */
       // 收到 frame
       拆包func(&recv_packet, ...other if needed...);
       
       I_am_open_source_handler(&recv_packet);
		
		// ..other if needed...
   }

   return 0;
}

那么这个位置我们要轻轻一改:

int
main(...你懂的...){
   struct protocol_packet recv_packet;
   
   // make me as daemon, come on!
   
   while(1) { /* loop until universe collapses */
       // 收到 frame
       拆包func(&recv_packet, ...other if needed...);
      
       recv_packet.packet2str = protocol_packet2str;  // LOOK AT ME !!!!!!!!
      
       I_am_open_source_handler(&recv_packet);
		
		// ..other if needed...
   }
   return 0;
}

终于到了最后:
这样你就可以在任何嵌套在 I_am_open_source_handler() 里面很深层次的函数里面这么使用了:

int
I_am_open_source_handler(struct protocol_packet recv_packet){
   // ...do things...
   switch([...]){
   case [...]:
       [...]
   case [...]:
       i_am_a_func_in_the_HANDLER(&recv_packet);
       [...]
       break;
   }
  return 0;
}

int
i_am_a_func_in_the_HANDLER(struct protocol_packet *recv_packet_p){
   // 我想要在这里知道 recv_packet 内部具体有哪些值,
   // OK, that's do it:
   char my_add_debug_str[2048] = {'\0'}; /* 长一点总不会坏事吧 */
   recv_packet_p->packet2str(my_add_debug_str, recv_packet_p); /* 注意,这里 recv_packet_p 是指针类型哦 */
   printf("%s", my_add_debug_str);  /* 看,这样就可以愉快地输出了 */

  /* 当我知道了 protocol 里面有哪些内容之后,
   * 我可能会在下面的 source code 前面的现在这里,
   * 添加一些自己的代码,做一些检查,定制化等等。
   */

   // ..do magic things...
   // BUT, I don't care this part code!
}

Summary

当然,你可以定义一个全局函数,不修改 struct protocol_packet 里面的内容,全局函数就跟 protocol_packet2str() 里面一样写就可以了。然后需要的地方,直接调用这个全局函数。

但是这样写还要知道这个函数不是吗?
在产品代码里面,这个函数一般都不会出现在像上面的 i_am_a_func_in_the_HANDLER() 这样的具体行为的代码里面,但是,如果定义了这样一个自动转化的指针,那么每个结构体初始化的位置: struct protocol_packet recv_packet; 你都可以给它绑定函数:
recv_packet.packet2str = protocol_packet2str; // LOOK AT ME !!!!!!!!

这个绑定语句可以紧跟在初始化位置后面,不用在上面的 while 循环之类的内部绑定即可。上面是为了方面理解。

这句代码可以留着,因为给结构体的一个成员绑定一个地址并不会消耗什么资源,并且它不会有任何输出;当另外一个人,完全没有接触过这个代码,或者说,你自己几个月之后又要跟踪 packet 的状态…
那么当你看到这样一个结构体:

struct protocol_packet { /* 假设 open source 已经帮你把字节码翻译成 ACSII / HEX 完成*/
   unsigned char Mac[16];
   unsigned char IPv4Addr[64];

   int (*packet2str)(*dstStr, struct protocol_packet);
   int (*spacket2str)(*dstStr, struct protocol_packet, size_t);
};

你就知道可以这么调用: recv_packet_p->packet2str(debug_str, recv_packet_p);
于是你就可以很自然地写出这三行代码:

  char debug_str[2048] = {'\0'};
  recv_packet_p->packet2str(debug_str, recv_packet_p); 
  printf("%s", debug_str);

从此观察结构体内部状态就变成了一个很轻松地事情了。

Reference

实际上这里没有啥 reference, 但是如果你觉的上面的内容有用,有些内容不理解?,想要多了解一下?
那么这里有一些是上面内容使用到的知识点:

  1. 指针退化
  2. C 语言的基于对象编程(编程范式)
  3. 一些一时说不上来的细节

快点学 python 吧!