惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
PCI Perspectives
PCI Perspectives
H
Help Net Security
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网
S
Secure Thoughts
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
大猫的无限游戏
大猫的无限游戏
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Security @ Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
T
Tor Project blog
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
博客园 - 【当耐特】
G
Google Developers Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
I
Intezer
S
Schneier on Security
S
Securelist
W
WeLiveSecurity
C
Cybersecurity and Infrastructure Security Agency CISA
P
Palo Alto Networks Blog
Scott Helme
Scott Helme
Project Zero
Project Zero
Google Online Security Blog
Google Online Security Blog
T
Threatpost
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Engineering at Meta
Engineering at Meta
Blog — PlanetScale
Blog — PlanetScale
V
Visual Studio Blog
Last Week in AI
Last Week in AI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
A
Arctic Wolf
GbyAI
GbyAI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog

hsfzxjy 的博客

解决 VSCode + CMake + MSVC 编译器信息乱码的问题 使用 3090 部署 1.58bit 动态量化版 DeepSeek R1 671b 如何在 VS Code DevContainer 中配置 HTTP 代理 如何在跳板机背后的服务器上使用 VS Code Remote - Containers Cohesive Digests for Ints and Floats Rust 中的隐匿概念 —— Place(位置) 美术馆 一尺之槌,日取其半,1075日而竭 老生常谈:使用 Cloudflare 自选 IP 加速站点访问 辩义 State、Nation 与 Country 将 Base64 编码的数据快速转换为 Uint8Array 折腾 NPU·第1章 —— 搭建 Level Zero 开发环境 折腾 NPU·第0章 —— Intel NPU 概述与 Level-Zero 新增域名 monad.run CSS 中为特定字符设置不同字体 Arbitary Lifetime Transmutation via Rust Unsoundness Dijkstra 算法的延伸 Manacher 回文计数算法 硬卧 Go Fact: Zero-sized Field at the Rear of a Struct Has Non-zero Size Display *big.Rat Losslessly and Smartly in Golang 代码的仪式 Building Electron From Scratch 中式亲属称谓研究之一:构建半群 Some Notes on Kotlin Coroutines Git sparse-checkout and partial clones for Mega-Repos 辩义“封建” Diving from the CUDA Error 804 into a bug of libnvidia-container Modern Cryptography, GPG and Integration with Git(hub) Move the Root Partition of Ubuntu A New Programmer Kicks a Roadblock Git-based Dependencies in Dart and Go Reversy Naming 人类一败涂地 Invalid Golang Pointers Can Bite You Even If You Don't Dereference Side Project(副业) A Flaw of Promoting Complex Trait Bounds in Rust Initialize Process Pool Worker with Individual Value [Extending Hexo For My Site] Part 1 [Extending Hexo For My Site] Part 0 Debug a 'torch.tensor(1).cuda()' hanging 不自由的互联网 Retrieve Contents over HTTP without curl or wget [Unravelling mocona] Part 1 - Verbosity or Anti-Pattern [Unravelling mocona] Part 0 - Preface Understanding pickle in Python Rough Notes on Deploying Vaultwarden & NextCloud Bookmarks 语言狂热者与实用主义者 Demystify the randomness in CUDA kernels Performant Bulk Mutations in IndexedDB Auto Rebuild .pyx Files with pyximport Cython and Threads Obtain a Random Available TCP Port with Bash Information Theory: KL Divergence Information Theory: Entropy and Mutual Information 铁板烧 西郊线 Proof of the Gumbel Max Trick Option::as_ref Rc, RefCell and Interior Mutability Visualizing Correlation 三月十日杂感 三月一日杂感 二月十一日杂感 一月二十六日杂感 SS Configuration 一月七日杂感 四月·病 Haskell 笔记:State Monad Haskell 笔记:Monad 引论 Haskell 笔记:Applicative Haskell 笔记:Category Theory and Functor Haskell 笔记:data, type, newtype Haskell 笔记:folds 使用 Aria2 在 Ubuntu 中下载百度云资源 从伪并行的 Python 多线程说起 一个 Reentrant Error 引发的对 Python 信号机制的探索和思考 Linux 文件权限 HSFZMUN 4.0 部署小记 午后雨·科大 最后的雨夜·广州 揭秘·变态的平方根倒数算法 神坑·Python 装饰类无限递归 Python“黑魔法”之 Encoding & Decoding Ubuntu 重新映射键盘布局 为什么我要翻墙 Python“黑魔法”之 Generator Coroutines 数学美 之 判断线段相交的最简方法 除夕杂感 17 行代码实现的简易 Javascript 字符串模板 Python“黑魔法”之 Meta Classes 诗集 生活,需要被“发现” 家书·十八岁成人礼 炫技?还是需求? 【译】响应式图片的现状 【译】“为什么有这么多的编程语言?” Wisecity 商赛总结——也谈前端自动化测试 记一次 DoS 诈骗网站的经历 那一年,我们望向星空
Rust - Python FFI From Scratch
2022-03-12 · via hsfzxjy 的博客

