ct_python/
lib.rs

1//! Execute Python code at compile time to generate Rust code.
2//!
3//! # Example
4//!
5//! ```
6//! use ct_python::ct_python;
7//!
8//! static SIN_2: f64 = ct_python! {
9//!     from math import sin
10//!     print(sin(2))
11//! };
12//!
13//! ct_python! {
14//!     print("type num = f64;")
15//! }
16//!
17//! fn main() {
18//!     assert_eq!(num::sin(2.0), SIN_2);
19//! }
20//! ```
21//!
22//! # How to use
23//!
24//! Use the `ct_python!{..}` macro to generate Rust code from an embedded
25//! Python script.
26//! The output of the script (`print()` and anything else through `sys.stdout`)
27//! is captured, and will be parsed and injected as Rust code.
28//!
29//! ## Python Errors
30//!
31//! Any syntax errors and runtime exceptions from the Python code will be
32//! reported by the Rust compiler as compiler errors.
33//!
34//! ## Syntax issues
35//!
36//! Since the Rust tokenizer will tokenize the Python code, some valid Python
37//! code is rejected. See [the `inline-python` documentation][1] for details.
38//!
39//! [1]: https://docs.rs/inline-python/#syntax-issues
40
41#![feature(proc_macro_span)]
42
43use proc_macro::{Span, TokenStream};
44use pyo3::{PyObject, PyResult, Python, prelude::*};
45use std::{ffi::CString, ptr::null_mut, str::FromStr};
46
47mod shared;
48use shared::*;
49
50/// A block of compile-time executed Rust code generating Python code.
51///
52/// See [the crate's module level documentation](index.html) for examples.
53#[proc_macro]
54pub fn ct_python(input: TokenStream) -> TokenStream {
55	ct_python_impl(input).unwrap_or_else(|e| e)
56}
57
58fn ct_python_impl(input: TokenStream) -> Result<TokenStream, TokenStream> {
59	let python = CString::new(python_from_macro(input.clone(), None)?).unwrap();
60	let filename = CString::new(Span::call_site().file()).unwrap();
61
62	Python::with_gil(|py| {
63		let code = compile_python(py, &python, &filename, input.clone())?;
64		let output = run_and_capture(py, code).map_err(|err| python_error_to_compile_error(py, err, input))?;
65		TokenStream::from_str(&output).map_err(|_| compile_error(None, "produced invalid Rust code"))
66	})
67}
68
69fn run_and_capture(py: Python, code: PyObject) -> PyResult<String> {
70	#[cfg(unix)]
71	let _ = ensure_libpython_symbols_loaded(py);
72
73	let globals = py.import("__main__")?.dict().copy()?;
74
75	let sys = py.import("sys")?;
76	let stdout = py.import("io")?.getattr("StringIO")?.call0()?;
77	let original_stdout = sys.dict().get_item("stdout")?;
78	sys.dict().set_item("stdout", &stdout)?;
79
80	let result = unsafe {
81		let ptr = pyo3::ffi::PyEval_EvalCode(code.as_ptr(), globals.as_ptr(), null_mut());
82		PyObject::from_owned_ptr_or_err(py, ptr)
83	};
84
85	sys.dict().set_item("stdout", original_stdout)?;
86
87	result?;
88
89	stdout.call_method0("getvalue")?.extract()
90}
91
92#[cfg(unix)]
93fn ensure_libpython_symbols_loaded(py: Python) -> PyResult<()> {
94	// On Unix, Rustc loads proc-macro crates with RTLD_LOCAL, which (at least
95	// on Linux) means all their dependencies (in our case: libpython) don't
96	// get their symbols made available globally either. This means that
97	// loading modules (e.g. `import math`) will fail, as those modules refer
98	// back to symbols of libpython.
99	//
100	// This function tries to (re)load the right version of libpython, but this
101	// time with RTLD_GLOBAL enabled.
102	let sysconfig = py.import("sysconfig")?;
103	let libdir: String = sysconfig.getattr("get_config_var")?.call1(("LIBDIR",))?.extract()?;
104	let so_name: String = sysconfig.getattr("get_config_var")?.call1(("INSTSONAME",))?.extract()?;
105	let path = CString::new(format!("{libdir}/{so_name}")).unwrap();
106	unsafe { libc::dlopen(path.as_ptr(), libc::RTLD_NOW | libc::RTLD_GLOBAL) };
107	Ok(())
108}