小机器一起工作(第1部分)

我们在做什么 幸运365官网 与其说是一个单一的整体,不如说是一些小东西的集合,这些小东西可以以各种方式配置和组合,以创造出许多奇妙的东西(或者我们希望如此?!). 这就是为什么我们把这个项目称为 机械 -它是由许多小机器一起工作.

这个架构的一个关键部分是所有这些小东西如何连接和合作. 我们希望它真的很容易插入和拔出不同的组件,甚至热重新加载他们. 在这篇文章中,我将展示这个系统是如何实现的.

找到朋友

考虑一个单独存在于自己DLL文件中的孤独的小组件. 它只知道如何做一件事,但它真的做得很好. 举个例子,它非常擅长中向外压缩. 无论如何,这个组件需要告诉系统的其他部分它的存在. 它还需要为使用其服务的其他组件公开API.

让我们先解决第二部分. 公开API最传统的方法是通过带有某些类型定义的头文件, 常数和全局函数. 例如, zlib 公开:

Int deflate(z_streamp strm, Int flush);

因为我们希望我们的系统支持动态链接,我们不能直接静态链接一个全局函数库,如 缩小(). 相反,我们需要使用函数指针,比如:

deflate_f *deflate = (deflate_f *)GetProcAddress(zlib_dll, "deflate");

处理大量这样的单独函数指针很快就会变得混乱. 为了让事情有条理, 我们将属于某个特定API的所有函数指针放入一个结构体中.

这就是我们假设的压缩组件的结构(为简单起见), 让我们假设它压缩整个缓冲区,而不是使用基于流的协议):

结构体 tm_piper_compression_i {
    Void (*compress)(uint8_t *compressed, const uint8_t *raw, 64_t raw_size);
    Void(*解压)(uint8_t *raw, const uint8_t *压缩,64_t raw_size);
};

将函数指针放在像这样的结构中有多种原因:

  1. 它清楚地标识了属于一起的函数,它们是同一个API的一部分.

  2. 它使得传递API变得更容易,我们只需要传递一个 结构体 tm_piper_compression_i * 而不是一堆单独的函数. 任何拥有指向结构体的指针的人都可以使用整个API.

  3. 它允许我们使用更短的函数名. 如果我们需要在整个应用程序中保证全局唯一的函数名,那么它们要么必须非常长( tm_piper_compression_compress),或使用某种缩写形式(tm_pc_compress). 这两种选择都不是特别吸引人. 的 结构体 变成了一种C语言的方式来获得类似于c++的API名称空间.

说到 为什么我们在api中使用C而不是c++呢?

首先,C有a 标准ABI (在实践中),而c++没有. 这意味着,为了能够链接c++对象文件,它们必须由 相同的编译器,相同版本 并使用 相同的编译标志. 这真是个大麻烦. 这意味着,如果我想做一个插件,你可以使用, 我不能只做一个DLL然后给你——我必须为你可能使用的每个编译器做一个DLL, 对于每个版本和每个配置. 对于一个灵活易用的插件系统来说,这几乎是不可能的.

我们更倾向于使用C语言来编写api,并且即使c++ ABI的标准化工作取得了成果,我们也可能会继续使用C语言,原因还有很多:

  • 简单 - C是a 比c++简单的语言. 使用C语言使我们的api更简单、更容易理解. 注意,我所说的简单并不是指容易使用——c++ api可以非常容易使用, 在很多情况下比C更容易. 但学习c++并真正理解它,比学习C要困难得多. 易于理解比易于使用更重要.

  • 较小的设计空间 - c++是一种庞大的多范式语言. 这意味着api可以用很多不同的“风格”编写. 只提一个方面,操作既可以写成类方法,也可以写成自由函数. 这种“表达能力”似乎是件好事, 但缺点是,它往往会使代码库碎片化并造成混乱, 在项目的不同部分使用不同的风格, 除非你非常小心地控制设计.

  • 更多的解耦 c++倾向于创建对象之间大量耦合的解决方案. 对象是由其他对象组成的,等等. 这并不符合我们的愿景,即许多独立的机器相互协作.

注意,我们只在接口层强制使用C. 实现可以用C或c++编写. 在接口, 简单性和一致性很重要, 因为每个界面都会被很多人使用. 实现包含了大量的代码, 因此在这里,表达式的强大功能可以用来减少代码的大小. 一个实现只需要被处理这段代码的人理解, 所以简单不 as 重要——它仍然很重要. 这就是为什么我们认为有时使用c++实现是有意义的.

当然,C和c++可以无休止地讨论. 如果我们真的想要构建一些东西,就没有时间了,让我们回到第一个问题. 一个组件如何告诉系统的其他部分它的存在?

为此,我们有一个核心组成部分,叫做 api_registry. 它只是跟踪系统中所有加载的组件/ api:

结构体 tm_api_registry_i
{
    Void (*add)(const char *的名字, Void *interf);
    空白(*删除)(void * interf);
    Void *(*first)(const char *的名字);
    void * (*) (void *上一页);
};

如您所见,每个组件都由惟一的标识符标识 const char *的名字. 该名称与API一起在头文件中定义:

#定义TM_PIPER_COMPRESSION_API_NAME“tm_piper_compression_i”

结构体 tm_piper_compression_i {
    Void (*compress)(uint8_t *compressed, const uint8_t *raw, 64_t raw_size);
    Void(*解压)(uint8_t *raw, const uint8_t *压缩,64_t raw_size);
};

因此, 头包含客户端从注册中心获取API并开始使用它所需的所有信息. 它看起来像这样:

结构体 tm_piper_compression_i *pc = (结构体 tm_piper_compression_i *)api_registry->first(TM_PIPER_COMPRESSION_API_NAME);

pc->compress(...);

在header中也有典型的其他内容, 如文档, 为了简洁,我在这里省略了.

你可以从 第()next () 函数中,同一个接口可以有多个实现. 例如,对于单元测试,我们定义一个 结构体 tm_unit_test_i API. 要运行单元测试,我们要查询所有API注册表 TM_UNIT_TEST_API_NAME 模块,在它们上运行单元测试,然后以某种有趣的方式显示结果.

请注意,API注册中心本身并没有提供任何机制来区分实现相同API的不同模块. 如果我们需要该功能,我们需要将其添加到API本身. 例如,单元测试API有一个 const char *名称() 函数,该函数返回测试的名称,可用于有选择地运行测试.

在下一篇文章中,我将展示如何用这些部件构建一个插件系统.

by Niklas灰色