C++从零开始实现LSM-Tree-KV存储-20-Python-SDK开发

这一章是加餐,主要是给大家介绍一下如何使用Python SDK来操作我们之前实现的LSM-Tree-KV存储。

代码仓库:ToniXWD/toni-lsm: A KV storage engine based on LSM Tree, supporting Redis RESP 感谢您的 Star!

欢迎支持本项目同步更新的从零开始实现的视频教程:https://avo6166ew2u.feishu.cn/docx/LXmVdezdsoTBRaxC97WcHwGunOc

欢迎加入讨论群:Toni-LSM项目交流群

1 SDK 的作用

Python SDK的作用是提供一个简单易用的接口,让用户可以使用Python这样的脚本语言方便地使用我们实现的LSM-Tree-KV存储。通常来说, 成熟的数据库等基础设施都会提供类似Python或者JavaScript这样的脚本语言的SDK,方便用户进行二次开发和使用。否则, 就只能使用C++这样的编译型语言来进行开发,或者调用动态链接库的接口,这样会比较麻烦。

这里我选择PythonSDk, 主要原因是Python的语法简单易懂,且有丰富的第三方库支持, 并且脚本语言也非常方便, 我自己调试的时候也更想用Python先验证一些流程的正确性, 然后再到具体的错误流程中进行C++的单步调试。

C/C++Python的SDK开发主要是通过pybind11这个库来实现的。pybind11是一个轻量级的库,可以让我们很方便地将C++代码暴露给Python,并且支持大部分的C++特性,包括类、函数、模板等。本章接下来的内容主要是介绍如何使用pybind11来实现一个简单的Python SDK,并且提供一些简单的示例代码来演示如何使用这个SDK。

2 pybind11 简介

2.1 pybind11 是什么

pybind11 是一个用于将 C++ 函数、类、模块 暴露给 Python 调用 的轻量级头文件库。它的目标是:

  • 简洁语法:像写 Python 模块一样写 C++ 绑定。
  • 无依赖性:只需要头文件,无需链接库。
  • 性能极高:性能几乎与纯 C++ 接近。

适用于以下场景:

  • Python 调用高性能 C++ 模块(如计算密集型任务)
  • Python 扩展模块编写(替代 CPython 的复杂 C API)
  • 将现有 C++ 库封装为 Python 模块

2.2 安装与环境配置

安装 Pythonpybind11(用于提供 --includes 编译选项):

1
pip install pybind11

获取 C++ 头文件(两种方式):

  1. 使用 pip 安装版本,编译时用 python -m pybind11 --includes
  2. 或者 clone 官方仓库:
    1
    git clone https://github.com/pybind/pybind11.git

这里我们在项目中可以用xmake的包管理工具添加依赖:

1
2
3
4
5
6
add_requires("pybind11")

target("lsm_pybind")
...
add_packages("pybind11")
...

2.3 示例:绑定 C++ 函数

📄 example.cpp 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <pybind11/pybind11.h>

// 普通 C++ 函数
int add(int i, int j) {
return i + j;
}

// 创建 Python 模块
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // 模块说明(可选)

// m.def("python函数名", C++函数指针, "函数文档字符串")
m.def("add", &add, "A function that adds two numbers");
}

🚀 编译命令(Linux/macOS):

1
2
3
c++ -O3 -Wall -shared -std=c++11 -fPIC \
$(python3 -m pybind11 --includes) example.cpp \
-o example$(python3-config --extension-suffix)

📌 说明

  • -O3:优化等级
  • -fPIC:生成与位置无关的代码
  • --extension-suffix:生成正确的 .so.pyd 文件名

🐍 在 Python 中使用

1
2
import example
print(example.add(3, 4)) # 输出 7

2.4 绑定类(C++ 类 → Python 类)

📄 example.cpp 中绑定类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <pybind11/pybind11.h>
using namespace pybind11;

class Pet {
public:
Pet(const std::string &name) : name(name) {}

void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }

private:
std::string name;
};

PYBIND11_MODULE(example, m) {
class_<Pet>(m, "Pet")
.def(init<const std::string &>()) // 绑定构造函数
.def("setName", &Pet::setName)
.def("getName", &Pet::getName);
}

🐍 Python 中使用:

