这个是fork自原版的KCP仓库的一个分支,对KCP源代码的某些东西作出了一定改进。但是修改后的代码只能支持C99及更新的C标准,会丧失一些远古平台的兼容性。
本仓库依旧保留原版KCP的license以及开源协议,允许进行二次开发。
Release中包含了几个常用系统的静态链接库可供使用,不想手动编译的话可以直接下载使用。
本仓库大量修改了KCP原版的头文件以及某些宏定义,依赖那些宏定义的程序可能会出现异常。
(个人愚见某些KCP的代码风格和设计似乎不是很符合我们工程上的一些做法,所以作出了一些改进)
主要的修改点有四处,分别是关于整型的定义、队列宏以及一些内存对齐的处理问题。下方是每项修改的详细说明。
ikcp.h文件中含有大量的冗余的整型类型的定义,例如:
typedef unsigned int ISTDUINT32;
typedef int ISTDINT32;
……我理解原版代码那么处理的原因,是为了考虑各种不同平台的编译器实现的问题(在C99之前)。但如今的话,绝大部分的编译器应该都已经已经支持C99标准了,因此直接使用stdint.h可以完全替代掉这里的冗余代码。
因此,代码中那些对数据长度有要求的地方,已经通通被uint32_t,int32_t,uint16_t,uint8_t替代了。
虽然在古早时期的C代码里面,char被定义为最小可寻址单元,甚至很多标准库里面的函数也是用char来指代“字节”这个概念的。
但是,现代的代码里面,一个字节就应该是严格等价于uint8_t或者unsigned char,因为绝大多数的编程语言都在默认这一行为。
因此某些kcp的函数,尤其是ikcp_recv之类的函数,它接受的参数是const char*,这个就会某种程度上带来FFI调用的一些困难以及语义上的混淆。
所以,我们理应该把这些代码优化一下让它更加规范。
KCP自己实现了一个非常简易的队列,但是这个队列几乎全部是靠宏来实现的,可读性非常差。虽然宏可以有相当方便的内联(有少量性能提升)以及某些花活。但对于现代的编译器来说,用宏强行内联可能还不如写个inline交给编译器来自行决定。对于个人而言,为了些许的性能提升而牺牲掉可读性,往往是不可取的(个人意见而已,除非某些性能极其关键的代码)
这个里面只有一个iqueue_entry方法是无法用宏之外的方法实现的,但是恰好KCP的源代码中,只有拿取iqueue_entry(ptr, IKCPSEG, node);这样一种调用,那么我们就考虑把这个宏变成一个写死的方法就可以了。
static inline struct IKCPSEG *iqueue_entry_from_node(struct IQUEUEHEAD *ptr)
{
// 假设ptr为0的时候,node的地址是多少,实际上就相当于node本身在结构体内的偏移量
struct IQUEUEHEAD *node_addr = &((struct IKCPSEG *)0)->node;
uintptr_t offset_of_node = (uintptr_t)node_addr;
// 减去这个偏移量就能拿到这个结构体的首地址了
uintptr_t ptr_addr = (uintptr_t)ptr;
return (struct IKCPSEG *)(ptr_addr - offset_of_node);
}改造完之后,这个iqueue相关的代码可读性就大大加强了,而且性能也不会有很大的损失。(我们这里用的是static inline方式进行内联,因此大概率这些函数会成功被编译器内联,所以性能实际上跟宏差不多)
ikcp.c中有若干个辅助函数,例如ikcp_encode32u这种,它用到的宏定义里面有一个IWORDS_MUST_ALIGN,也就是说在一些要求内存对齐的平台上,它会强制要求逐字节拷贝而不是memcpy。
说实话这个操作我没有特别理解为什么要这么做,也欢迎原作者来打我的脸。我觉得这里原作者可能是存在了一点误解的。正好笔者的电脑是Apple M2 Max,它正好就是一个要求内存对齐访问的平台(除非你强行关掉它),不妨让我们来看看到底什么情况下内存对齐会影响代码的执行:
我们先来做一个错误示例:
uint8_t* data = xxxxx; // 从某个地方弄来的一段数据,uint8_t本身不会有任何的内存对齐要求
// 我们故意让data + 1,刻意构造一个不对齐的内存出来(当然这个也不一定就不对齐,多试几次总能搞出来)
uint32_t* u32_ptr = (uint32_t*)(data + 1);
// 此时给这个u32指针解引用,就会触发crash
// Apple Silicon上面这个是必崩的,因为它硬件是要求内存对齐访问的
uint32_t u32 = *u32_ptr;我们对一个非对齐的地址做解引用操作的时候,才触发了异常。那么,根据我们查资料的情况来看,所谓的内存对齐访问,仅仅只是在你要求访问多大的内存区域的时候,会对它有要求而已。
换句大白话来说,就是你访问一个4字节的空间,你就必须4整数倍对齐,8字节的空间,就必须8倍对齐。其它的情况,都是不会有要求的。我们不妨继续写一个故意的C代码来看看情况:
// 在苹果芯片上,这个会被存成小端序,也就是78 56 34 12
uint32_t u32 = 0x12345678;
// 我们故意把这个内存地址强行解释为一个uint8_t
uint8_t* p = (uint8_t*)(&u32);
uint8_t buf[64];
// 然后我故意只想拿出来u32的中间两个字节,故意搞一个不对齐的内存访问
memcpy(buf, p+1, 2);事实上,上面的代码是不会有任何问题的,而且可以正常拿到0x56和0x34。那么这就更加说明了这个内存对齐访问仅限于“你在要求访问多大的内存空间时,要求对齐到此空间的整数倍”,而且也只限于2,4,8,毕竟暂时没有更大的基础类型了。
所以实际上来说,KCP原本的代码考虑这个内存对齐访问的问题,压根就不必要,只要是小端序的情况下,直接用memcpy就是正确的选择。