SOUI官方论坛

 找回密码
 立即注册
查看: 968|回复: 9

【库制作】如何写一个好的库

[复制链接]

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
发表于 2020-4-19 21:20:08 | 显示全部楼层 |阅读模式
本帖最后由 darkannie 于 2020-4-19 22:26 编辑

之前的一个开源工程poco上的blog看到的。
原文地址:https://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/
简单点将就是如何写一个工程级的库的,有时间再来翻译,先占位置再说。

翻译:
注意:我本人没啥精力去翻译这个blog,大部分是我通过百度翻译进行翻译的,然后简单校对下。
正文:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
我痴迷于漂亮的API。然而,不仅仅是api,还要使使用库的总体体验尽可能好。对于Python来说,到目前为止已经有很多最佳实践了,但是感觉关于如何正确构造本机库的信息并不多。
我说的本地库是什么意思?本质上是dylib/DLL/so。
因为我现在在C和C++上花费的时间比Python工作时间多,所以我觉得我可以抓住这个机会,收集我的想法,看看如何编写合适的共享库,而不会打扰你的用户。

静态库或者动态库?

这篇文章几乎完全假设您正在构建一个DLL或共享库,而不是静态链接的东西。虽然听起来静态和动态链接的库本质上是相同的,唯一的区别是如何链接到它,但它还有很多。
通过动态链接库,您可以更好地控制符号。动态链接库在不同的编程语言之间也能更好地工作。
没有什么阻止你在C++中编写一个库,然后在Python中使用它。事实上,这正是我建议对此类库进行单元测试的方式。以后再说。

采用哪种语言编写?
所以你想写一个库,编译成一个DLL或类似的东西,它应该有点独立于平台。
你能在那里使用哪种语言?现在你可以在C和C++之间选择,很快你也可以添加Rust到这个列表中。
为什么不是其他语言?
C很简单:因为这是唯一一种真正定义某种稳定ABI的语言。严格地说,定义它的不是语言,而是操作系统,但在某种程度上,C是库的首选语言,C调用约定是共享库的通用语言。

“C所做过的最伟大的尝试是让全世界相信它没有运行时”。我不确定我是从哪里听到这句话的,但当谈到库时,这是非常恰当的。
从本质上讲,C是如此的普遍,以至于所有的东西都可以假定一些基本功能是由C标准库提供的。这是每个人都同意的一件事。
对于C++,情况更为复杂。C++需要一组额外的功能,而这些功能不是由C标准库提供的。它主要需要对异常处理的支持。
然而,C++很好地退化到C调用约定,所以在它里面仍然很容易编写库,这完全隐藏了C++背后的事实。

但对于其他语言来说却不那么容易。例如,为什么在Go中编写一个库不是一个好主意?原因是Go-for需要大量的运行时来进行垃圾收集,并为其协程提供调度程序。
Rust已经接近于除了C标准库之外没有任何运行时需求,这将使在其中编写库成为可能。

然而,C++最有可能是你想使用的语言。为什么不是C?原因是微软的C编译器在接收语言更新方面非常糟糕,你会被C89困住。
很明显,你可以在Windows上使用别的编译器,但是如果你的库的用户想自己编译的话,这会给他们带来很多问题。
使用不是特别操作系统相关的工具链,可能会让你的库不太受欢迎(原文: Requiring a tool chain that is not native to the operating system is an easy way to alienate your developer au***nce.)

然而,我通常建议C++的一个非常类似的子集:不要使用异常,不要使用RTTI,不要建立疯狂的构造函数。
文章的其余部分假定C++确实是首选语言。


公共头文件



理想情况下,您正在构建的库应该只有一个公共头文件。在内部发疯,创建尽可能多的头文件,只要你想。
您希望存在一个公共头文件,即使您认为您的库只会链接到非C的内容。例如,Python的CFFI库可以解析头文件并从中构建绑定。所有语言的人都知道header是如何工作的,他们会查看它们来构建自己的绑定。
标题中有哪些规则可遵循?

文件保护头

其他人使用的每个公共头都应该有足够独特的头保护,以确保可以安全地多次包含它们。
不要对警卫太有创意,也不要对他们太普通。如果头部顶部有一个超级通用的include-guard(就像UTILS-H和其他什么都没有),
那就不好玩了。您还需要确保C++中有外部的“C”标记。
这将是您的最小头文件:

  1. #ifndef YOURLIB_H_INCLUDED
  2. #define YOURLIB_H_INCLUDED
  3. #ifdef __cplusplus
  4. extern "C" {
  5. #endif

  6. /* code goes here */

  7. #ifdef __cplusplus
  8. }
  9. #endif
  10. #endif
