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. (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}