1
2
3
4
5
6
import example

dog = example.Pet("Buddy")
print(dog.getName()) # 输出:Buddy
dog.setName("Rocky")
print(dog.getName()) # 输出:Rocky

📌 关键语法说明

  • class_<T>(module, "Python类名"):绑定 C++ 类
  • .def(init<Args...>()):绑定构造函数
  • .def("方法名", &方法指针):绑定成员方法

2.5 传递 STL 类型

pybind11 支持常见 STL 类型的自动转换:

1
2
3
4
5
6
7
8
9
10
11
#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // STL 支持
#include <vector>

std::vector<int> generate() {
return {1, 2, 3, 4};
}

PYBIND11_MODULE(example, m) {
m.def("generate", &generate);
}

Python 使用:

1
print(example.generate())  # [1, 2, 3, 4]

2.6 总结

功能 语法 说明
绑定函数 m.def("name", &func) 将 C++ 函数暴露给 Python
绑定类 class_<T>(m, "Class") 创建 Python 类
构造函数 .def(init<Args...>()) 构造器绑定
成员方法 .def("name", &method) 类成员方法绑定
STL支持 #include <pybind11/stl.h> 启用 vector/map/list 自动转换
NumPy支持 #include <pybind11/numpy.h> 绑定 NumPy 数组

3 Python SDK 实现

3.1 SDK 文件夹结构

1
2
3
4
5
6
7
8
9
10
11
12
sdk/
├── lsm_pybind.cpp # Pybind11绑定入口
├── tonilsm/
│ ├── __init__.py # 包安装脚本
│ ├── tonilsm/
│ │ ├── __init__.py # 核心模块加载器
│ │ ├── __init__.pyi # 类型提示文件
│ │ └── core/ # 编译产物目录
├── └setup.py # Python包安装脚本
xmake.lua # 构建配置文件
src/ # C++存储引擎源码
include/ # C++头文件

这里对每个模块说明如下:

  • sdk/:SDK的根目录
    • lsm_pybind.cpp:SDK的绑定入口文件,主要是将C++的类和函数绑定到Python
    • tonilsm/Python模块的目录,包含了Python模块的核心代码和类型提示文件,以及类型提示文件
      • tonilsm/__init__.pyPython模块的安装脚本,主要是将tonilsm模块导入到Python
      • tonilsm/__init__.pyi:类型提示文件,主要是给Python的IDE提供类型提示
      • tonilsm/core/:编译产物目录,主要是存放编译后的Python模块
      • setup.pyPython模块的安装脚本,主要是将tonilsm模块安装到Python
  • xmake.luaxmake的构建配置文件,主要是配置C++的编译选项和依赖

3.2 代码绑定

3.2.1 代码绑定的C++代码

这里我们只需要将lsm/engine.h 中的类和函数绑定到 Python 中即可, 但由于我们的主类类似beigin这样的迭代器返回的对象是一个迭代器, 且参数中包括一些自定义的隔离级别的枚举, 所以我们需要先将这些自定义数据类型也绑定到 Python 中。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// sdk/lsm_pybind.cpp
#include "../include/lsm/engine.h"
#include "../include/lsm/level_iterator.h"
#include <pybind11/functional.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;

// 绑定 TwoMergeIterator 迭代器
void bind_TwoMergeIterator(py::module &m) {
py::class_<TwoMergeIterator>(m, "TwoMergeIterator")
.def("__iter__", [](TwoMergeIterator &it) { return &it; })
.def("__next__", [](TwoMergeIterator &it) {
if (!it.is_valid())
throw py::stop_iteration();
auto kv = *it;
++it;
return py::make_tuple(kv.first, kv.second);
});
}

void bind_Level_Iterator(py::module &m) {
py::class_<Level_Iterator>(m, "Level_Iterator")
.def("__iter__", [](Level_Iterator &it) { return &it; })
.def("__next__", [](Level_Iterator &it) {
if (!it.is_valid())
throw py::stop_iteration();
auto kv = *it;
++it;
return py::make_tuple(kv.first, kv.second);
});
}

// 绑定 TranContext 事务上下文

// 提前声明 TranContext(如果头文件未包含)
class TranContext;