复制代码
专家的头文件:



因为您自己可能也会包含头文件,所以您需要确保定义了用于导出函数的宏。
这在Windows上是必要的,在其他平台上也是一个非常好的主意。
实际上,它可以用来改变符号的可见性。我稍后再讨论,暂时只需添加如下内容:



  1. #ifndef YL_API
  2. #  ifdef _WIN32
  3. #     if defined(YL_BUILD_SHARED) /* build dll */
  4. #         define YL_API __declspec(dllexport)
  5. #     elif !defined(YL_BUILD_STATIC) /* use dll */
  6. #         define YL_API __declspec(dllimport)
  7. #     else /* static library */
  8. #         define YL_API
  9. #     endif
  10. #  else
  11. #     if __GNUC__ >= 4
  12. #         define YL_API __attribute__((visibility("default")))
  13. #     else
  14. #         define YL_API
  15. #     endif
  16. #  endif
  17. #endif
复制代码

在Windows上,它会根据设置的标志,为dll设置适当的YL_API(这里我使用YL作为“Your Library”的缩写,选择适合您的前缀)。
任何人在没有任何花哨动作的情况下头球都会自动得到(dllimport)。这在Windows上是一个非常好的默认行为。
对于其他平台,除非使用最近的GCC/clang版本,否则不会设置任何内容,在这种情况下,将添加默认的可见性标记。
如您所见,可以定义一些宏来更改执行的分支。例如,当您构建库时,您将告诉编译器还定义了YL_build_SHARED。


在Windows上,DLLs的默认行为始终是:除非用__declspec(dllexport)标记,否则所有符号都不会默认导出
不幸的是,在其他平台上,行为总是导出所有内容。有多种方法可以解决这个问题,一种是GCC 4的可见性控制。
这行得通,但还有一些额外的事情需要考虑。

首先,源代码内的可见性控制不是银弹。首先,除非使用-fvisibility=hidden编译库,否则标记将不起任何作用。
然而,比这更重要的是,这只会影响您自己的库。如果静态地链接库中的任何内容,则该库可能会公开不希望公开的符号。
例如,假设您编写的库依赖于要静态链接的另一个库。除非您阻止,否则此库的符号也将从库中导出。

这在不同平台上的工作方式不同。在Linux上,可以将--exclude libs ALL传递给ld,链接器将自动删除这些符号。
在OSX上,这是个骗局,因为链接器中没有这样的功能。最简单的解决方案是对所有函数都有一个公共前缀。
例如,如果您的所有函数都以yl__开头,那么很容易告诉链接器隐藏所有其他内容。
通过创建符号文件,然后使用-exported_symbols_list symbols.txt将链接器指向该文件,可以完成此操作。
此文件的内容可以是单行。我们可以忽略的窗口,因为dll需要显式的导出标记。


Careful with Includes and Defines
有一点需要注意,你的标题不应该包含太多的内容。一般来说,我认为头部包含stdint.h之类的内容来获得一些常见的整数类型是可以的。
然而,你不应该做的是自作聪明和定义自己的类型。
例如,由于msgpack缺少stdint.h头,所以它有一个"聪明" 的想法,可以为Visual Studio 2008定义int32_t和其他一些类型。
这是有问题的,因为只有一个库可以定义这些类型。相反,更好的解决方案是要求用户为旧的Visual Studio版本提供替换stdint.h头文件。

尤其不要在库头中包含windows.h。这个标题包含了太多的内容,
以至于微软增加了额外的定义以使它更精简(WINDOWS_LEAN_AND_MEAN,WINDOWS_EXTRA_LEAN、NOMINMAX)。
如果需要包含windows.h,请使用仅包含在.cpp文件中的专用头文件(译者注:如果一个头文件可能被用户直接include,那么不要在里面包含windows.h这个文件)。








该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 22:27:01 | 显示全部楼层

Stable ABI

不要将任何结构放入公共头中,除非您100%确定永远不会更改它们。
如果确实要公开结构,并且以后确实要添加额外的成员,请确保用户不必分配(allocated)该头。
如果用户确实需要分配(allocated)该头,请将版本或大小信息作为第一个成员添加到结构中。
(译者注:改变已经存在的结构体,可能导致用户的程序发送故障,如果用户不编译就直接升级)

