ext_php_rs_introspection/lib.rs
1//! ABI-stable description of an `ext-php-rs` extension.
2//!
3//! An extension built with `ext-php-rs` exports `ext_php_rs_describe_module_v2`,
4//! returning a heap-allocated [`Description`] as `*mut Description`. The
5//! `cargo-php` CLI loads the extension with `dlopen`, calls that function and
6//! renders PHP stubs through [`ToStub`].
7//! Both sides depend on this crate and nothing else from the Zend engine, so
8//! the CLI builds without PHP and the layout of every `#[repr(C)]` type below
9//! is the whole contract between them.
10//!
11//! [`VERSION`] travels inside [`Description`]. `cargo-php` reads it first and
12//! refuses an extension that was built with a different minor of this crate.
13//! A layout change therefore causes a clear error, not a misread.
14//!
15//! # Changing a type here
16//!
17//! An `assert_ffi_safe!` invocation follows every `#[repr(C)]` type. It makes
18//! sure that rustc accepts the type by value in an `extern "C"` signature, so
19//! a payload with a Rust-only layout such as `&str` is rejected. It also makes
20//! sure that the size and the alignment of the type on 64-bit targets match
21//! the recorded numbers.
22//!
23//! If you add, remove or reorder a field or a variant, or if a recorded number
24//! changes, the change is a breaking change of this crate. Mark the commit
25//! with `!`, so the release bumps the version that `cargo-php` compares, and
26//! update the numbers next to the type.
27use std::vec::Vec as StdVec;
28
29use abi::{Option, RString, Str, Vec, assert_ffi_safe};
30
31pub mod abi;
32mod data_type;
33mod stub;
34
35pub use data_type::DataType;
36pub use stub::ToStub;
37
38/// Version of the description ABI, the version of this crate.
39pub const VERSION: &str = env!("CARGO_PKG_VERSION");
40
41/// A slice of strings containing documentation comments.
42pub type DocComments = &'static [&'static str];
43
44/// Representation of the extension used to generate PHP stubs.
45///
46/// `version` stays the first field forever: `cargo-php` reads it through the
47/// pointer before trusting the rest of the layout, so it has to sit at an
48/// offset that never moves. The value is returned by pointer rather than by
49/// value so a larger layout on the extension side cannot overrun a buffer
50/// sized by the CLI.
51#[repr(C)]
52pub struct Description {
53 /// Version of `ext-php-rs-introspection` the extension was built with.
54 pub version: Str,
55 /// Extension description.
56 pub module: Module,
57}
58assert_ffi_safe!(Description, size = 96, align = 8);
59
60impl Description {
61 /// Creates a new description.
62 ///
63 /// # Parameters
64 ///
65 /// * `module` - The extension module representation.
66 #[must_use]
67 pub fn new(module: Module) -> Self {
68 Self {
69 version: VERSION.into(),
70 module,
71 }
72 }
73}
74
75/// Represents a set of comments on an export.
76#[repr(C)]
77#[derive(Debug, PartialEq)]
78pub struct DocBlock(pub Vec<Str>);
79
80impl From<&'static [&'static str]> for DocBlock {
81 fn from(val: &'static [&'static str]) -> Self {
82 Self(
83 val.iter()
84 .map(|s| (*s).into())
85 .collect::<StdVec<_>>()
86 .into(),
87 )
88 }
89}
90
91/// Represents an extension containing a set of exports.
92#[repr(C)]
93pub struct Module {
94 /// Name of the extension.
95 pub name: RString,
96 /// Functions exported by the extension.
97 pub functions: Vec<Function>,
98 /// Classes exported by the extension.
99 pub classes: Vec<Class>,
100 /// Enums exported by the extension.
101 pub enums: Vec<Enum>,
102 /// Constants exported by the extension.
103 pub constants: Vec<Constant>,
104}
105assert_ffi_safe!(Module, size = 80, align = 8);
106
107/// Represents an exported function.
108#[repr(C)]
109pub struct Function {
110 /// Name of the function.
111 pub name: RString,
112 /// Documentation comments for the function.
113 pub docs: DocBlock,
114 /// Return value of the function.
115 pub ret: Option<Retval>,
116 /// Parameters of the function.
117 pub params: Vec<Parameter>,
118}
119assert_ffi_safe!(Function, size = 88, align = 8);
120
121/// Represents a parameter attached to an exported function or method.
122#[repr(C)]
123#[derive(Debug, PartialEq)]
124pub struct Parameter {
125 /// Name of the parameter.
126 pub name: RString,
127 /// Type of the parameter.
128 pub ty: Option<DataType>,
129 /// Whether the parameter is nullable.
130 pub nullable: bool,
131 /// Whether the parameter is variadic.
132 pub variadic: bool,
133 /// Default value of the parameter.
134 pub default: Option<RString>,
135}
136assert_ffi_safe!(Parameter, size = 80, align = 8);
137
138/// Represents an exported class or interface.
139#[repr(C)]
140pub struct Class {
141 /// Name of the class.
142 pub name: RString,
143 /// Documentation comments for the class.
144 pub docs: DocBlock,
145 /// Name of the class the exported class extends. Stubs render it fully
146 /// qualified.
147 pub extends: Option<RString>,
148 /// Names of the interfaces the exported class implements, or the parent
149 /// interfaces of an exported interface. Stubs render them fully qualified.
150 pub implements: Vec<RString>,
151 /// Properties of the class.
152 pub properties: Vec<Property>,
153 /// Methods of the class.
154 pub methods: Vec<Method>,
155 /// Constants of the class.
156 pub constants: Vec<Constant>,
157 /// Whether the export is an interface rather than a class.
158 pub is_interface: bool,
159}
160assert_ffi_safe!(Class, size = 128, align = 8);
161
162impl Class {
163 /// Creates the class representing a Rust closure, exported as
164 /// `RustClosure` when the `closure` feature of `ext-php-rs` is enabled.
165 #[must_use]
166 pub fn closure() -> Self {
167 Self {
168 name: "RustClosure".into(),
169 docs: DocBlock(StdVec::new().into()),
170 extends: Option::None,
171 implements: StdVec::new().into(),
172 properties: StdVec::new().into(),
173 methods: vec![Method {
174 name: "__invoke".into(),
175 docs: DocBlock(StdVec::new().into()),
176 ty: MethodType::Member,
177 params: vec![Parameter {
178 name: "args".into(),
179 ty: Option::Some(DataType::Mixed),
180 nullable: false,
181 variadic: true,
182 default: Option::None,
183 }]
184 .into(),
185 retval: Option::Some(Retval {
186 ty: DataType::Mixed,
187 nullable: false,
188 }),
189 r#static: false,
190 visibility: Visibility::Public,
191 r#abstract: false,
192 }]
193 .into(),
194 constants: StdVec::new().into(),
195 is_interface: false,
196 }
197 }
198}
199
200/// Represents an exported enum.
201#[repr(C)]
202#[derive(Debug, PartialEq)]
203pub struct Enum {
204 /// Name of the enum.
205 pub name: RString,
206 /// Documentation comments for the enum.
207 pub docs: DocBlock,
208 /// Cases of the enum.
209 pub cases: Vec<EnumCase>,
210 /// Backing type of the enum.
211 pub backing_type: Option<RString>,
212}
213assert_ffi_safe!(Enum, size = 72, align = 8);
214
215/// Represents a case in an exported enum.
216#[repr(C)]
217#[derive(Debug, PartialEq)]
218pub struct EnumCase {
219 /// Name of the enum case.
220 pub name: RString,
221 /// Documentation comments for the enum case.
222 pub docs: DocBlock,
223 /// Value of the enum case.
224 pub value: Option<RString>,
225}
226assert_ffi_safe!(EnumCase, size = 56, align = 8);
227
228/// Represents a property attached to an exported class.
229#[repr(C)]
230#[derive(Debug, PartialEq)]
231pub struct Property {
232 /// Name of the property.
233 pub name: RString,
234 /// Documentation comments for the property.
235 pub docs: DocBlock,
236 /// Type of the property.
237 pub ty: Option<DataType>,
238 /// Visibility of the property.
239 pub vis: Visibility,
240 /// Whether the property is static.
241 pub static_: bool,
242 /// Whether the property is nullable.
243 pub nullable: bool,
244 /// Whether the property is readonly.
245 pub readonly: bool,
246 /// Default value of the property as a PHP stub string.
247 pub default: Option<RString>,
248}
249assert_ffi_safe!(Property, size = 96, align = 8);
250
251/// Represents a method attached to an exported class.
252#[repr(C)]
253#[derive(Debug, PartialEq)]
254pub struct Method {
255 /// Name of the method.
256 pub name: RString,
257 /// Documentation comments for the method.
258 pub docs: DocBlock,
259 /// Type of the method.
260 pub ty: MethodType,
261 /// Parameters of the method.
262 pub params: Vec<Parameter>,
263 /// Return value of the method.
264 pub retval: Option<Retval>,
265 /// Whether the method is static.
266 pub r#static: bool,
267 /// Visibility of the method.
268 pub visibility: Visibility,
269 /// Not describe method body, if is abstract.
270 pub r#abstract: bool,
271}
272assert_ffi_safe!(Method, size = 112, align = 8);
273
274/// Represents a value returned from a function or method.
275#[repr(C)]
276#[derive(Debug, PartialEq)]
277pub struct Retval {
278 /// Type of the return value.
279 pub ty: DataType,
280 /// Whether the return value is nullable.
281 pub nullable: bool,
282}
283assert_ffi_safe!(Retval, size = 32, align = 8);
284
285/// Enumerator used to differentiate between methods.
286#[repr(C)]
287#[derive(Clone, Copy, Debug, PartialEq)]
288pub enum MethodType {
289 /// A member method.
290 Member,
291 /// A static method.
292 Static,
293 /// A constructor.
294 Constructor,
295}
296assert_ffi_safe!(MethodType, size = 4, align = 4);
297
298/// Enumerator used to differentiate between different method and property
299/// visibilties.
300#[repr(C)]
301#[derive(Clone, Copy, Debug, PartialEq)]
302pub enum Visibility {
303 /// Private visibility.
304 Private,
305 /// Protected visibility.
306 Protected,
307 /// Public visibility.
308 Public,
309}
310assert_ffi_safe!(Visibility, size = 4, align = 4);
311
312/// Represents an exported constant, stand alone or attached to a class.
313#[repr(C)]
314pub struct Constant {
315 /// Name of the constant.
316 pub name: RString,
317 /// Documentation comments for the constant.
318 pub docs: DocBlock,
319 /// Value of the constant.
320 pub value: Option<RString>,
321 /// Visibility of the constant. Always public for module constants.
322 pub visibility: Visibility,
323}
324assert_ffi_safe!(Constant, size = 64, align = 8);
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 assert_ffi_safe!(DocBlock, size = 16, align = 8);
330
331 #[test]
332 fn description_carries_the_introspection_version() {
333 let module = Module {
334 name: "test".into(),
335 functions: vec![].into(),
336 classes: vec![].into(),
337 enums: vec![].into(),
338 constants: vec![].into(),
339 };
340
341 let description = Description::new(module);
342 assert_eq!(description.version.str(), VERSION);
343 assert_eq!(description.module.name, "test".into());
344 }
345
346 #[test]
347 fn doc_block_from_slice() {
348 let docs: &'static [&'static str] = &["doc1", "doc2"];
349 let docs: DocBlock = docs.into();
350 assert_eq!(docs.0.len(), 2);
351 assert_eq!(docs.0[0], "doc1".into());
352 assert_eq!(docs.0[1], "doc2".into());
353 }
354
355 #[test]
356 fn closure_class_is_not_an_interface() {
357 let class = Class::closure();
358 assert!(!class.is_interface);
359 assert_eq!(class.methods.len(), 1);
360 }
361}