// 绑定 TranContext
void bind_TranContext(py::module &m) {
py::class_<TranContext, std::shared_ptr<TranContext>>(m, "TranContext")
.def("commit", &TranContext::commit,
py::arg("test_fail") = false) // 处理默认参数
.def("abort", &TranContext::abort)
.def("get", &TranContext::get)
.def("remove", &TranContext::remove)
.def("put", &TranContext::put);
}

void bind_IsolationLevel(py::module &m) {
py::enum_<IsolationLevel>(m, "IsolationLevel")
.value("READ_UNCOMMITTED", IsolationLevel::READ_UNCOMMITTED)
.value("READ_COMMITTED", IsolationLevel::READ_COMMITTED)
.value("REPEATABLE_READ", IsolationLevel::REPEATABLE_READ)
.value("SERIALIZABLE", IsolationLevel::SERIALIZABLE)
.export_values();
}

pybind11 语法解释:

  • py::class_: 用于将 C++ 类暴露给 Python。第一个参数是 C++ 类类型,第二个参数是 Python 类的名称。
  • def(): 用于绑定方法或函数。例如,.def(“方法名”, &类::方法) 将 C++ 方法绑定到 Python 方法。
  • py::arg(): 允许指定默认参数。例如,py::arg(“test_fail”) = false 设置 test_fail 参数的默认值。
  • py::make_tuple(): 将 C++ 元组转换为 Python 元组,方便返回多个值。
  • py::stop_iteration(): 用于在 Python 中信号迭代结束,类似于抛出 StopIteration 异常。
  • py::enum_: 用于将 C++ 枚举暴露给 Python。每个 .value() 调用将 C++ 枚举常量映射到 Python 枚举常量。
  • export_values(): 导出所有定义的枚举值,以便可以直接访问,而不需要使用枚举名称前缀。

在此后, 我们需要将LSM这个对外暴露的C++类绑定到Python中, 这里我们只需要将LSM类的构造函数和一些对外暴露的接口绑定到Python中即可, 具体代码如下:

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
29
30
31
32
33
34
// sdk/lsm_pybind.cpp
PYBIND11_MODULE(lsm_pybind, m) {
// 绑定辅助类
bind_TwoMergeIterator(m);
bind_Level_Iterator(m);
bind_TranContext(m);
bind_IsolationLevel(m);

// 主类 LSM
py::class_<LSM>(m, "LSM")
.def(py::init<const std::string &>())
// 基础操作
.def("put", &LSM::put, py::arg("key"), py::arg("value"),
"Insert a key-value pair (bytes type)")
.def("get", &LSM::get, py::arg("key"),
"Get value by key, returns None if not found")
.def("remove", &LSM::remove, py::arg("key"), "Delete a key")
// 批量操作
.def("put_batch", &LSM::put_batch, py::arg("kvs"),
"Batch insert key-value pairs")
.def("remove_batch", &LSM::remove_batch, py::arg("keys"),
"Batch delete keys")
// 迭代器
.def("begin", &LSM::begin, py::arg("tranc_id"),
"Start an iterator with transaction ID")
.def("end", &LSM::end, "Get end iterator")
// 事务
.def("begin_tran", &LSM::begin_tran, py::arg("isolation_level"),
"Start a transaction")
// 其他方法
.def("clear", &LSM::clear, "Clear all data") // ! Fix bugs
.def("flush", &LSM::flush, "Flush memory table to disk")
.def("flush_all", &LSM::flush_all, "Flush all pending data");
}

3.2.2 编译为动态链接库

我们做的这些绑定操作都需要编译为动态链接库, 这里我们使用xmake来编译, 具体的xmake.lua文件如下:

1
2
3
4
5
6
7
8
9
10
11
target("lsm_pybind")
set_kind("shared")
add_files("sdk/lsm_pybind.cpp")
add_packages("pybind11")
add_deps("lsm_shared")
add_includedirs("include", {public = true})
set_targetdir("$(buildir)/lib")
set_filename("lsm_pybind.so") -- 确保生成的文件名为 lsm_pybind.so
add_ldflags("-Wl,-rpath,$ORIGIN")
add_defines("TONILSM_EXPORT=__attribute__((visibility(\"default\")))")
add_cxxflags("-fvisibility=hidden") -- 隐藏非导出符号

3.3 Python包组织