微软通常会将结构的大小放入结构中,以允许以后添加成员,但这会导致api的使用并不有趣。
如果可以,尝试避免在头中包含太多的结构,如果不能,至少尝试使用其他方法来减少API的消耗。


对于结构,您还遇到一个问题,即不同编译器之间的对齐方式可能不同。
不幸的是,有些情况下,您处理的项目会强制整个项目的对齐方式不同,这显然也会影响头文件中的结构。结构越少越好:-)
(译者注:这块内容我也不是很懂,有懂内存对齐的大佬,评论区说下)

应该不言而喻的是:不要将宏作为API的一部分。宏不是一个符号,使用非基于C语言的用户会讨厌你在那里使用宏。

关于ABI稳定性的另一个注意事项是:最好将库的版本包含在头文件中,并编译到二进制文件中。这样,您就可以轻松地验证报头是否与二进制文件匹配,从而避免许多麻烦。
就像这样:

    #define YL_VERSION_MAJOR 1
    #define YL_VERSION_MINOR 0
    #define YL_VERSION ((YL_VERSION_MAJOR << 16) | YL_VERSION_MINOR)

    unsigned int yl_get_version(void);
    int yl_is_compatible_dll(void);

    And this in the implementation file:

    unsigned int yl_get_version(void)
    {
    return YL_VERSION;
    }

    int yl_is_compatible_dll(void)
    {
    unsigned int major = yl_get_version() >> 16;
    return major == YL_VERSION_MAJOR;
    }



Exporting a C API

当将C++ API暴露给C时,没有太多需要考虑的问题。
通常,对于您拥有的每个内部类,都会有一个没有任何字段的外部不透明结构。然后提供调用内部函数的函数。
想象这样一个类:

    namespace yourlibrary {
    class Task {
    public:
    Task();
    ~Task();

    bool is_pending() const;
    void tick();
    const char *result_string() const;
    };
    }


内部C++ API是很明显的,但是如何通过C来公开它呢?
因为外部ABI现在不再知道结构有多大,所以需要为外部调用方分配内存,或者给它一个方法来计算要分配多少内存。
我通常更喜欢为外部用户分配,并提供一个免费的功能。对于如何使内存分配系统仍然灵活,请看下一部分。
For now this is the external header (this has to be in extern "C" braces):

    struct yl_task_s;
    typedef struct yl_task_s yl_task_t;

    YL_API yl_task_t *yl_task_new();
    YL_API void yl_task_free(yl_task_t *task);
    YL_API int yl_task_is_pending(const yl_task_t *task);
    YL_API void yl_task_tick(yl_task_t *task);
    YL_API const char *yl_task_get_result_string(const yl_task_t *task);

    And this is how the shim layer would look like in the implementation:

    #define AS_TYPE(Type, Obj) reinterpret_cast<Type *>(Obj)
    #define AS_CTYPE(Type, Obj) reinterpret_cast<const Type *>(Obj)

    yl_task_t *yl_task_new()
    {
    return AS_TYPE(yl_task_t, new yourlibrary::Task());
    }

    void yl_task_free(yl_task_t *task)
    {
    if (!task)
    return;
    delete AS_TYPE(yourlibrary::Task, task);
    }

    int yl_task_is_pending(const yl_task_t *task)
    {
    return AS_CTYPE(yourlibrary::Task, task)->is_pending() ? 1 : 0;
    }

    void yl_task_tick(yl_task_t *task)
    {
    AS_TYPE(yourlibrary::Task, task)->tick();
    }

    const char *yl_task_get_result_string(const yl_task_t *task)
    {
    return AS_CTYPE(yourlibrary::Task, task)->result_string();
    }


注意构造函数和析构函数是如何完全包装的。
现在标准C++有一个问题:它会引起异常。
由于构造函数没有返回值来向外部发出出错的信号,因此如果分配失败,它将引发异常。
(译者注:new会抛异常,虽然也有不跑异常的版本,new(std::nothrow),)
但这不是唯一的问题。我们现在如何定制库如何分配内存?C++在这方面相当丑陋。但基本上是可以解决的。
在我们继续之前:在任何情况下,请创建一个库,它会用泛型名称污染命名空间。
为了降低名称空间冲突的风险,请始终在所有符号(如yl_u)之前放置一个公共前缀。

Context Objects