I was recently working on a side project that involves communication between binaries written in Rust and web interfaces written in Python. Moving a part of my project onto a language like Rust is under several considerations: 1) the logic is all about manipulating byte arrays, where Python has deficit and system language like Rust is superior; 2) the logic happens to be complicated, I need a static type system to ensure the correctness, and also the match expression of Rust is found helpful in getting things concise; 3) I was planning to develop CLI tools with Rust, which calls this fraction of functionality, and I don’t want to rewrite the stuff in the future.

For half a day, I was reading about FFI between the two languages, and finally got everything work as intended. Before stepping into the technical sharing, I would firstly set up the stage of our story.

My project stores a list of “widgets” in local files on the server side. As of what “widgets” are, it’s sufficient to know that they are stored in a compact binary format. A web endpoint recieves user-provided modification in a similar format, applies them on existing widgets and writes the new widgets back to the files. Technically, the endpoint has two byte arrays as input, mods extracted from the HTTP request and widgets read from local files, which are processed at the Rust side by a function named mod_widgets(), and yields one byte array new_widgets as output.

Rust SidePython Sidemod_widgets()consumenew_widgets...new_widgetsrecieved modswidgets,mods

“byte array” here refers to an array of consecutively stored unsigned 8-bit integers, which is a language-agnostic term. Different languages may adopt their own types or representations for this term, e.g., bytearray in Python, Vec<u8> in Rust or Uint8Array in Javascript.

I have learned about PyO3, a fascinating framework for writing Python extensions in Rust. But this time I choose not to use it but build everything from scratch. The data passed around is in a basic representation of byte array, simple enough even without the help of PyO3. Besides, building from scratch will give me a better understanding of how the gear works, and how to accomplish in a memory-safe 1 and efficient 2 way.

To this end, I was going to write a dynamic library in Rust, from which the Python code calls certain functions to do the job. The post is organized in three parts. The first part briefly introduces the fundamental of calling functions written by a different programming language, and the following parts show detailed implementation in both Rust and Python.

FFI Fundamental

There’s technical term FFI 3 to describe the mechanism of calling functions from a different programming language. One of the common ways to do FFI is wrapping the functions written in one language in a dynamically linked library 4, which is then loaded from another language. However in practice, there’re tons of troubles to concern about and coding can get really cumbersome.

One of the troubles is ABI compatibility. ABI here refers to Application Binary Inteface, an interface between two binary program modules. The term is less familiar to whom work at a higher level such as Python users, but it does exist everywhere in your computer.

When a program calls functions from a library, they should conform the same conventions in various aspects, such as calling conventions 5, memory layout 6, etc.. For example, C organizes struct fields in the order of their definition, while Rust by default does not guarantee. There’re also other details like name mangling, where Rust may give the function a different name when producing bianries. If not turned off, the functions can not be located from the Python side.

Currently, most foreign code exposes a C ABI, i.e., the ABI generated by the C compiler on your platform. While not standardized, the mechanism serves as a widely used ABI in many cases.

Another concern is the implications of different languages that might cause undesired behavior on the other side. For example, in GC-based languages like Python, if you pass the pointer to an variable to a foreign function and then drop the variable, the variable might be recycled and the pointer becomes invalid at the other side:

def func_getb():
a = bytearray(...)
b = ffi_atob(ptr_of(a))
return b


def func_btoc(b):
return ffi_btoc(b)

b = func_getb()
c = func_btoc(b)

Similar cases exist in Rust. If one allocates a Vec<u8> on the stack and passes out its inner raw pointer, the vec will be dropped at the end of the function and invalidates the pointer:

pub unsafe extern "C" fn foo() -> *const u8 {
let v = vec![0u8; 1024];
v.as_ptr()

}

Exception handling is another headache. Since different languages have their own approaches to propagate exceptions, it’s essential to stop them before crossing the binary boundary, or otherwise undefined behaviors will occur or even the whole program be crashed.

In this story, I follow the convention of using C ABI as the bridge of foreign calling, that is, exposing interfaces with extern "C" at Rust and calling with ctypes at Python. Efforts also should be my to ensure exception and memory safety.

The Rust Side

Let’s start by creating a new cargo package. Simply open a terminal in a new directory and type cargo init --lib. The brilliant Rust package manager will prepare the scaffold to build a shared library. We then add the following lines to the manifest file Cargo.toml


[lib]
crate-type = ["cdylib"]

As described in the doc, the crate-type = ["cdylib"] is essential for generating FFI-purposed libraries, without which the library won’t be built with a stable ABI.

Our core implementation will go into the file lib.rs. In this simple library, we are going to expose a function mod_widgets. Let’s write down the a skeleton for it


#[no_mangle]
pub unsafe extern "C" fn mod_widgets(widgets: *mut FFIVec, mods: *mut FFIVec) -> *const FFIVec {
todo!()
}

#[no_mangle] here disables name mangling for this function. It will therefore appear in the symbol table with exactly the name mod_widgets and become accessible from outside. The extern "C" syntax tells the compiler that our function should conform the C ABI, so that other binaries can call it with the same convention.

FFIVec is a struct encapsulates a byte array and acts as a bridge to exchange data between the two languages. The details will be introduced shortly later, but before which I would like to clarify a fact, that is, there actually exist two kinds of byte arrays in our story

  1. Byte arrays created at Python and accessed from Rust;
  2. Byte arrays created at Rust and accessed from Python.

Why is it important? Because objects created at one language cannot be de-allocated at the other side. Imagine, for instance, you apply for a loan at Bank A but later you pay back the money to Bank B. Bank B will have no idea how to deal with the money and Bank A will never remove your loan record, causing a mess in both sides. Each language has its own and distinct implementation for memory allocation, and we must pair the allocator with corresponding de-allocator. We must keep this in mind when crafting the details of FFIVec.

Byte Arrays from Python

To access the first kind of byte array in Rust, it is sufficient to use the following data structure, which tells the starting location and the length of the array

#[repr(C)]
#[derive(Debug)]
pub struct FFIVec {
len: usize,
data: *const u8,
}

The attribute #[repr(C)] enforces the compiler to use a memory layout that conform the C ABI. Otherwise Rust code might read unaligned or corrupted data.

Code at Python side can construct a similar struct using something like ctypes and pass its reference to mod_widgets(). Code at Rust side then re-interprets the *mut FFIVec pointer to type &[u8] for easier further processing, with the help of function FFIVec::raw_to_slice()

impl FFIVec {
unsafe fn raw_to_slice<'a>(ptr: *mut Self) -> &'a [u8] {
let v = Box::from_raw(ptr);
let slice = core::slice::from_raw_parts(v.data, v.len);

std::mem::forget(v);
slice
}
}

It’s worth noting that raw_to_slice() does not perform buffer copying, and therefore impose little runtime overhead, which is efficient.

Byte Arrays from Rust

As of returning a byte array to Python (i.e., the second kind), however, things get a bit complicated. Remember the pre-caution above – if a piece of memory is allocated by Rust code, it should eventually be de-allocated by Rust code. Our library thus need to export another function

#[no_mangle]
pub extern "C" fn free_ffi_vec(v: *mut FFIVec) {
todo!()
}

