Skip to main content

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}