全局 stats很糟糕,那么解决办法是什么?
一般来说,解决方案是使用我称之为“context”的对象来保存状态。
这些对象将包含所有重要的内容,否则将放入全局变量中。
这样,库的用户就可以拥有其中的多个。然后让每个API函数将该上下文作为第一个参数。

如果库不是线程安全的,这一点特别有用。这样,每个线程至少可以有一个context,
这可能已经足够从代码中获得一些并行性了。

理想情况下,每个上下文对象也可以使用不同的分配器,但是考虑到C++中的复杂性,如果您没有这样做,我不会感到非常失望。

Memory Allocation Customization

如前所述,构造函数可能会失败,我们希望自定义内存分配,那么我们如何做到这一点?
在C++中,有两个系统负责内存分配:分配运算符运算符new和new new []以及容器的分配器。
如果要自定义分配器,则需要同时处理这两个问题。首先,您需要一种方法让其他人重写分配器函数。最简单的方法是在public头中提供这样的内容:

    YL_API void yl_set_allocators(void *(*f_malloc)(size_t),
    void *(*f_realloc)(void *, size_t),
    void (*f_free)(void *));
    YL_API void *yl_malloc(size_t size);
    YL_API void *yl_realloc(void *ptr, size_t size);
    YL_API void *yl_calloc(size_t count, size_t size);
    YL_API void yl_free(void *ptr);
    YL_API char *yl_strdup(const char *str);