The Python code is responsible to call free_ffi_vec() manually after consuming the FFIVec struct returned by mod_widgets(). Our working flow is thus extended into

Rust SidePython Sidemod_widgets()consumenew_widgets...new_widgetsfree_ffi_vec()received modswidgets, modsnew_widgets

At Rust side, the duty of free_ffi_vec() is to de-allocate the underlying memory, given the argument of type *mut FFIVec as handle. However, it’s not that trivial as we thought, if we stick to a design of FFIVec like above.

To allocate a byte array, a “Rust-ic” and common way is by constructing a Vec<u8>. It’s straight-forward to write a function that converts a Vec<u8> into *mut FFIVec, in order that it could be pass across the FFI boundary

impl FFIVec {
fn from_vec(vec: Vec<u8>) -> *mut Self {

let v = Box::new(Self {
len: vec.len(),
data: vec.as_ptr(),
});
std::mem::forget(vec);
Box::into_raw(v)
}
}

Fairly neat and sound. But it upsets you soon, since there’s no way to implement free_ffi_vec() with such a design!

The problem is, our only way to do de-allocation is recovering the *mut FFIVec back to a Vec<u8> and call the drop() function. However, the recover job is impossible, since we “lose” it after the forget() calling.

As a workaround, we have to make our FFIVec a litte bit “fatter”, and invite the original vec to live within the struct. Namely, the implementation is modified into

#[repr(C)]
#[derive(Debug)]
pub struct FFIVec {
len: usize,
data: *const u8,

storage: *mut Vec<u8>,

}

impl FFIVec {
fn from_vec(vec: Vec<u8>) -> *mut Self {

let vec = Box::new(vec);

let v = Box::new(Self {
len: vec.len(),
data: vec.as_ptr(),

storage: Box::into_raw(vec),

});
Box::into_raw(v)
}
}

A pointer to the original vec is stored as a member in the new design of FFIVec. We now are able to recover it even with a bare *mut FFIVec, and happy to implement the de-allocation

#[no_mangle]
pub extern "C" fn free_ffi_vec(v: *mut FFIVec) {
if v != 0 as *mut FFIVec {
unsafe {
let v = Box::from_raw(v);
drop(Box::from_raw(v.storage));
drop(v)
}
}
}

Don’t Panic

As aforementioned, we have to care a bunch of implications of Rust, and panic is one of them. The Python interpreter have no idea of who it is interacting with, not even the alien mechanism of panic. It will goes “really panick” if such things cross the boundary, and it’s our duty to stop them just at Rust side.

There’re many ways to avoid panics. For example, always do checking before .wrap() or something alike. In this story, I adopt a rather brutal one which is using std::panic::catch_unwind() 7. This guy behaves as a top level try-catch in other languages that invokes a closure and blocks potential panics from leaking. More information on this function can be found in the doc.

Now put them together, we finish the part of code to bridge the FFI gap at Rust side

#[repr(C)]
#[derive(Debug)]
pub struct FFIVec {
len: usize,
data: *const u8,
storage: *mut Vec<u8>,
}

impl FFIVec {
unsafe fn raw_to_slice<'a>(ptr: *mut Self) -> &'a [u8] {
let v = Box::from_raw(ptr);
let slice = core::slice::from_raw_parts(v.data, v.len);
std::mem::forget(v);
slice
}
fn from_vec(vec: Vec<u8>) -> *mut Self {
let vec = Box::new(vec);
let v = Box::new(Self {
len: vec.len(),
data: vec.as_ptr(),
storage: Box::into_raw(vec),
});
Box::into_raw(v)
}
}

#[no_mangle]
pub extern "C" fn free_ffi_vec(v: *mut FFIVec) {
if v != 0 as *mut FFIVec {
unsafe {
let v = Box::from_raw(v);
drop(Box::from_raw(v.storage));
drop(v)
}
}
}

