davidcole1340 / ext-php-rs

Bindings for the Zend API to build PHP extensions natively in Rust.
Apache License 2.0
580 stars 62 forks source link

👋🏻 Not an issue; I just wrote an extension! #129

Open jphenow opened 2 years ago

jphenow commented 2 years ago

This is a great lib! Tripped through some details that was probably mostly related to my level of Rust ability honestly.

Anyways, I figured I'd share in case the example can be useful for folks: https://github.com/jphenow/tomlrs-php

It's incomplete because I didn't finish packaging it up for composer or anything but still might give folks some ideas as they get started.

Feel free to just close if this isn't useful at all - mostly wanted to say thanks for this library!

davidcole1340 commented 2 years ago

Cool! I'm glad it's faster than the PHP implementation (haha). I wonder if there's anything we could speed up (I assume a C version would be faster, albeit more of a PITA to write).

I will add it to the README examples.

vodik commented 2 years ago

Which reminds me I should document how I build and load PHP rust extensions inside Docker.Should have a look and see if there's any tooling we should build to improve the build process. Think something like Python's maturin.I think we should also be able to auto generated stub files for ide completion.

davidcole1340 commented 2 years ago

Which reminds me I should document how I build and load PHP rust extensions inside Docker.Should have a look and see if there's any tooling we should build to improve the build process. Think something like Pyle's maturin.

Yeah I agree. It would be pretty cool if we could run cargo php build --php 8.0,8.1 --os linux,macos and it would produce 4 dylibs. I'll try look into it if I've got time.

I think we should also be able to auto generated stub files for ide completion.

cargo php already has the ability to generate stubs, but it's pretty crappy: depends quite heavily on the ext-php-rs version (won't load an extension if a symbol is missing), can't expand parent class names, and I'm not convinced my ABI-stable work is completely correct. I think the better approach will be to embed something like php-extension-stub-generator into the binary and call it at runtime.

vodik commented 2 years ago

For the sake of recording it somewhere - here's the relevant parts of my Dockerfile:

# syntax=docker/dockerfile:1.2
ARG PHP_TAG=8.1-fpm-bullseye
ARG RUST_VERSION=1.56.1

FROM php:$PHP_TAG as php-base

# Prepare an builder base image.
# Use the php base image and adds rust/cargo-chef.
FROM php-base as builder
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  clang \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

# Adapted from official rust container
# https://github.com/rust-lang/docker-rust/blob/878a3bd2f3d92e51b9984dba8f8fd8881367a063/1.55.0/bullseye/slim/Dockerfile
ARG RUST_VERSION
ENV RUSTUP_HOME=/usr/local/rustup \
    CARGO_HOME=/usr/local/cargo \
    PATH=/usr/local/cargo/bin:$PATH \
    RUST_VERSION=$RUST_VERSION

RUN set -ex; \
  curl --proto '=https' --tlsv1.2 -fsSLO --compressed "https://static.rust-lang.org/rustup/archive/1.24.3/x86_64-unknown-linux-gnu/rustup-init"; \
  echo "3dc5ef50861ee18657f9db2eeb7392f9c2a6c95c90ab41e45ab4ca71476b4338 *rustup-init" | sha256sum -c -; \
  chmod +x rustup-init; \
  ./rustup-init -y --no-modify-path --profile minimal --default-toolchain $RUST_VERSION --default-host x86_64-unknown-linux-gnu \
  rm rustup-init; \
  chmod -R a+w $RUSTUP_HOME $CARGO_HOME; \
  rustup --version; \
  cargo --version; \
  rustc --version; \
  cargo install cargo-chef; \
  cargo-chef --version

WORKDIR /app

FROM builder as planner
COPY crate .
RUN cargo chef prepare --recipe-path recipe.json

FROM builder as ext-builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY crate .
RUN cargo build --release

FROM php-base as app
RUN echo extension=extension.so >/usr/local/etc/php/conf.d/ext-extension.ini
COPY --from=ext-builder /app/target/release/extension.so /usr/local/lib/php/extensions/no-debug-non-zts-20210902/extension.so

# rest of build goes here

I'll have a look at the stub builder when I have a chance.

I don't know how you did it, but I have an idea how to make it work. I'd try to store some extra metadata with #[php_class], etc to make the annotations explicit. Write them out to file.

davidcole1340 commented 2 years ago

@vodik

Just for context around the stub builder, the #[php_module] macro exports an additional C function from the extension, ext_php_rs_describe_module, which returns a struct containing information about the extension. The cargo-php executable then dynamically loads the extension library with dlopen to call this function.

As Rust is not ABI-stable, the ext_php_rs::describe::abi::* module was added with some ABI-stable types (Str, Vec and Option), as it's possible cargo-php and the extension are compiled with different versions of Rust.

At the moment it doesn't work too terribly, however if you extend a class it doesn't expand the class name in PHP (will give you something like class MyException extends zend::ce::exception() {}. Plus, there are some linkage issues I ran into primarily on macOS, as when the library is loaded it attempts to link with functions exported by PHP such as emalloc, which obviously don't exist in cargo-php. This also doesn't work on Windows now that it is supported by the crate.

For this reason I think it'd be best to do stub generation in PHP where all the parent classes (among other things) are resolved. Have tried to look into it however:

The exported describe function once expanded looks similar to this:

#[cfg(debug_assertions)]
#[no_mangle]
pub extern "C" fn ext_php_rs_describe_module() -> ::ext_php_rs::describe::Description {
    use ::ext_php_rs::describe::*;
    Description::new(Module {
        name: "ext-php-rs-test".into(),
        functions: <[_]>::into_vec(box [Function {
            name: "hello_world".into(),
            docs: DocBlock(::alloc::vec::Vec::new().into()),
            ret: abi::Option::Some(Retval {
                ty: <String as ::ext_php_rs::convert::IntoZval>::TYPE,
                nullable: false,
            }),
            params: <[_]>::into_vec(box [Parameter {
                name: "name".into(),
                ty: abi::Option::Some(<&str as ::ext_php_rs::convert::FromZvalMut>::TYPE),
                nullable: false,
                default: abi::Option::None,
            }])
            .into(),
        }])
        .into(),
        classes: ::alloc::vec::Vec::new().into(),
        constants: ::alloc::vec::Vec::new().into(),
    })
}
vodik commented 2 years ago

I didn't realize PHP extensions can describe themselves, though it makes sense. I'll look into that closer.

The thought process I had is that given we know all the functions, classes, etc. that make up the module, we could be in a position where we just write out a .php file directly with stubs as part of the build process, like how barryvdh/laravel-ide-helper works. Comments could be just copied over verbatim.

So for example the snippet in the README:

/// Gives you a nice greeting!
/// 
/// @param string $name Your name.
/// 
/// @return string Nice greeting!
#[php_function]
pub fn hello_world(name: String) -> String {
    format!("Hello, {}!", name)
}

could compile an companion _example.php (or whatever convention) file that looks like:

/**
  * Gives you a nice greeting!
  *
  * @param string $name Your name.
  * 
  * @return string Nice greeting!
  */
public function helloWorld(string $name): string {}
vodik commented 2 years ago

Maybe it wouldn't hurt to move this conversation off this issue :grin:

We might be able to convert rustdoc conventions into PHP ones with this technique. No reason the rust version couldn't do the standard:

/// Gives you a nice greeting!
/// 
/// # Arguments
///
/// * `name` - Your name
/// 
/// # Returns
///
/// Nice greeting! 
#[php_function]
pub fn hello_world(name: String) -> String {
    format!("Hello, {}!", name)
}