https://blog.frankel.ch/rust-from-python/ A Java geek * Me * Books * Speaking * Mentions * Focus Oct 8, 2023 / HTTP, IPC, FFI, RUST, PYTHON Calling Rust from Python I recently watched GOTO conferences' talk Calling Functions Across Languages by Richard Feldman. I'm afraid I have to disagree with using the term 'language' in this context. It's a no-brainer to call Java from Kotlin or Scala or to call Java from Kotlin. Hence, in the rest, I'll use 'stack'. In the talk, the speaker cites two main reasons to go on this road: * Gradual migration from one stack to the other * Using a library that has no equivalent in one's stack under the assumption that it's too expensive to implement it * Performance, e.g., use a library "closer" to the metal * On the opposite, developer experience, valuing ergonomics at the expense of performance All reasons make a lot of sense and depend on the context. He then proceeds to demo three methods to call a function developed in one stack from a different stack: * HTTP: I assume every reader is more or less familiar with HTTP * Inter-Process Communication: given the prevalence of Nix-based systems, the implementation is Unix Socket-based. In short, a dedicated file allows writing and reading from different processes. * Foreign Function Interface: The idea is to make a library out of the code of one's stack and load that library from the other stack. In general, C serves as the bridge between both stacks. The demo shows that moving from HTTP to IPC, then from IPC to FFI, removes boilerplate code. It uses JavaScript as the client stack and Ruby as the underlying stack. While JavaScript makes sense, using Ruby, a dynamic language not famous for its performance, is not a good fit IMHO. The demo I already wrote about calling Rust from Java using JNI, the JVM FFI implementation. However, in this post, I want to use Python as the client stack and Rust as the target stack. I want to improve my knowledge in both, and it perfectly fits the performance reason mentioned above. The example shall delegate complex number computations from Python to Rust. Even though Python supports them natively, it's good enough for our goal. Moreover, it allows parsing and displaying them without extra code. The idea is to use a CLI to do the computation, e.g.: python main.py --add --arg1 1+3j --arg2 -5j We expect the following output: (1-2j) For command handling, we will leverage the click library: Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the "Command Line Interface Creation Kit". It's highly configurable but comes with sensible defaults out of the box. -- click Here's the "wrapper" code: @click.command() @click.option('--add', 'command', flag_value='add') @click.option('--sub', 'command', flag_value='sub') @click.option('--mul', 'command', flag_value='mul') @click.option('--arg1', help='First complex number in the form x+yj') @click.option('--arg2', help='Second complex number in the form x\'+y\'j') def cli(command: Optional[str], arg1: Optional[str], arg2: Optional[str]) -> None: #result: complex = compute result #print(result) if __name__ == '__main__': cli() Now that it's settled, let's check the options mentioned above. HTTP HTTP is the most straightforward approach from a developer's point of view, even though it's the worst in terms of performance: we need to go through lots of the OSI model layers. Even if the client and the server and the client implement the same stack, HTTP mandates a serialization format. Despite my love of XML, JSON is the most widespread one. On the design level, we will use the HTTP POST method to be able to send body data along the request. We map each operation to a URL, e.g., add and sub. Python client While Python supports HTTP via the http package out-of-the-box, the requests library is a tremendous help. json: dict[str, list[float]] = dict(a=[n1.real, n1.imag], b=[n2.real, n2.imag]) (1) req = requests.post(f'http://localhost:3000/{command}', json=json) (2) body: dict[str: list[float]] = req.json() (3) if body and 'result' in body: real: Optional[str] = body['result'][0] imaginary: Optional[str] = body['result'][1] return complex(float(real), float(imaginary)) (4) raise Exception() 1 Create the request body 2 Send the post request to the Rust service with the payload 3 Get the response body 4 Create a Python complex from the response body Rust server I'll use axum, as it's the framework I'm most familiar with: + Route requests to handlers with a macro free API. + Declaratively parse requests using extractors. + Simple and predictable error handling model. + Generate responses with minimal boilerplate. + Take full advantage of the tower and tower-http ecosystem of middleware, services, and utilities. -- axum README We shall first model the input and output: #[derive(Deserialize)] struct Input { a: Complex, b: Complex, } #[derive(Serialize)] struct Output { result: Complex } Next, we need to provide an async function to manage the operation: async fn add(Json(payload): Json) -> impl IntoResponse { Json(Output { result: payload.a + payload.b }) } Finally, we can scaffold the routes and the server itself: #[tokio::main] (1) async fn main() { let app = axum::Router::new() (2) .route("/add", post(add)) (3) .route("/sub", post(sub)) (3) .route("/mul", post(mul)); (3) axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) (4) .serve(app.into_make_service()) (5) .await .unwrap() } 1 tokio macro to use the async main function as an entry-point 2 Create the router object 3 Add each function under its dedicated subpath 4 Create an HTTP server that binds to port 3000 5 Make the magic happen Inter-Process Communication We don't need HTTP because the client and the server are on the same machine. Henceforth, we can replace HTTP with IPC. On Nix-based systems, we can leverage domain sockets: The API for Unix domain sockets is similar to that of an Internet socket, but rather than using an underlying network protocol, all communication occurs entirely within the operating system kernel. Unix domain sockets may use the file system as their address name space. (Some operating systems, like Linux, offer additional namespaces.) Processes reference Unix domain sockets as file system inodes, so two processes can communicate by opening the same socket. -- Unix domain socket on Wikipedia Python client Python natively supports Unix sockets via the `socket ' package. with socket.socket(socket.AF_UNIX) as client: (1) data: dict[str, list[float]] = dict(command=command,a=[n1.real, n1.imag], b=[n2.real, n2.imag]) encoded: bytes = json.dumps(data).encode('UTF-8') (2) client.connect("/tmp/socket") (3) client.send(encoded) (4) 1 Create a socket client 2 Encode the JSON string to bytes 3 Connect to the socket 4 Send data From a network perspective, it's much more straightforward. From a developer perspective, we only need to transform the JSON to bytes. Rust server Rust also natively supports Unix sockets. let srv_path = "/tmp/socket"; (1) if metadata(srv_path).is_ok() { remove_file(srv_path).unwrap(); (2) } let listener = UnixListener::bind(srv_path).unwrap(); (3) loop { let (mut stream, _) = listener.accept().unwrap(); (4) let mut payload = String::new(); (5) let _ = stream.read_to_string(&mut payload); (6) println! {"Received {}", payload}; let input = serde_json::from_str::(&payload).unwrap(); (7) let output = &command(input); let result = serde_json::to_string(output).unwrap(); (8) stream.write(result.as_bytes()).unwrap_or_default(); (9) println! {"Sent {}", result}; } 1 A Unix socket is a specific kind of file 2 Clean up the potential previous socket 3 Bind the socket to the path 4 Listen to the socket for data 5 Create a new mutable resizable string 6 Read socket data into the string 7 Deserialize input string to JSON 8 Serialize output string to JSON 9 Send data back to the socket I have no prior experience with Unix sockets. I can send data from Python to Rust but cannot return the data (point 9 above). Any help on the subject would be greatly appreciated. Foreign Function Interface So far, calling Rust from Python required passing data via the network, either (potentially) remote or local. FFI allows keeping everything in a single process. However, we need a way to pass data across different tech stacks. For historical reasons, most provide a bridge for calling C-based libraries. Indeed, we can compile the Rust code to a C-compatible library and call it from Python. Python client Python allows loading C libraries via the ctypes package: ctypes is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python. -- Python FF library Depending on the platform, the library can be a .dll (Windows), .so (Nix) or .dylib (OSX). I find myself on the latter. Hence, I can load it with: rust = ctypes.CDLL('/path/to/lib/my.dylib') From this point on, we can call any function defined in the library via the rust variable. The remaining issue is that Python types are not Rust types. Hence, we need to use C types on both sides. from ctypes import c_double rust.compute(command.encode("UTF-8"), c_double(n1.real), c_double(n1.imag), c_double(n2.real), c_double(n2.imag)) The c_double function makes a C double out of a Python float. We can even re-use our custom classes via the Structure class: Structures and unions must derive from the Structure and Union base classes which are defined in the ctypes module. Each subclass must define a fields attribute. fields must be a list of 2-tuples, containing a field name and a field type. -- Structures and unions As we know the form of the Rust Complex, that's easy to provide: class RustComplex(Structure): _fields_ = [("re", c_double), (1) ("im", c_double)] (2) def __init__(self, c: complex, *args: Any, **kw: Any) -> None: (3) super().__init__(*args, **kw) self.re = c.real self.im = c.imag 1 As per the documentation 2 Constructor for ease of use The final bit is to set the return type of the compute function. By default, it's an int: By default functions are assumed to return the C int type. Other return types can be specified by setting the restype attribute of the function object. -- Return types rust.compute.restype = RustComplex (1) return rust.compute(command.encode("UTF-8"), RustComplex(n1), RustComplex(n2)) (2) 1 Set the compute return type 2 Leverage the class created above Rust library In Rust, the std::ffi module supports FFI: Utilities related to FFI bindings. This module provides utilities to handle data across non-Rust interfaces, like other programming languages and the underlying operating system. It is mainly of use for FFI (Foreign Function Interface) bindings and code that needs to exchange C-like strings with other languages. -- Module std::ffi The main difference with previous steps is that we no longer want an app but a library. We need to rename our main.rs file to lib.rs. Additionally, we have to configure the project to create a lib: Cargo.toml [lib] name = "my" crate-type = ["dylib"] Besides, the only dependency we need is complex. However, the code becomes very straightforward: lib.rs #[no_mangle] (1) fn compute(c_string_ptr: *const c_char, a: Complex, b: Complex) -> Complex { (2) let bytes = unsafe { CStr::from_ptr(c_string_ptr).to_bytes() }; (3) let command = str::from_utf8(bytes).unwrap(); match command { "add" => a + b, "sub" => a - b, "mul" => a * b, _ => panic!("Unknown command"), } } 1 Keep symbol names for external usage Use compatible types. The first parameter is a pointer to a C 2 string, the others are regular structures since Python will pass them in the expected format 3 Transform a C string into a Rust string At this point, we can now use the Rust code inside the Python process. Conclusion In this post, we detailed three different ways to call Rust from Python: HTTP, IPC, and FFI. FFI is the most performant one but is only sometimes possible. The IPC alternative is good on a single machine. When both fail, one can always rely on HTTP. The complete source code for this post can be found on Github. To go further: * Calling Functions Across Languages * Inter-process communication * Foreign function interface * Calling Rust from Python * ctypes -- A foreign function library for Python Follow @nicolas_frankel Nicolas Frankel Nicolas Frankel Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author. Read More -- A Java geek -- Http * Leverage the richness of HTTP status codes * Web resource caching: Server-side * Web resource caching: Client-side See all 4 posts - Oct 1, 2023 performance imgproxy Apache APISIX Resizing images on-the-fly As a web architect, one of the many issues is asset management. And the most significant issue in assets is images. A naive approach would be to set an image and let the browser resize the image via CSS: img { height: 100%; width: 100%; object-fit: contain; } However, it means that you download the original image. It entails two problems: the size of the original image and the suboptimal browser-based resizing. This post will cover two alternatives: traditional and brand-new s [me_small] Nicolas Frankel A Java geek -- Calling Rust from Python Share this A Java geek (c) 2008-2023 v. 30ac56176eee59c2866b60ffcb58094368320a52/ 5246981005 Latest Posts