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. (Not implemented #326)
146 pub extends: Option<RString>,
147 /// Names of the interfaces the exported class implements. (Not implemented
148 /// #326)
149 pub implements: Vec<RString>,
150 /// Properties of the class.
151 pub properties: Vec<Property>,
152 /// Methods of the class.
153 pub methods: Vec<Method>,
154 /// Constants of the class.
155 pub constants: Vec<Constant>,
156 /// Whether the export is an interface rather than a class.
157 pub is_interface: bool,
158}
159assert_ffi_safe!(Class, size = 128, align = 8);
160
161impl Class {
162 /// Creates the class representing a Rust closure, exported as
163 /// `RustClosure` when the `closure` feature of `ext-php-rs` is enabled.
164 #[must_use]
165 pub fn closure() -> Self {
166 Self {
167 name: "RustClosure".into(),
168 docs: DocBlock(StdVec::new().into()),
169 extends: Option::None,
170 implements: StdVec::new().into(),
171 properties: StdVec::new().into(),
172 methods: vec![Method {
173 name: "__invoke".into(),
174 docs: DocBlock(StdVec::new().into()),
175 ty: MethodType::Member,
176 params: vec![Parameter {
177 name: "args".into(),
178 ty: Option::Some(DataType::Mixed),
179 nullable: false,
180 variadic: true,
181 default: Option::None,
182 }]
183 .into(),
184 retval: Option::Some(Retval {
185 ty: DataType::Mixed,
186 nullable: false,
187 }),
188 r#static: false,
189 visibility: Visibility::Public,
190 r#abstract: false,
191 }]
192 .into(),
193 constants: StdVec::new().into(),
194 is_interface: false,
195 }
196 }
197}
198
199/// Represents an exported enum.
200#[repr(C)]
201#[derive(Debug, PartialEq)]
202pub struct Enum {
203 /// Name of the enum.
204 pub name: RString,
205 /// Documentation comments for the enum.
206 pub docs: DocBlock,
207 /// Cases of the enum.
208 pub cases: Vec<EnumCase>,
209 /// Backing type of the enum.
210 pub backing_type: Option<RString>,
211}
212assert_ffi_safe!(Enum, size = 72, align = 8);
213
214/// Represents a case in an exported enum.
215#[repr(C)]
216#[derive(Debug, PartialEq)]
217pub struct EnumCase {
218 /// Name of the enum case.
219 pub name: RString,
220 /// Documentation comments for the enum case.
221 pub docs: DocBlock,
222 /// Value of the enum case.
223 pub value: Option<RString>,
224}
225assert_ffi_safe!(EnumCase, size = 56, align = 8);
226
227/// Represents a property attached to an exported class.
228#[repr(C)]
229#[derive(Debug, PartialEq)]
230pub struct Property {
231 /// Name of the property.
232 pub name: RString,
233 /// Documentation comments for the property.
234 pub docs: DocBlock,
235 /// Type of the property.
236 pub ty: Option<DataType>,
237 /// Visibility of the property.
238 pub vis: Visibility,
239 /// Whether the property is static.
240 pub static_: bool,
241 /// Whether the property is nullable.
242 pub nullable: bool,
243 /// Whether the property is readonly.
244 pub readonly: bool,
245 /// Default value of the property as a PHP stub string.
246 pub default: Option<RString>,
247}
248assert_ffi_safe!(Property, size = 96, align = 8);
249
250/// Represents a method attached to an exported class.
251#[repr(C)]
252#[derive(Debug, PartialEq)]
253pub struct Method {
254 /// Name of the method.
255 pub name: RString,
256 /// Documentation comments for the method.
257 pub docs: DocBlock,
258 /// Type of the method.
259 pub ty: MethodType,
260 /// Parameters of the method.
261 pub params: Vec<Parameter>,
262 /// Return value of the method.
263 pub retval: Option<Retval>,
264 /// Whether the method is static.
265 pub r#static: bool,
266 /// Visibility of the method.
267 pub visibility: Visibility,
268 /// Not describe method body, if is abstract.
269 pub r#abstract: bool,
270}
271assert_ffi_safe!(Method, size = 112, align = 8);
272
273/// Represents a value returned from a function or method.
274#[repr(C)]
275#[derive(Debug, PartialEq)]
276pub struct Retval {
277 /// Type of the return value.
278 pub ty: DataType,
279 /// Whether the return value is nullable.
280 pub nullable: bool,
281}
282assert_ffi_safe!(Retval, size = 32, align = 8);
283
284/// Enumerator used to differentiate between methods.
285#[repr(C)]
286#[derive(Clone, Copy, Debug, PartialEq)]
287pub enum MethodType {
288 /// A member method.
289 Member,
290 /// A static method.
291 Static,
292 /// A constructor.
293 Constructor,
294}
295assert_ffi_safe!(MethodType, size = 4, align = 4);
296
297/// Enumerator used to differentiate between different method and property
298/// visibilties.
299#[repr(C)]
300#[derive(Clone, Copy, Debug, PartialEq)]
301pub enum Visibility {
302 /// Private visibility.
303 Private,
304 /// Protected visibility.
305 Protected,
306 /// Public visibility.
307 Public,
308}
309assert_ffi_safe!(Visibility, size = 4, align = 4);
310
311/// Represents an exported constant, stand alone or attached to a class.
312#[repr(C)]
313pub struct Constant {
314 /// Name of the constant.
315 pub name: RString,
316 /// Documentation comments for the constant.
317 pub docs: DocBlock,
318 /// Value of the constant.
319 pub value: Option<RString>,
320 /// Visibility of the constant. Always public for module constants.
321 pub visibility: Visibility,
322}
323assert_ffi_safe!(Constant, size = 64, align = 8);
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 assert_ffi_safe!(DocBlock, size = 16, align = 8);
329
330 #[test]
331 fn description_carries_the_introspection_version() {
332 let module = Module {
333 name: "test".into(),
334 functions: vec![].into(),
335 classes: vec![].into(),
336 enums: vec![].into(),
337 constants: vec![].into(),
338 };
339
340 let description = Description::new(module);
341 assert_eq!(description.version.str(), VERSION);
342 assert_eq!(description.module.name, "test".into());
343 }
344
345 #[test]
346 fn doc_block_from_slice() {
347 let docs: &'static [&'static str] = &["doc1", "doc2"];
348 let docs: DocBlock = docs.into();
349 assert_eq!(docs.0.len(), 2);
350 assert_eq!(docs.0[0], "doc1".into());
351 assert_eq!(docs.0[1], "doc2".into());
352 }
353
354 #[test]
355 fn closure_class_is_not_an_interface() {
356 let class = Class::closure();
357 assert!(!class.is_interface);
358 assert_eq!(class.methods.len(), 1);
359 }
360}