然后在内部头文件中添加一组内联函数,这些函数重定向到设置为内部结构的函数指针。
因为我们不允许用户提供calloc和strdup,所以您可能还希望重新实现这些函数:
(译者注:C++有个问题是不能跨模块释放内存,会引起堆坏错误,如果是在windows上,内存使用的原则是谁申请,谁释放。
之前soui作者写过一篇博文(地址:https://www.cnblogs.com/setoutsoft/p/3961808.html),为啥SOUI utilies单独编译,里面提到了这个问题,别的blog也提到过。)

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 22:28:02 | 显示全部楼层
续上文:

  1. struct yl_allocators_s {
  2.     void *(*f_malloc)(size_t);
  3.     void *(*f_realloc)(void *, size_t);
  4.     void (*f_free)(void *);
  5. };
  6. extern struct yl_allocators_s _yl_allocators;

  7. inline void *yl_malloc(size_t size)
  8. {
  9.     return _yl_allocators.f_malloc(size);
  10. }

  11. inline void *yl_realloc(void *ptr, size_t size)
  12. {
  13.     return _yl_allocators.f_realloc(ptr, size);
  14. }

  15. inline void yl_free(void *ptr)
  16. {
  17.     _yl_allocators.f_free(ptr);
  18. }

  19. inline void *yl_calloc(size_t count, size_t size)
  20. {
  21.     void *ptr = _yl_allocators.f_malloc(count * size);
  22.     memset(ptr, 0, count * size);
  23.     return ptr;
  24. }

  25. inline char *yl_strdup(const char *str)
  26. {
  27.     size_t length = strlen(str) + 1;
  28.     char *rv = (char *)yl_malloc(length);
  29.     memcpy(rv, str, length);
  30.     return rv;
  31. }

  32. For the setting of the allocators themselves you probably want to put that into a separate source file:

  33. struct yl_allocators_s _yl_allocators = {
  34.     malloc,
  35.     realloc,
  36.     free
  37. };

  38. void yl_set_allocators(void *(*f_malloc)(size_t),
  39.                        void *(*f_realloc)(void *, size_t),
  40.                        void (*f_free)(void *))
  41. {
  42.     _yl_allocators.f_malloc = f_malloc;
  43.     _yl_allocators.f_realloc = f_realloc;
  44.     _yl_allocators.f_free = f_free;
  45. }
复制代码

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 22:48:14 | 显示全部楼层
Memory Allocators and C++

既然我们已经设置了这些函数,我们如何使C++使用它们呢?这部分既棘手又烦人。
要通过yl_malloc分配自定义类,需要在所有类中实现分配运算符。
因为这是一个相当重复的过程,我建议为它编写一个宏,可以放在类的私有部分。
我选择按惯例选择它必须私有化,即使它实现的功能是公共的。
我这样***要是为了让它靠近定义数据的地方,在我的例子中通常是私有的。您需要确保不会忘记将该宏添加到所有类的私有部分:

  1. #define YL_IMPLEMENTS_ALLOCATORS \
  2. public: \
  3.     void *operator new(size_t size) { return yl_malloc(size); } \
  4.     void operator delete(void *ptr) { yl_free(ptr); } \
  5.     void *operator new[](size_t size) { return yl_malloc(size); } \
  6.     void operator delete[](void *ptr) { yl_free(ptr); } \
  7.     void *operator new(size_t, void *ptr) { return ptr; } \
  8.     void operator delete(void *, void *) {} \
  9.     void *operator new[](size_t, void *ptr) { return ptr; } \
  10.     void operator delete[](void *, void *) {} \
  11. private:
复制代码


Here is how an example usage would look like:

  1. class Task {
  2. public:
  3.     Task();
  4.     ~Task();

  5. private:
  6.     YL_IMPLEMENTS_ALLOCATORS;
  7.     // ...
  8. };
复制代码


现在,所有类都将通过分配器函数分配。但是如果你想使用STL容器呢?这些容器尚未通过您的函数分配。
要解决这个问题,您需要编写一个STL代理分配器。这是一个非常烦人的过程,因为接口非常复杂,实际上什么也不做。

  1. #include <limits>

  2. template <class T>
  3. struct proxy_allocator {
  4.     typedef size_t size_type;
  5.     typedef ptrdiff_t difference_type;
  6.     typedef T *pointer;
  7.     typedef const T *const_pointer;
  8.     typedef T& reference;
  9.     typedef const T &const_reference;
  10.     typedef T value_type;

  11.     template <class U>
  12.     struct rebind {
  13.         typedef proxy_allocator<U> other;
  14.     };

  15.     proxy_allocator() throw() {}
  16.     proxy_allocator(const proxy_allocator &) throw() {}
  17.     template <class U>
  18.     proxy_allocator(const proxy_allocator<U> &) throw() {}
  19.     ~proxy_allocator() throw() {}

  20.     pointer address(reference x) const { return &x; }
  21.     const_pointer address(const_reference x) const { return &x; }

  22.     pointer allocate(size_type s, void const * = 0) {
  23.         return s ? reinterpret_cast<pointer>(yl_malloc(s * sizeof(T))) : 0;
  24.     }

  25.     void deallocate(pointer p, size_type) {
  26.         yl_free(p);
  27.     }

  28.     size_type max_size() const throw() {
  29.         return std::numeric_limits<size_t>::max() / sizeof(T);
  30.     }

  31.     void construct(pointer p, const T& val) {
  32.         new (reinterpret_cast<void *>(p)) T(val);
  33.     }

  34.     void destroy(pointer p) {
  35.         p->~T();
  36.     }

  37.     bool operator==(const proxy_allocator<T> &other) const {
  38.         return true;
  39.     }

  40.     bool operator!=(const proxy_allocator<T> &other) const {
  41.         return false;
  42.     }
  43. };
复制代码


所以在我们继续之前,我们该如何使用这种可憎的东西呢?这样:
  1. #include <deque>
  2. #include <string>

  3. typedef std::deque<Task *, proxy_allocator<Task *> > TaskQueue;
  4. typedef std::basic_string<char, std::char_traits<char>,
  5.                           proxy_allocator<char> > String;
复制代码


我建议在某个地方创建一个头,定义所有要使用的容器,然后强制自己不要使用STL中的任何其他内容,除非typedefing使用正确的分配器。
小心:不要像调用全局new操作符那样使用new TaskQueue()这些东西。
将它们作为成员放在您自己的结构中,以便分配作为具有自定义分配器的对象的一部分进行。或者把它们放在堆栈上。

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 22:56:07 | 显示全部楼层
Memory Allocation Failures
在我看来,处理内存分配失败的最好方法是不处理它们。只是不要导致任何分配失败。
对于一个容易实现的库,只需知道在最坏的情况下,您将分配多少内存,如果您是无限的,请为库的用户提供一种方法,让他们知道事情有多糟。原因是没有人处理分配失败。

首先,STL完全取决于std::bad_alloc是从operator new抛出的(我们上面没有这么做,呵呵),
它只会弹出错误供您处理。当编译库而不进行异常处理时,库将终止进程。
那很可怕,但如果你不小心的话,无论如何都会发生这种事。我看到的忽略malloc返回值的代码比正确处理它的代码多。

除此之外:在某些系统上,malloc会完全欺骗你有多少可用内存。Linux会很高兴地为您提供指向内存的指针—它无法用真正的物理内存进行备份。
这种fiat内存行为非常有用,但也意味着您通常必须假设不会发生分配失败。
所以,如果不报告分配错误,如果你使用C++,而且你还想坚持STL,那么放弃它,只是不要耗尽内存。
(译者注:这段写的啥我也没看懂,有大佬评论区说下,谢谢)

在电脑游戏中,一般的概念是给子系统分配他们自己的分配器,只是确保它们分配的永远不会超过给定的。
EA似乎建议分配器处理分配失败。例如,当无法加载更多内存时,它将检查是否可以释放一些不需要的资源(如缓存),而不是让调用者知道存在内存故障。
即使是C++的标准使用分配器也可以进行有限的设计。



该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 22:58:38 | 显示全部楼层
Building

既然已经编写了代码,那么如何在不让用户不满意的情况下构建库呢?如果你和我一样,你来自一个Unix背景,makefile是构建软件的基础。
然而,这不是每个人都想要的。
Autotools/autoconf是非常糟糕的软件,如果你把它给一个windows用户,他们会叫你各种各样的名字。相反,请确保有Visual Studio解决方案。

如果你不想和Visual Studio打交道,因为它不是你选择的工具链呢?如果你想让解决方案和makefile保持同步呢?
这个问题的答案是premake或cmake。
你用哪一个很大程度上取决于你。两者都可以从一个简单的定义脚本中生成makefile、XCode或Visual Studio解决方案。

我以前是cmake的超级粉丝,但现在我改成了premake。
原因是cmake有一些硬编码的东西需要我定制(例如,为Xbox 360构建一个Visual Studio解决方案是你不能用stockcmake做的)。
Premake与cmake有许多相同的问题,但它几乎完全是用lua编写的,并且可以很容易地定制。
Premake本质上是一个包含lua解释器和一堆lua脚本的可执行文件。
它很容易重新编译,如果你不想,你的预生成文件可以覆盖一切,如果你只是知道如何。

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 23:09:03 | 显示全部楼层
Testing

最后:你如何测试你的库?现在,显然有很多用C和C++编写的测试工具,但是我认为最好的工具实际上是在其他地方。
共享库不仅仅是C和C++所能享受的,你可以用各种语言使用它们。
有什么更好的方法来测试你的API,从一个不是C++的语言使用它?

在我的例子中,我使用Python来测试我的库。更重要的是:我使用py.test和CFFI来测试我的库。这比直接在C/C++中做的有很多优点。

最大的优点是提高了迭代速度。我根本不需要编译我的测试,它们只是运行。
不仅编译步骤会消失,我还可以利用Python的动态类型和py.test良好的assert语句。
我自己编写帮助程序来打印信息,并在我的库和Python之间转换数据,我获得了良好的错误报告的所有好处。

第二个好处是很好的隔离。pytest-xdist是py.test的插件,它将--boxed标志添加到py.test中,py.test在单独的进程中运行每个测试。
如果你有可能由于segfault而崩溃的测试,那么这是非常有用的。
如果在系统上启用coredumps,那么可以在gdb中加载segfault并找出问题所在。这也非常有效,因为您不需要处理由于断言失败和代码跳过清理而发生的内存泄漏。
操作系统将分别为每个测试清理。不幸的是,这是通过fork()系统调用实现的,所以它现在在windows上不能很好地工作。


那么,你如何使用你的库与CFFI?
您需要做两件事:您需要确保您的公共头文件不包含任何其他头文件。
如果您不能,只需添加一个禁用include的定义(如YL\u NOINCLUDE)。
(译者注:CFFI(C Foreign Function Interface) 是Python的C语言外部函数接口。 Python可以与几乎任何C语言代码进行交互,基于类似C语言的声明,您通常可以从头文件或文档中复制粘贴。)

This is all that's needed to make CFFI work:

  1. import os
  2. import subprocess
  3. from cffi import FFI

  4. here = os.path.abspath(os.path.dirname(__file__))
  5. header = os.path.join(here, 'include', 'yourlibrary.h')

  6. ffi.cdef(subprocess.Popen([
  7.     'cc', '-E', '-DYL_API=', '-DYL_NOINCLUDE',
  8.     header], stdout=subprocess.PIPE).communicate()[0])
  9. lib = ffi.dlopen(os.path.join(here, 'build', 'libyourlibrary.dylib'))
复制代码

Place it in a file called testhelpers.py next to your tests.

很明显,这是一个只在OSX上运行的简单版本,但是很容易扩展到不同的操作系统。
实际上,这会调用C预处理器并添加一些额外的定义,然后将其返回值馈送给CFFI解析器。之后,你有一个漂亮的包装库工作。
下面是这样一个测试的例子。把它放在一个名为test_something.py的文件中,让py.test执行它:

  1. import time
  2. from testhelpers import ffi, lib

  3. def test_basic_functionality():
  4.     task = lib.yl_task_new()
  5.     while lib.yl_task_is_pending(task)
  6.         lib.yl_task_process(task)
  7.         time.sleep(0.001)
  8.     result = lib.yl_task_get_result_string(task)
  9.     assert ffi.string(result) == ''
  10.     lib.yl_task_free(task)
复制代码


py.test还有其他优点。例如,它支持fixture,允许您设置可以在测试之间重用的公共资源。
这是非常有用的,例如,如果使用库需要创建某种上下文对象,在其上设置公共配置,然后销毁它。
To do that, just create a conftest.py file with the following content:
  1. import pytest
  2. from testhelpers import lib, ffi

  3. @pytest.fixture(scope='function')
  4. def context(request):
  5.     ctx = lib.yl_context_new()
  6.     lib.yl_context_set_api_key(ctx, "my api key")
  7.     lib.yl_context_set_debug_mode(ctx, 1)
  8.     def cleanup():
  9.         lib.yl_context_free(ctx)
  10.     request.addfinalizer(cleanup)
  11.     return ctx

  12. To use this now, all you need to do is to add a parameter called context to your test function:

  13. from testhelpers import ffi, lib

  14. def test_basic_functionality(context):
  15.     task = lib.yl_task_new(context)
  16.     ...
复制代码








该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 23:12:39 | 显示全部楼层
Summary

由于这比通常要长,这里简要总结了在构建本机共享库时要记住的最重要的事情:
把它写在C或C++,不要疯狂地用一种语言来构建它,它占用了整个运行时,占用了CPU和内存。
如果你能避免的话,就没有global state!
不要在公共头中定义公共类型
不要在公共标题中包含像windows.h这样的疯狂头文件。
在你的头文件中包括在一起。考虑添加一种通过define禁用all include的方法。
注意你的名字空间。不要暴露你不想暴露的符号。
创建一个类似YL_API的宏,该宏在要公开的每个符号前面加上前缀。
努力建立一个稳定的ABI
不要对结构疯狂
让人们自定义内存分配器。如果不能按“context”对象执行,至少要按库执行。
使用STL时要小心,始终只能通过添加分配器的typedef。
不要强迫用户使用您喜欢的构建工具,始终确保库的用户找到一个Visual Studio解决方案并生成适当的文件。


就这样!构建库快乐

    Write it in C or C++, don't get crazy with building it in a language that pulls in a whole runtime that takes up CPU and memory.
    No global state if you can avoid it!
    Do not define common types in your public headers
    Do not include crazy headers like windows.h in your public headers.
    Be light on includes in your headers altogether. Consider adding a way to disable all includes through a define.
    take good care about your namespace. Don't expose symbols you do not want to be exposed.
    Create a macro like YL_API that prefixes each symbol you want to expose.
    Try to build a stable ABI
    Don't go crazy with structs
    let people customize the memory allocators. If you can't do it per “context” object, at least do it per library.
    Be careful when using the STL, always only through a typedef that adds your allocator.
    Don't force your users to use your favourite build tool, always make sure that the user of a library finds a Visual Studio solution and makefile in place.

That's it! Happy library building!

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 23:14:22 | 显示全部楼层
完了,就这么多,大都是一些基础使用的东西,
翻译的不好,简易大家去看下原文,原文没广告,很干净。
原文地址:https://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/
希望国内的IT事业能发展更好,谢谢大家。

该用户从未签到

39

主题

93

帖子

462

积分

02:00元婴期

Rank: 3Rank: 3

积分
462
 楼主| 发表于 2020-4-19 23:22:46 | 显示全部楼层
去看了下这个作者(Armin Ronacher)的博客(https://lucumr.pocoo.org/),这兄弟看起来挺强悍的,主要是做web,c,python的。
抱歉,原来这兄弟就是python flask 框架的作者,失敬失敬,难怪他的python项目都这么多star。。。。。。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|SOUI官方论坛

GMT+8, 2024-5-19 06:47

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表