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

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

本文并非标题的具体实现。而是提供一种编程方式,习惯,一种探讨。
本文有一点点门槛,有 socket,开源协议栈学习/开发经验者阅读更佳。

Overview

  • [*流畅的 C*] C语言将结构体转化为字符串
    • 思路
      • struct (packet) to string “抽象实现”
      • 如何使用
    • 全文亮点
    • Reference

[流畅的 C]C语言将结构体转化为字符串_第1张图片

思路

直接使用 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;
}

struct (packet) to string “抽象实现”

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

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

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

    int (*packet2str)(char*, struct protocol_packet*);
    int (*spacket2str)(char*, 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 成为你想要的 string,
     * 因为有些成员可能是 HEX 之类的,输出时会有问题,
     * 所以只能针对它内部的成员一个一个拷贝;
     * 因为是用来输出的,所以这个时候你可以直接添加一些字段的含义说明在 dstStr 中
     */
     unsigned char mapping_struct_Mac[16];
     unsigned char mapping_struct_IPv4Addr[64];
     /* 假设 open source 已经将 MAC 解析成了 FF0073E4FFFF 的形式*/
     memcpy(mapping_struct_Mac, recv_packet->Mac, 16);
     memcpy(mapping_struct_IPv4Addr, recv_packet->IPv4Addr, 64);

   /** 现在你可以使用 snprintf() 定制你的字符串输出了
    * 注意, MAC 不能使用 '%s' 的方式, 你得 for 循环 使用 %x(%X), 再注意,MAC 不是 16 byte 长度!
    * 为了避免误导,因为我主要的时间都在写 Python,所以C语言没有测试可能会有错误,
    * 因此下面不给实例,要你自己实现 snprintf() 的具体内容,这里是思路:
    * 比如针对 MAC:     01234    */
   snprint(dstStr, 5"MAC: ");  /* 有没有人能教我一些 C 语言里面计算偏移的技巧??? */
   snprint(&dstStr[5], 18, "%02X:%02X:%02X:%02X:%02X:%02X",
           mapping_struct_MAC[0], mapping_struct_MAC[1], mapping_struct_MAC[2],
           mapping_struct_MAC[3], mapping_struct_MAC[4], mapping_struct_MAC[5]);
   // ...do the final job by you....
    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 -网络字节码-> theFrame
       拆包func(&recv_packet, &theFrame, /*..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 -网络字节码-> theFrame
       拆包func(&recv_packet, &theFrame, /*..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!
}

全文亮点

当然,你可以定义一个全局函数,不修改 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 内部脑海里就出现黑人问号…

那么当你看到这样一个结构体:

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

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

你就知道可以这么调用:
recv_packet_p->packet2str(debug_str, recv_packet_p);
来的得到整个结构体内部变量的值是怎么样的了,然后很轻松地把它打印显示出来。

非指针是这么调用: recv_packet.packet2str(debug_str, &recv_packet) ;

于是你就可以很自然地写出这三行代码:

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

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

一般我们可能只是要个输出,那么完全可以定义一个
void (*packet_format_print)(struct protocol_packet*); 在结构体内部,
只不过这个方法可能不足够安全,因为一些系统知识有关的原因,在这个函数内部的实现输出根据不同的平台需要做一个适配的调整。比如输出到文件/syslog?输出到默认终端?输出到 console? 是指这个意思。


在初始化结构体的时候绑定函数,就是基于对象的做法,类似 C++ 有个构造函数。
struct 可以当作一个 default public 的类来使用,所以只要自己定义好了构造函数,在 C 里面也是可以很方便的直接初始化它,而不用上述显式地去绑定函数。
但是这部分说明,对于这个字符串化结构体这个主题,相关性不大。所以这里只是一个提及。

Reference

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

  1. 指针退化;
  2. C 语言的基于对象编程(编程范式)-> 使用函数指针实现对象“方法”;
  3. ACSII 码可以 %s,查看,HEX 码万万不能误用 %s 来输出;
  4. daemon 进程一般程序内部使用 printf 不会输出到终端;
  5. 一些一时说不上来的细节。

快点学 python 吧!


欢迎朋友指出上面文章内容的错误之类。
或者有更好的建议。
本人水平有限,本文仅供参考。

你可能感兴趣的:(#,LINUX:RPi,Ubuntu,etc,嵌入式,物联网:Arduino,STM32,etc,#,系统编程,#,C/C++,效率Trick,#,编程范式,设计模式,#,流畅的,C)