Skip to content

Commit 8f4bf6e

Browse files
committed
rust_src: Add a workaround to be able to work with any Python version (3.7+)
1 parent a2a3e3b commit 8f4bf6e

2 files changed

Lines changed: 243 additions & 2 deletions

File tree

src/rust_src/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
[package]
2-
name = "ring-python"
2+
name = "ring-python-impl"
33
version = "1.0.0"
44
edition = "2024"
5+
build = "build.rs"
56

67
[lib]
78
crate-type = ["cdylib"]
89

10+
[build-dependencies]
11+
cc = "1"
12+
913
[dependencies]
1014
libc = "0.2"
11-
pyo3 = { version = "0.28", features = ["auto-initialize"] }
15+
pyo3 = { version = "0.28", features = ["abi3-py37", "extension-module"] }
1216
ring-lang-rs = "0.1"
1317

1418
[profile.release]

src/rust_src/build.rs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
use std::env;
2+
use std::path::PathBuf;
3+
4+
fn main() {
5+
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
6+
let target_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
7+
.join("target")
8+
.join(env::var("PROFILE").unwrap_or("release".into()));
9+
10+
// Compile the loader .so
11+
// This tiny C library:
12+
// 1. Has ZERO Python symbol dependencies (loads cleanly via Ring's loadlib)
13+
// 2. dlopen's libpython with RTLD_GLOBAL (makes Python symbols available)
14+
// 3. dlopen's libring_python_impl.so (the real Rust/PyO3 library)
15+
// 4. Forwards ringlib_init to the real library
16+
let loader_c = out_dir.join("loader.c");
17+
std::fs::write(&loader_c, LOADER_SOURCE).unwrap();
18+
19+
// Platform-specific loader compilation
20+
let target = env::var("TARGET").unwrap_or_default();
21+
let is_windows = target.contains("windows");
22+
let is_macos = target.contains("apple") || target.contains("darwin");
23+
24+
if is_windows {
25+
// On Windows, the loader is compiled by the C compiler from the host
26+
// For cross-compilation from Linux CI, we skip loader compilation
27+
// and rely on the Rust cdylib being named ring_python_impl.dll
28+
// The loader .dll is built separately or by MSVC on Windows CI
29+
let loader_dll = target_dir.join("ring_python.dll");
30+
let status = std::process::Command::new("cl.exe")
31+
.args([
32+
"/LD", "/O2", "/Fe:", loader_dll.to_str().unwrap(),
33+
loader_c.to_str().unwrap(),
34+
])
35+
.status();
36+
// If cl.exe is not available (Linux CI), try gcc for Windows target
37+
if status.is_err() || !status.unwrap().success() {
38+
let status = std::process::Command::new("gcc")
39+
.args([
40+
"-shared", "-O2",
41+
"-o", loader_dll.to_str().unwrap(),
42+
loader_c.to_str().unwrap(),
43+
])
44+
.status()
45+
.expect("Failed to compile loader");
46+
assert!(status.success(), "Failed to compile loader .dll");
47+
}
48+
} else if is_macos {
49+
let loader_dylib = target_dir.join("libring_python.dylib");
50+
let status = std::process::Command::new("gcc")
51+
.args([
52+
"-shared", "-fPIC", "-O2",
53+
"-install_name", "@rpath/libring_python.dylib",
54+
"-o", loader_dylib.to_str().unwrap(),
55+
loader_c.to_str().unwrap(),
56+
"-ldl",
57+
])
58+
.status()
59+
.expect("Failed to compile loader");
60+
assert!(status.success(), "Failed to compile loader .dylib");
61+
} else {
62+
// Linux, FreeBSD, etc.
63+
let loader_so = target_dir.join("libring_python.so");
64+
let status = std::process::Command::new("gcc")
65+
.args([
66+
"-shared", "-fPIC", "-O2",
67+
"-Wl,-soname,libring_python.so",
68+
"-o", loader_so.to_str().unwrap(),
69+
loader_c.to_str().unwrap(),
70+
"-ldl",
71+
])
72+
.status()
73+
.expect("Failed to compile loader");
74+
assert!(status.success(), "Failed to compile loader .so");
75+
}
76+
77+
println!("cargo:rerun-if-changed=build.rs");
78+
}
79+
80+
const LOADER_SOURCE: &str = r#"
81+
/*
82+
* ring_python loader — loads libpython globally, then loads the real impl.
83+
* This file has ZERO Python dependencies so dlopen always succeeds.
84+
*/
85+
#ifdef _WIN32
86+
87+
#include <windows.h>
88+
#include <stdio.h>
89+
90+
typedef void (*ringlib_init_fn)(void*);
91+
static HMODULE real_lib = NULL;
92+
93+
static HMODULE try_load(const char* name) {
94+
return LoadLibraryA(name);
95+
}
96+
97+
static void load_python(void) {
98+
/* Try python3.dll (stable ABI on Windows) */
99+
if (try_load("python3.dll")) return;
100+
101+
/* Try version-specific */
102+
char buf[64];
103+
for (int minor = 30; minor >= 7; minor--) {
104+
snprintf(buf, sizeof(buf), "python3%d.dll", minor);
105+
if (try_load(buf)) return;
106+
}
107+
}
108+
109+
void ringlib_init(void* pRingState) {
110+
load_python();
111+
112+
/* Find impl next to this loader */
113+
char path[MAX_PATH];
114+
HMODULE self;
115+
GetModuleHandleExA(
116+
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
117+
(LPCSTR)ringlib_init, &self);
118+
GetModuleFileNameA(self, path, MAX_PATH);
119+
120+
/* Replace filename with impl name */
121+
char* slash = strrchr(path, '\\');
122+
if (!slash) slash = strrchr(path, '/');
123+
if (slash) slash[1] = '\0'; else path[0] = '\0';
124+
strcat(path, "ring_python_impl.dll");
125+
126+
real_lib = LoadLibraryA(path);
127+
if (!real_lib) {
128+
fprintf(stderr, "ring_python: cannot load %s (error %lu)\n", path, GetLastError());
129+
return;
130+
}
131+
132+
ringlib_init_fn real_init = (ringlib_init_fn)GetProcAddress(real_lib, "ringlib_init");
133+
if (!real_init) {
134+
fprintf(stderr, "ring_python: ringlib_init not found in impl\n");
135+
return;
136+
}
137+
real_init(pRingState);
138+
}
139+
140+
#else /* Unix (Linux, macOS, *BSD) */
141+
142+
#define _GNU_SOURCE
143+
#include <dlfcn.h>
144+
#include <stdio.h>
145+
#include <string.h>
146+
#include <stdlib.h>
147+
148+
typedef void (*ringlib_init_fn)(void*);
149+
static void* real_lib = NULL;
150+
151+
#ifdef __APPLE__
152+
#define RTLD_GLOBAL_FLAG 0x8
153+
#define LIBEXT "dylib"
154+
#define IMPLNAME "libring_python_impl.dylib"
155+
#else
156+
#define RTLD_GLOBAL_FLAG 0x100
157+
#define LIBEXT "so"
158+
#define IMPLNAME "libring_python_impl.so"
159+
#endif
160+
161+
static int try_dlopen(const char* name) {
162+
return dlopen(name, RTLD_LAZY | RTLD_GLOBAL_FLAG) != NULL;
163+
}
164+
165+
static void load_python_global(void) {
166+
/* Try generic name first */
167+
char buf[64];
168+
snprintf(buf, sizeof(buf), "libpython3.%s", LIBEXT);
169+
if (try_dlopen(buf)) return;
170+
171+
/* Try version-specific (newest first) */
172+
for (int minor = 30; minor >= 7; minor--) {
173+
snprintf(buf, sizeof(buf), "libpython3.%d.%s", minor, LIBEXT);
174+
if (try_dlopen(buf)) return;
175+
#ifndef __APPLE__
176+
snprintf(buf, sizeof(buf), "libpython3.%d.so.1.0", minor);
177+
if (try_dlopen(buf)) return;
178+
#endif
179+
}
180+
}
181+
182+
static void build_impl_path(const char* base, char* out, size_t sz) {
183+
const char* slash = strrchr(base, '/');
184+
if (slash) {
185+
int dirlen = (int)(slash - base);
186+
snprintf(out, sz, "%.*s/" IMPLNAME, dirlen, base);
187+
} else {
188+
snprintf(out, sz, IMPLNAME);
189+
}
190+
}
191+
192+
void ringlib_init(void* pRingState) {
193+
/* Step 1: Load libpython into global symbol table */
194+
load_python_global();
195+
196+
/* Step 2: Find and load the real impl library (next to this loader) */
197+
Dl_info info;
198+
char path[4096];
199+
real_lib = NULL;
200+
201+
if (dladdr((void*)ringlib_init, &info) && info.dli_fname) {
202+
/* Try next to the path the linker used (may be a symlink) */
203+
build_impl_path(info.dli_fname, path, sizeof(path));
204+
real_lib = dlopen(path, RTLD_LAZY | RTLD_GLOBAL_FLAG);
205+
206+
/* If not found, resolve symlinks and try next to the real file */
207+
if (!real_lib) {
208+
char resolved[4096];
209+
if (realpath(info.dli_fname, resolved)) {
210+
build_impl_path(resolved, path, sizeof(path));
211+
real_lib = dlopen(path, RTLD_LAZY | RTLD_GLOBAL_FLAG);
212+
}
213+
}
214+
}
215+
216+
/* Last resort: try bare name (relies on LD_LIBRARY_PATH / system paths) */
217+
if (!real_lib) {
218+
snprintf(path, sizeof(path), IMPLNAME);
219+
real_lib = dlopen(path, RTLD_LAZY | RTLD_GLOBAL_FLAG);
220+
}
221+
222+
if (!real_lib) {
223+
fprintf(stderr, "ring_python: cannot load " IMPLNAME ": %s\n", dlerror());
224+
return;
225+
}
226+
227+
/* Step 3: Forward to real ringlib_init */
228+
ringlib_init_fn real_init = (ringlib_init_fn)dlsym(real_lib, "ringlib_init");
229+
if (!real_init) {
230+
fprintf(stderr, "ring_python: ringlib_init not found in %s\n", path);
231+
return;
232+
}
233+
real_init(pRingState);
234+
}
235+
236+
#endif
237+
"#;

0 commit comments

Comments
 (0)