#[no_mangle]
pub unsafe extern "C" fn mod_widgets(widgets: *mut FFIVec, mods: *mut FFIVec) -> *const FFIVec {
std::panic::catch_unwind(|| {
let widgets: &[u8] = FFIVec::raw_to_slice(widgets);
let mods: &[u8] = FFIVec::raw_to_slice(mods);
let new_widgets: Vec<u8> = play_with(widgets, mods);
FFIVec::from_vec(new_widgets)
})

.unwrap_or(0 as *const FFIVec)
}

The Python Side

It is now half the battle! And the rest half is easier to conquer.

To play with the shared library generated by Rust, we need the ctypes module. ctypes is the FFI library for Python. It provides facilities to compose or read from C data structures, and allows calling functions from a shared library. I won’t give an overall introduction to the library since it’s such a huge project, but rather focus on what we would use.

The Data Bridge

We would first deal with data bridge. As a peer to FFIVec in Rust, we should also declare the same struct at Python side

import ctypes

class FFIVec(ctypes.Structure):
_fields_ = [
("len", ctypes.c_size_t),
("data", ctypes.c_void_p),
("_storage", ctypes.c_void_p),
]

The syntax is straight-forward. ctypes provides various data types like c_size_t or c_void_p to represents their correspondences in C. These types work as guidance for data conversion. For example, when an Python int object (which has varied length) passed to a c_size_t, ctypes would know that it should be casted into an 8-bit unsigned integer (on 64-bit platform), and vice versa.

The FFIVec class is only a broker. Just like in Rust, we must implement the conversion between it and some more common Python data types, e.g., bytearray or bytes, to make it useful. The two methods below would work for the purpose

class FFIVec(...):
...
@classmethod
def from_bytearray(cls, buf: bytearray) -> "FFIVec":
l = len(buf)
ptr = (ctypes.c_uint8 * l).from_buffer(buf)
data = ctypes.cast(ptr, ctypes.c_void_p)
return cls(len=length, data=data, _storage=None)

def to_bytes(self) -> bytes:
ptr = (ctypes.c_uint8 * self.len).from_address(self.data)
return bytes(ptr)

The class method from_bytearray() creates an FFIVec instance from a given bytearray object. The instance shares the same memory view with that bytearray, and no memory copying occurs. The instance method to_bytes() instead constructs a bytes object, which the underlying memory is copied into.

You may notice the c_uint8 * l syntax. The expression is not doing arithmetics, but creates an array type. It’s equivalent to C uint8_t array type with fixed length l. With this type, we can access the buffer address of a bytearray object, or pump data into a bytes object.

Call the Functions

There’s one last piece in our puzzle – to interact with the functions from a foreign language. But firstly, we should load the shared library. This can be handled by ctypes.cdll.LoadLibrary

libw = ctypes.cdll.LoadLibrary("path/to/libmod_widgets.so")

After that, we declare the functions to be called, by attributing libw

PFFIVec = ctypes.POINTER(FFIVec)

mod_widgets = libw.mod_widgets
mod_widgets.argtypes = (PFFIVec, PFFIVec)
mod_widgets.restype = PFFIVec

free_ffi_vec = libw.free_ffi_vec
free_ffi_vec.argtypes = (PFFIVec,)

In the above code, we also specify the argument and return types of the foreign functions. This is not required 8, but I recommend to do so whenever possible. Specifying the types can avoid data being incorrectly coerced by ctypes.

That’s it! The functions mod_widgets and free_ffi_vec can now be called just like ordinary Python functions

old_widgets = bytearray(...)
mods = bytearray(...)
new_widgets = mod_widgets(
FFIVec.from_bytes(old_widgets),
FFIVec.from_bytes(mods),
).contents

new_widgets_bytes = new_widgets.to_bytes()
free_ffi_vec(new_widgets)

Cheers!

Epilogue

This post showcases a simple yet practical example to call functions written in Rust from Python. I deliberately built all the stuff from scratch. Working at such a low level forces me to think carefully about potential edge cases, which I have never noticed before. It’s quite an entertainment and I learnt a good lesson.


Author: hsfzxjy.
Link: .
License: CC BY-NC-ND 4.0.
All rights reserved by the author.
Commercial use of this post in any form is NOT permitted.
Non-commercial use of this post should be attributed with this block of text.