3.3.1 模块结构

在有了这个动态链接库后, 我们需要将这个动态链接库组织成一个Python包, 这部分模块结构为:

1
2
3
4
5
6
7
8
9
10
tonilsm
├── tonilsm
│ ├── core
│ │ ├── __init__.py
│ │ ├── __init__.pyi
│ │ ├── lib
│ │ │ ├── liblsm_shared.so
│ │ │ └── lsm_pybind.so
│ ├── __init__.py
│ └── setup.py

这里忽略了自动生成和缓存的文件夹和文件

这里简单介绍下这部分的模块:

  • tonilsm/: 这是整个 Python 包的根目录。

    • tonilsm/__init__.py: 这个文件用于初始化 tonilsm 包。它可以包含包的导入逻辑和其他初始化代码。
  • tonilsm/core/: 这个目录包含核心的实现代码和动态链接库。

    • tonilsm/core/__init__.py: 这个文件用于初始化 core 子包。通常在这个文件中导入动态链接库中的模块,以便用户可以直接从 tonilsm.core 中访问这些模块。

    • tonilsm/core/__init__.pyi: 这个文件是类型提示文件(type stub),用于提供类型信息,帮助 IDE 和静态类型检查工具(如 mypy)更好地理解代码结构。

    • tonilsm/core/lib/: 这个目录存放编译生成的动态链接库文件。

      • tonilsm/core/lib/liblsm_shared.so: 这是 C++ 存储引擎的共享库文件。
      • tonilsm/core/lib/lsm_pybind.so: 这是通过 pybind11 绑定生成的 Python 扩展模块。

3.3.2 模块导出

tonilsm/__init__.py:

1
2
3
4
# tonilsm/__init__.py
from .core import LSM, IsolationLevel # 从 core 子模块导出 LSM 类

__all__ = ["LSM", "IsolationLevel"]

这里的作用是将 LSMIsolationLevel 模块导入到 tonilsm 包中,这样用户可以直接使用 from tonilsm import ... 的方式来访问核心功能。

3.3.3 动态链接库加载

tonilsm/core/__init__.py 中,我们需要加载动态链接库 lsm_pybind.so,以便在 Python 中使用 C++ 实现的功能。我们可以使用 importlib 模块来动态加载这个库。

tonilsm/core/__init__.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import sys
import importlib.util

# 获取当前目录
current_dir = os.path.dirname(os.path.abspath(__file__))

# 添加动态链接库目录到系统路径
lib_dir = os.path.join(current_dir, 'lib')
sys.path.append(lib_dir)

# 加载动态链接库
spec = importlib.util.spec_from_file_location("lsm_pybind", os.path.join(lib_dir, "lsm_pybind.so"))
lsm_pybind = importlib.util.module_from_spec(spec)
spec.loader.exec_module(lsm_pybind)

# 导出模块中的类和函数
from lsm_pybind import LSM, TwoMergeIterator, Level_Iterator, TranContext, IsolationLevel

__all__ = ['LSM', 'TwoMergeIterator', 'Level_Iterator', 'TranContext', 'IsolationLevel']

3.3.4 类型提示文件

类型提示文件 tonilsm/core/__init__.pyi 用于提供类型信息,帮助 IDE 和静态类型检查工具(如 mypy)更好地理解代码结构。这个文件的内容与 tonilsm/core/__init__.py 中的类和函数定义相对应。
这里我们需要定义 LSM 类、TwoMergeIterator 类、Level_Iterator 类、TranContext 类和 IsolationLevel 枚举类型的接口。
tonilsm/core/__init__.pyi 文件的内容如下:

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
29
30
31
32
33
34
35
36
from typing import *

class LSM:
def __init__(self, path: str) -> None: ...
def put(self, key: bytes, value: bytes) -> None: ...
def get(self, key: bytes) -> Optional[bytes]: ...
def remove(self, key: bytes) -> None: ...
def put_batch(self, kvs: List[Tuple[bytes, bytes]]) -> None: ...
def remove_batch(self, keys: List[bytes]) -> None: ...
def begin(self, tranc_id: int) -> 'TwoMergeIterator': ...
def end(self) -> 'TwoMergeIterator': ...
def begin_tran(self, isolation_level: 'IsolationLevel') -> 'TranContext': ...
def clear(self) -> None: ...
def flush(self) -> None: ...
def flush_all(self) -> None: ...

