• faster python之ctypes调用so/dll


    .so和.dll文件

    .dll(动态链接库)和.so(共享对象文件)是两种不同操作系统上使用的动态链接库格式。

    1. .dll文件

      • Windows系统上使用的动态链接库文件格式。
      • 通常用C/C++编写,并由编译器生成。
      • 可以被多个程序共享,并在运行时动态加载到内存中。
      • 允许不同程序之间共享代码和数据,有助于节省内存。
      • 在Python中可以使用ctypes库或者其他扩展库来调用.dll文件中的函数。
    2. .so文件

      • 类Unix系统(如Linux)上使用的动态链接库文件格式。
      • 通常用C/C++编写,并由编译器生成。
      • 也可以被多个程序共享,并在运行时动态加载到内存中。
      • .dll类似,允许不同程序之间共享代码和数据。

    为什么Go和C文件都可以编译成.dll.so文件呢?

    这是因为编译器(如GCC、Clang)和构建工具(如Go的工具链)支持将源代码编译成多种目标格式,包括可执行文件、静态库和动态链接库。

    Go编译器支持将Go代码编译成可执行文件,静态库(.a文件)和共享对象文件(.so文件或.dll文件),这使得Go可以用于构建独立的应用程序,也可以用于构建共享库,供其他程序使用。

    C编译器同样也可以将C代码编译成可执行文件,静态库(.a文件)和共享对象文件(.so文件或.dll文件),这使得C语言也具有相似的灵活性。

    总的来说,.dll.so文件是为了方便代码的共享和重用,使得多个程序可以共享一组函数或者代码库。这对于在不同程序之间共享代码是非常有用的,特别是当你想要避免在每个程序中都复制相同的代码时。

    将go代码编译为动态链接库

    1. 准备xxx.go文件(必须要有一个main包才可以编译)

      package main
      
      import "C"
      
      //export Add
      func Add(a, b int) int {
      	return a + b
      }
      
      func main() {}
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    2. 执行命令编译:

      go build -buildmode=c-shared -o tool.so tool.go
      
      • 1

      在这里插入图片描述

    3. python调用

      import ctypes
      
      mylibrary = ctypes.CDLL('./tool.so')
      
      result = mylibrary.Add(3, 4)
      print(result)  # 这将打印出 7
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      在这里插入图片描述

    将C代码编译成动态链接库

    1. 准备xxx.c文件

      int add(int a, int b) {
      	return a + b;
      }
      
      • 1
      • 2
      • 3
    2. 执行命令编译

      gcc tool.c -fPIC -shared -o ctool.so
       
      * -shared 为链接库  让编译器知道是要编译一个共享库
      * -fPIC(Position Independent Code)   编译生成代码与位置无关
      * 如果想能够调试可加上-g -Wall等参数
      
      • 1
      • 2
      • 3
      • 4
      • 5
    3. python调用

      import ctypes
      
      mylibrary = ctypes.CDLL('./ctool.so')
      
      result = mylibrary.add(3, 4)
      print(result)  # 这将打印出 7
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      在这里插入图片描述

    ctypes库

    附:3.7文档:https://docs.python.org/zh-cn/3.7/library/ctypes.html

    ctypes是Python标准库中的一个模块,它提供了一种与C语言兼容的外部函数库接口,允许Python程序调用动态链接库(DLL或.so文件)中的C函数。这使得Python可以与C语言编写的库或者其他外部库进行交互。

    以下是ctypes库的一些主要概念和用法:

    1. 加载共享库

      使用ctypes.CDLL()来加载共享库。例如:

      import ctypes
      
      mylibrary = ctypes.CDLL('./mylibrary.so')
      
      • 1
      • 2
      • 3

      这将加载名为mylibrary.so的共享对象文件。

    2. 调用C函数

      一旦共享库被加载,你可以使用Python来调用其中的C函数。例如:

      result = mylibrary.Add(3, 4)
      
      • 1

      这将调用名为Add的C函数,并将参数3和4传递给它。

    3. 指定参数和返回类型

      在调用C函数之前,你应该确保使用ctypes正确地指定了参数类型和返回类型,以便与C函数的签名匹配。

      mylibrary.Add.argtypes = [ctypes.c_int, ctypes.c_int]
      mylibrary.Add.restype = ctypes.c_int
      
      • 1
      • 2

      这个例子中,我们指定了Add函数的参数类型为两个整数,返回类型也是一个整数。

    4. 处理指针和数据类型

      ctypes可以处理C中的基本数据类型以及指针等复杂数据结构。你可以使用ctypes中的类型来映射C数据类型。

    5. 错误处理

      如果调用C函数可能会返回错误码,你可以通过检查返回值来处理错误。

    6. 回调函数

      你可以使用ctypes来定义Python回调函数,并将其传递给C函数,以便C函数在适当的时候调用Python函数。

    7. 结构体和联合体

      你可以使用ctypes来创建和操作C中的结构体和联合体。

    8. 内存管理

      ctypes提供了一些工具来处理内存分配和释放,以确保与C代码交互时不会出现内存泄漏等问题。

    总的来说,ctypes是一个强大的工具,可以让Python与C代码无缝交互。它使得Python能够利用C语言编写的库,同时也提供了一种方便的方式来测试和调试C代码。然而,由于ctypes是一个动态的Python库,所以在性能要求严格的情况下,可能需要考虑使用更高级的工具,如Cython或SWIG。

    基础数据类型

    ctypes 定义了一些和C兼容的基本数据类型:

    ctypes 类型

    C 类型

    Python 类型

    c_bool

    _Bool

    bool (1)

    c_char

    char

    单字符字节串对象

    c_wchar

    wchar_t

    单字符字符串

    c_byte

    char

    int

    c_ubyte

    unsigned char

    int

    c_short

    short

    int

    c_ushort

    unsigned short

    int

    c_int

    int

    int

    c_uint

    unsigned int

    int

    c_long

    long

    int

    c_ulong

    unsigned long

    int

    c_longlong

    __int64long long

    int

    c_ulonglong

    unsigned __int64unsigned long long

    int

    c_size_t

    size_t

    int

    c_ssize_t

    ssize_t or Py_ssize_t

    int

    c_float

    float

    float

    c_double

    double

    float

    c_longdouble

    long double

    float

    c_char_p

    char* (以 NUL 结尾)

    字节串对象或 None

    c_wchar_p

    wchar_t* (以 NUL 结尾)

    字符串或 None

    c_void_p

    void*

    int 或 None

    使用方法

    基本数据类型
    # -*- coding: utf-8 -*-
    from ctypes import *
    
    # 字符,仅接受one character bytes, bytearray or integer
    char_type = c_char(b"a")
    # 字节
    byte_type = c_char(1)
    # 字符串
    string_type = c_wchar_p("abc")
    # 整型
    int_type = c_int(2)
    # 直接打印输出的是对象信息,获取值需要使用value方法
    print(char_type, byte_type, int_type)
    print(char_type.value, byte_type.value, string_type.value, int_type.value)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出:

    c_char(b'a') c_char(b'\x01') c_int(2)
    b'a' b'\x01' abc 2
    
    • 1
    • 2
    数组类型

    数组的创建和C语言的类似,给定数据类型和长度即可,

    # 数组
    # 定义类型
    char_array = c_char * 3
    # 初始化
    char_array_obj = char_array(b"a", b"b", 2)
    # 打印只能打印数组对象的信息
    print(char_array_obj)
    # 打印值通过value方法
    print(char_array_obj.value)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输出:

    <main.c_char_Array_3 object at 0x7f2252e6dc20>
    b'ab\x02'
    
    • 1
    • 2

    也可以在创建的时候直接进行初始化,

    int_array = (c_int * 3)(1, 2, 3)
    for i in int_array:
        print(i)
    
    char_array_2 = (c_char * 3)(1, 2, 3)
    print(char_array_2.value)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出:

    1
    2
    3
    b'\x01\x02\x03'
    
    • 1
    • 2
    • 3
    • 4

    这里需要注意,通过value方法获取值只适用于字符数组,其他类型如print(int_array.value)的使用会报错:

    AttributeError: 'c_int_Array_3' object has no attribute 'value'
    
    • 1
    指针类型

    ctypes提供了pointer()和POINTER()两种方法创建指针,区别在于:

    pointer()用于将对象转化为指针,如下:

    # 指针类型
    int_obj = c_int(3)
    int_p = pointer(int_obj)
    print(int_p)
    # 使用contents方法访问指针
    print(int_p.contents)
    # 获取指针指向的值
    print(int_p[0])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    输出:

    <__main__.LP_c_int object at 0x7fddbcb1de60>
    c_int(3)
    3
    
    • 1
    • 2
    • 3

    POINTER()用于定义某个类型的指针,如下:

    # 指针类型
    int_p = POINTER(c_int)
    # 实例化
    int_obj = c_int(4)
    int_p_obj = int_p(int_obj)
    print(int_p_obj)
    print(int_p_obj.contents)
    print(int_p_obj[0])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    输出:

    <__main__.LP_c_int object at 0x7f47df7f79e0>
    c_int(4)
    4
    
    • 1
    • 2
    • 3

    如果弄错了初始化的方式会报错,POINTER()如下:

    # 指针类型
    int_p = POINTER(c_int)
    # 实例化
    int_obj = c_int(4)
    int_p_obj = POINTER(int_obj)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    报错:

    TypeError: must be a ctypes type
    
    • 1

    pointer()如下:

    # 指针类型
    int_p = pointer(c_int)
    
    • 1
    • 2

    报错:

    TypeError: _type_ must have storage info
    
    • 1

    创建空指针的方式

    null_ptr = POINTER(c_int)()
    print(bool(null_ptr))
    
    • 1
    • 2

    输出:

    False
    
    • 1

    指针类型的转换
    ctypes提供cast()方法将一个ctypes实例转换为指向另一个ctypes数据类型的指针,cast()接受两个参数,一个是ctypes对象,它是或可以转换成某种类型的指针,另一个是ctypes指针类型。它返回第二个参数的一个实例,该实例引用与第一个参数相同的内存块。

    int_p = pointer(c_int(4))
    print(int_p)
    
    char_p_type = POINTER(c_char)
    print(char_p_type)
    
    cast_type = cast(int_p, char_p_type)
    print(cast_type)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    输出:

    <__main__.LP_c_int object at 0x7f43e2fcc9e0>
    <class 'ctypes.LP_c_char'>
    <ctypes.LP_c_char object at 0x7f43e2fcc950>
    
    • 1
    • 2
    • 3
    结构体类型

    结构体类型的实现,结构和联合必须派生自ctypes模块中定义的结构和联合基类。每个子类必须 定义一个_fields_属性,_fields_必须是一个二元组列表,包含字段名和字段类型。_pack_属性 决定结构体的字节对齐方式,默认是4字节对齐,创建时使用_pack_=1可以指定1字节对齐。比如初始化student_t的方法如下,特别需要注意的是字段名不能和python关键字重名

    # -*- coding: utf-8 -*-
    from ctypes import *
    
    # 学生信息如下
    stu_info = [("class", "A"),
                ("grade", 90),
                ("array", [1, 2, 3]),
                ("point", 4)]
    
    # 创建结构提类
    class Student(Structure):
        _fields_ = [("class", c_char),
                ("grade", c_int),
                ("array", c_long * 3),
                ("point", POINTER(c_int))]
    
    print("sizeof Student: ", sizeof(Student))
    
    # 实例化
    long_array = c_long * 3
    long_array_obj = long_array(1, 2, 3)
    int_p = pointer(c_int(4))
    stu_info_value = [c_char(b"A"), c_int(90), long_array_obj, int_p]
    
    stu_obj = Student(*stu_info_value)
    # 这样打印报错,因为字段名和python关键字class重名了,这是需要特别注意的点
    # print("stu info:", stu_obj.class, stu_obj.grade, stu_obj.array[0], stu_obj.point[0])
    print("stu info:", stu_obj.grade, stu_obj.array[0], stu_obj.point[0])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    输出:

    sizeof Student: 40
    stu info: 90 1 4
    
    • 1
    • 2

    如果把_pack_改为1,则输出:

    sizeof Student: 37
    stu info: 90 1 4
    
    • 1
    • 2
    嵌套结构体

    嵌套结构体的使用需要创建基础结构体的类型,然后将基础结构体的类型作为嵌套结构体 的成员,注意基础结构体所属字段的字段类型是基础结构体的类名,如下:

    # 创建类型, nest_stu字段的类型为基础结构体的类名
    class NestStudent(Structure):
        _fields_ = [("rank", c_char),
                    ("nest_stu", Student)]
    
    # 实例化
    nest_stu_info_list = [c_char(b"M"), stu_obj]
    nest_stu_obj = NestStudent(*nest_stu_info_list)
    
    print("nest stu info: ", nest_stu_obj.rank, "basic stu info: ", nest_stu_obj.nest_stu.grade)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    输出:

    nest stu info: b'M' basic stu info: 90
    
    • 1
    结构体数组

    结构体数组与普通数组的创建类似,需要提前创建结构体的类型,然后使用struct type * array_length 的方法创建数组。

    # 结构体数组
    # 创建结构体数组类型
    stu_array = Student * 2
    # 用Student类的对象实例化结构体数组
    stu_array_obj = stu_array(stu_obj, stu_obj)
    
    # 增加结构体数组成员
    class NestStudent(Structure):
        _fields_ = [("rank", c_char),
                    ("nest_stu", Student),
                    ("strct_array", Student * 2)]
    
    # 实例化
    nest_stu_info_list = [c_char(b"M"), stu_obj, stu_array_obj]
    nest_stu_obj = NestStudent(*nest_stu_info_list)
    
    # 打印结构体数组第二个索引的grade字段的信息
    print("stu struct array info: ", nest_stu_obj.strct_array[1].grade, nest_stu_obj.strct_array[1].array[0])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出:

    stu struct array info: 90 1
    
    • 1
    结构体指针

    首先创建结构体,然后使用ctype的指针方法包装为指针。

    # 结构体指针
    # # 创建结构体数组类型
    stu_array = Student * 2
    # # 用Student类的对象实例化结构体数组
    stu_array_obj = stu_array(stu_obj, stu_obj)
    # 曾接结构体指针成员,注意使用类型初始化指针是POINTER()
    class NestStudent(Structure):
        _fields_ = [("rank", c_char),
                    ("nest_stu", Student),
                    ("strct_array", Student * 2),
                    ("strct_point", POINTER(Student))]
    
    # 实例化,对Student的对象包装为指针使用pointer()
    nest_stu_info_list = [c_char(b"M"), stu_obj, stu_array_obj, pointer(stu_obj)]
    nest_stu_obj = NestStudent(*nest_stu_info_list)
    
    # 结构体指针指向Student的对象
    print("stu struct point info: ", nest_stu_obj.strct_point.contents)
    # 访问Student对象的成员
    print("stu struct point info: ", nest_stu_obj.strct_point.contents.grade)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出:

    stu struct point info: <__main__.Student object at 0x7f8d80e70200> # 结构体指针指向的对象信息
    stu struct point info: 90 # Student结构体grade成员
    
    • 1
    • 2
    结构体指针数组

    创建结构体指针数组的顺序为先创建结构体,然后包装为指针,最后再创建数组,用结构体指针去实例化数组。

    # 结构体指针数组
    # 创建结构体数组类型
    stu_array = Student * 2
    # # 用Student类的对象实例化结构体数组
    stu_array_obj = stu_array(stu_obj, stu_obj)
    # 创建结构体指针数组
    stu_p_array = POINTER(Student) * 2
    # 使用pointer()初始化
    stu_p_array_obj = stu_p_array(pointer(stu_obj), pointer(stu_obj))
    # 曾接结构体指针成员,注意使用类型初始化指针是POINTER()
    class NestStudent(Structure):
        _fields_ = [("rank", c_char),
                    ("nest_stu", Student),
                    ("strct_array", Student * 2),
                    ("strct_point", POINTER(Student)),
                    ("strct_point_array", POINTER(Student) * 2)]
    
    # 实例化,对Student的对象包装为指针使用pointer()
    nest_stu_info_list = [c_char(b"M"), stu_obj, stu_array_obj, pointer(stu_obj), stu_p_array_obj]
    nest_stu_obj = NestStudent(*nest_stu_info_list)
    
    # 数组第二索引为结构体指针
    print(nest_stu_obj.strct_point_array[1])
    # 指针指向Student的对象
    print(nest_stu_obj.strct_point_array[1].contents)
    # Student对象的grade字段
    print(nest_stu_obj.strct_point_array[1].contents.grade)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    输出:

    <__main__.LP_Student object at 0x7f3f9a8e6200>
    <__main__.Student object at 0x7f3f9a8e6290>
    90
    
    • 1
    • 2
    • 3

    ctypes加载DLL的方式

    ctypes库提供了几种方式来加载动态链接库(DLL)。以下是常用的方法:

    1. 使用 CDLL

    使用ctypes.CDLL类来加载动态链接库,这是最常用的方式。

    import ctypes
    
    # 加载 DLL
    mylibrary = ctypes.CDLL('mylibrary.dll')
    
    # 调用 DLL 中的函数
    result = mylibrary.Add(3, 4)
    print(result)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2. 使用 WinDLL 类(Windows特定)

    在Windows系统上,可以使用ctypes.WinDLL类来加载DLL。它与ctypes.CDLL类似,但使用了stdcall调用约定。

    import ctypes
    
    # 加载 DLL
    mylibrary = ctypes.WinDLL('mylibrary.dll')
    
    # 调用 DLL 中的函数
    result = mylibrary.Add(3, 4)
    print(result)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3. 使用 cdll.LoadLibrary 方法

    可以使用ctypes库中的cdll.LoadLibrary方法来加载动态链接库:

    from ctypes import cdll
    
    # 加载 DLL
    mylibrary = cdll.LoadLibrary('mylibrary.dll')
    
    # 调用 DLL 中的函数
    result = mylibrary.Add(3, 4)
    print(result)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4. 使用绝对路径

    如果 DLL 文件不在Python脚本的当前工作目录下,你可以使用绝对路径来加载它:

    from ctypes import cdll
    import os
    
    # 获取 DLL 文件的绝对路径
    dll_path = os.path.abspath('mylibrary.dll')
    
    # 加载 DLL
    mylibrary = cdll.LoadLibrary(dll_path)
    
    # 调用 DLL 中的函数
    result = mylibrary.Add(3, 4)
    print(result)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5. 使用 os.add_dll_directory(Python 3.8+)

    如果你使用的是Python 3.8及以上版本,可以使用os.add_dll_directory来将包含DLL的目录添加到系统路径中:

    import os
    from ctypes import cdll
    
    # 添加包含 DLL 的目录到系统路径
    os.add_dll_directory(r'C:\path\to\dll\directory')
    
    # 加载 DLL
    mylibrary = cdll.LoadLibrary('mylibrary.dll')
    
    # 调用 DLL 中的函数
    result = mylibrary.Add(3, 4)
    print(result)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这种方法可以避免一些在加载DLL时可能会遇到的路径问题。

    请确保将示例中的 mylibrary.dll 替换成你实际要加载的DLL的文件名。

    编译属于自己的python解释器

    在 Windows 平台上编译修改后的 CPython 源代码,可以使用 Microsoft Visual Studio 编译器和一些附带的工具来完成。以下是详细步骤:

    1. 安装所需软件

      • 安装 Microsoft Visual Studio。建议安装一个完整的版本,包括 C++ 开发工具。
      • 安装 Git,用于从 GitHub 克隆 CPython 仓库。
    2. 获取源代码

      打开命令提示符或 PowerShell,运行以下命令克隆 CPython 仓库:

      git clone https://github.com/python/cpython.git
      
      • 1
    3. 安装 Windows SDK

      在 Visual Studio 中,使用 Visual Studio Installer 安装 “Desktop development with C++” 工作负载,并包括 Windows 10 SDK。

    4. 打开 Visual Studio

      打开 Visual Studio,然后打开 cpython\PCbuild\pcbuild.sln 解决方案文件。

    5. 进行修改

      在 Visual Studio 中进行你的修改。
      在这里插入图片描述

    6. 下载编译 cpython 需要的外部依赖

      命令行运行 PCbuild/get_externals.bat

      完成后在 cpython 中会多出来一个 externals 文件夹,其中包含编译 cpython 需要的外部依赖项

    7. 构建

      打开 pcbuild.sln ,进入 Visual Studio 进行编译。编译平台可选择 Win32、x64、ARM 和 ARM64。编译模式除了普通的 debug 和 release 之外还有 PGInstrument 和 PGUpdate 模式。

      在这里插入图片描述

      在 Visual Studio 中,选择 Release 或者 Debug 配置,然后按下 Ctrl+Shift+B 或者选择 “生成” > “解决方案生成” 来构建代码。

      如果你在 Visual Studio 中使用 Debug 配置构建,你将能够在构建过程中在 Visual Studio 中进行调试。

      PGInstrument 和 PGUpdate 是在 release 模式下加入 PGO (Profile Guided Optimization) 优化,这种编译模式需要 Visual Studio 的 Premium 版本。在 python 官网 下载的 python 就是在 PGO 优化模式下编译得到的。

      在 Visual Studio 中编译 CPython 的速度还是很快的,debug + win32 模式在笔记本上半分钟就可以编译完。编译成功后在 PCbuild/win32 路径下会生成 python_d.exe 和 python312_d.dll ,文件名中的 312 是版本号。这就是从源代码编译得到的 python,可以双击 python_d.exe 运行(后缀 _d 表示 CPython 是在 debug 模式下编译的。)

      在这里插入图片描述

    python 的核心功能由 python3.dll 来提供,python.exe 只是充当一个入口。python.exe 只是给 python3.dll 套了个壳,把命令行参数 argc argv 传递给 Py_Main 函数。

  • 相关阅读:
    P95陷阱
    springboot整合jwt认证
    【ATT&CK】ATT&CK视角下的水坑钓鱼攻防战法
    BF算法详解(JAVA语言实现)
    基于HOG特征提取和GRNN神经网络的人脸表情识别算法matlab仿真,测试使用JAFFE表情数据库
    星戈瑞DSPE-SS-PEG-CY7近红外花菁染料
    CommonJs和Es Module的区别
    day36-IO流03
    C++17静态数据成员声明为inline
    docker 遇到权限问题
  • 原文地址:https://blog.csdn.net/General_zy/article/details/133818549