class TwoMergeIterator:
def __iter__(self) -> 'TwoMergeIterator': ...
def __next__(self) -> Tuple[bytes, bytes]: ...

class Level_Iterator:
def __iter__(self) -> 'Level_Iterator': ...
def __next__(self) -> Tuple[bytes, bytes]: ...

class TranContext:
def commit(self, test_fail: bool = False) -> None: ...
def abort(self) -> None: ...
def get(self, key: bytes) -> Optional[bytes]: ...
def remove(self, key: bytes) -> None: ...
def put(self, key: bytes, value: bytes) -> None: ...

class IsolationLevel(Enum):
READ_UNCOMMITTED: 'IsolationLevel'
READ_COMMITTED: 'IsolationLevel'
REPEATABLE_READ: 'IsolationLevel'
SERIALIZABLE: 'IsolationLevel'

3.3.5 安装脚本

安装脚本 tonilsm/setup.py 用于构建和安装 Python 包。这个脚本使用 setuptools 构建 Python 包:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import os
import platform
from setuptools import setup, find_packages
from setuptools.command.build_py import build_py

# 版本和基本描述
VERSION = "0.1.0"
DESCRIPTION = "A Python binding for Toni's LSM Tree Storage Engine"

class CustomBuild(build_py):
def run(self):
# 使用绝对路径确保准确性
build_lib_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../build/lib"))
package_lib_dir = os.path.join(self.build_lib, "tonilsm/core/lib")

# 创建目标目录
self.mkpath(package_lib_dir)

# 动态库文件列表
lib_files = [
("liblsm_shared.so", "liblsm_shared.so"),
("lsm_pybind.so", "lsm_pybind.so")
]

# 复制文件
for src_name, dst_name in lib_files:
src = os.path.join(build_lib_dir, src_name)
dst = os.path.join(package_lib_dir, dst_name)
if os.path.exists(src):
self.copy_file(src, dst)
else:
raise FileNotFoundError(f"Missing library file: {src}")

super().run()

setup(
name="tonilsm",
version=VERSION,
description=DESCRIPTION,
packages=find_packages(),
package_data={
"tonilsm.core": [
"lib/*.so",
"lib/*.dylib",
"lib/*.dll",
"lib/*.pyd"
]
},
cmdclass={"build_py": CustomBuild},
install_requires=["pybind11>=2.10"], # 需声明绑定依赖
python_requires=">=3.8",
author="Your Name",
classifiers=[
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
]
)

4 SDK 使用

现在我们可以直接通过Python来使用这个LSM存储引擎了, 这里我们给出一个简单的使用示例:

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
import tonilsm

db = tonilsm.LSM("test2_db")

db.put(b"tomxx", b"catxx")

db.get("tomxx")
# 'catxx'

t = db.begin_tran(isolation_level=tonilsm.IsolationLevel.READ_COMMITTED)

t.get('tomxx')
# 'catxx'

t.put('tomxx', '1')

t.get('tomxx')
# '1'

db.get("tomxx")
# 'catxx'

t.commit()
# True

db.get("tomxx")
# '1'

但这里其实有一个bug:

bug

调用clear方法后直接core dump了, 这个clear函数在C++文件中是正常使用的, 但不知道为什么到Python层就core dump了, 这个我暂时不知道什么原因。如果有网页感兴趣,可以看看这个issue

5 总结

在这一章中,我们介绍了如何使用 pybind11 来将C++代码绑定到 Python 中,并且实现了一个简单的 Python SDK。我们还介绍了如何组织 Python 包的结构,以及如何使用 setuptools 来构建和安装 Python 包。

这一章算是加餐内容吧, 基础的Python SDK已经实现了, 但有一些细节的bug和优化没有在之前的文章中提及, 因为比较琐碎, 写起来也比较复杂。

如果本项目有帮到你,请给个star吧,您的支持是我继续维护和开发这个项目的动力。如果你想自己从零开始手敲代码实现这个项目,欢迎支持我的付费视频课程:https://avo6166ew2u.feishu.cn/docx/LXmVdezdsoTBRaxC97WcHwGunOc