-
Notifications
You must be signed in to change notification settings - Fork 425
feat(extensions): add the PHP extension generator #1649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
f6cf817 to
f99078a
Compare
f99078a to
3e5d07c
Compare
3e5d07c to
516453f
Compare
516453f to
f089854
Compare
|
Just added more validation on parameters and return types, see the Validation section in the PR description. |
withinboredom
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking forward to taking this for a test drive!
| typeMap := map[string]string{ | ||
| "string": "string", | ||
| "int": "int", "int64": "int", "int32": "int", "int16": "int", "int8": "int", | ||
| "uint": "int", "uint64": "int", "uint32": "int", "uint16": "int", "uint8": "int", | ||
| "float64": "float", "float32": "float", | ||
| "bool": "bool", | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to handle other "weird" php types, like callable? For example, we had an onRequestFailure($closure) extension to handle some logging on failed requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ultimately it would be nice to support all types yes! I need to do more investigation on how this could work with Go, thus the restricted types for now. But definitely, if we can support all types, that would be super nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC (I no longer work there), it was just stored as a pointer to the function info + the other function thing (I can't remember the name of it) that needs to be kept. On the go side, we just called a helper passing those (think something like ExecutePHPCallback(funcStruct)) that would then do a bunch of monkeying around in C to ensure it got dispatched on the correct thread. In this case, you could probably just pass the pointers via a channel waiting on the appropriate thread. Our case violated the C-Go-C rules (same as the old fibers issue), so -- figuring out how to get around that will be interesting, I'm sure.
|
This is a really cool idea, but I'm wondering how suitable Go really is to develop php extensions. The runtime will limit this to static compilation (unless doing some voodoo stuff with -buildmode=c-shared, linking against a shared Go runtime, loading the library in via php's extension loading mechanism again), which means users will have to be actively involved in how to compile frankenphp & extensions. And C++'s memory model and C interoperability are much better than Go's, as is performance for crucial tasks. This will essentially make for a rather narrow range of application: developers with experience in Go, that wish to tightly couple their application(s) to FrankenPHP, who have no experience with zend and C/C++. Or you really need the functionality of an existing Go library. That being said, I'm excited to see this being developed and will be happy if it eventually takes off. Are you going for a small demonstration on Tuesday? |
Quite suitable actually! Many PHP devs know Go and even use Go. Go and PHP go quite well together; IMHO. In a lot of ways, Go reminds me of PHP in the early days -- back when people knew PHP, not frameworks. That being said,
There are Go plugin libraries. Caddy even looked into using them at one point in the early days (I remember having a discussion on Twitter with @mholt way back then). They're much more mature today, so it wouldn't be all that complicated to add a layer onto this that allows you to use PHP extensions as a plugin via a frankenphp extension. I have no idea what the performance impact would be, but I doubt it would be too insane. |
I didn't really mean from a programming language point of view, but runtime and memory model complexities. It's typically much easier to extend systems in a language that closely resembles the language used to create the system. That's why we use C++ to interface with games written in C++ (using the same compiler toolchain), and C# to interface with games written in C#. There are already some troubles with type conversions now, I'm not sure how well it'll go with callables, array types (how much overhead will the mapping introduce?) and structs.
At the very least it adds two layers of indirection with every function call. On top of that, from what I understand, longer function calls may also cause a context switch. And last but not least, while Go is fast, it's not quite as fast as using C/C++ directly. It has great benefits too, especially in cases involving concurrency and the ease of using libraries, so there will definitely be a field where it makes sense, but I have the feeling that native C extensions will likely be the better choice in a lot of scenarios. |
|
Indeed, you've just pointed out something important! We're well aware that the development of highly optimized extensions with particular performance needs will always require the use of C, at one time or another. So yes, when you mention concurrency and Go libraries, your right on target! |
|
@alexandre-daubois is there a draft of the documentation? I'd love to write a test extension, but I'm not sure how to get started. |
|
I created one. It will be published tomorrow during JetBrains PHPVerse! |
|
Looks like a ton of effort went into this extension generator, also excited to try it 👍 |
|
@withinboredom yes! As Kevin said, he created an extension that will be revealed tomorrow and that will definitely help you getting started. In the meantime, you can find a some docs here: https://github.com/alexandre-daubois/frankenphp/blob/ext-generator/docs/extensions-generator.md. I need to get back to it to ensure it's up to date with the latest changes, but it covers all supported features if you want to have a look while waiting for tomorrow 🙂 |
|
My biggest feedback: reads that it is going to create a static method, not an instanced one. edit: I guess there could be a |
|
ok, this is pretty neat. In just an hour or two, after reading the docs and fiddling with it, I was able to create an extension from this PR. I feel like this feature still has a looong way to go; but it is already quite powerful. My silly extension just takes a URL and spams it with 50 threads, in parallel, and returns the body by simply calling Go really shines with parallel tasks, which is a true pain with PHP (and C, for that matter). I highly suspect that it is only a matter of time before someone writes a Revolt compatible event loop, in Go. |
|
DDOSing in PHP just became so much easier! 💯 Haha, I'm jesting. Another great idea would be to integrate Ristretto because a lot of applications will only need an in-memory cache rather than a shared one. It's on my to-do list after the packaging (unless someone else picks it up first, I see this as revolutionary compared to using APCu with its very slow serialisation and fragmentation issues and... well, necessary system calls). Although it would be very important to benchmark given the very many interop calls and potentially fall back to CacheLib.
Boost cobalt + coroutines come to the rescue. The dependency tree isn't too crazy, it doesn't require the entirety of boost, but of course it comes with the usual C++ style of things. I'd only write the very simplest of extensions in C when using C++ is essentially just as easy on the C API side but gives you all the required tools to write modern, safe code. |
|
A cache also makes a lot of sense for this.
cgo overhead isn’t too bad, it now only even shows up on flame graphs because we’ve trimmed the fat off of frankenphp into a lean and mean request-processing machine. When frankenphp was slower (i.e. 10–20k rps), you couldn’t even see it on the graph. Most likely, since this can only handle basic value types, you will spend most of the time serializing to string. (though you could also just store an index in the cache that you then retrieve from an array on the PHP side). |
|
After this PR is merged, here are some things I would like to do to enhance this feature:
|
* feat(extensions): add the PHP extension generator * unexport many types * unexport more symbols * cleanup some tests * unexport more symbols * fix * revert types files * revert * add better validation and fix templates * remove GoStringCopy * small fixes --------- Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
* feat(extensions): add the PHP extension generator * unexport many types * unexport more symbols * cleanup some tests * unexport more symbols * fix * revert types files * revert * add better validation and fix templates * remove GoStringCopy * small fixes --------- Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
* feat: add helpers to create PHP extensions (#1644) * feat: add helpers to create PHP extensions * cs * feat: GoString * test * add test for RegisterExtension * cs * optimize includes * fix * feat(extensions): add the PHP extension generator (#1649) * feat(extensions): add the PHP extension generator * unexport many types * unexport more symbols * cleanup some tests * unexport more symbols * fix * revert types files * revert * add better validation and fix templates * remove GoStringCopy * small fixes --------- Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> * try to fix tests * fix CS * try some workarounds * try some workarounds * ingore TestRegisterExtension * exclude cgo tests in Docker images * fix * workaround... * race detector * simplify tests and code * make linter happy * feat(gofile): use templates to generate the Go file (#1666) --------- Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
This is something that I was also thinking about implementing, as APCu is kind of junk. I just came across this excellent article that makes a good case for using Otter rather than Ristretto. I'd also like to try NATS KV, which would be similar to the etcd example that kevin made |
|
I gave it a little bit more thought (though I haven't started it yet) and came to the conclusion that writing it in C++ using CacheLib and the existing igbinary extension would likely be the more performant choice. It offers several advantages, such as saving/reloading of cache, no runtime call cost to C, better unwrapping of php types and interop with igbinary, because I wasn't really looking to implement a high performance serialiser on top. |
After a few weeks working on this with Kevin, I'm happy to propose the extension generator to the review. This PR targets the
extbranch and notmain, so we can iterate on it before actually shipping the generator inmain.PHP Extension Generator
It's been hinted here and there that FrankenPHP allows to "easily" create PHP extensions written in Go. However, having to understand PHP internal mechanisms can be quite hard and discouraging when it comes to creating an extension. This tool was designed to remove this barrier and make it easier to create an extension in Go and abstract C code and type juggling for most basic use cases.
It looks like Symfony's maker bundle: great to kickstart things and the majority of use cases, help you to greatly reduce the boilerplate, but it cannot cover everything if you need extra optimization and super advanced use cases.
The generator cannot cover all PHP features, but we're doing our best to propose the most used ones.
Note
The documentation is being written and will be proposed in a separate PR. This one's already big enough 🙂
Writing the extension in Go
Thanks to Go directive, we can declare functions, constants, classes and methods right from the Go code. Let's say you have the following code:
Many things to note here:
//export_php:function,//export_php:const,//export_php:classconst,//export_php:classand//export_php:methodto export symbols to PHP in our extension//export_php:functionand//export_php:methodtakes an "argument": the function prototype, which will be directly used by PHP stubs to generate the arginfo fileRepeatName)getName()andsetName())iotaas their value: their value will be automatically generated when running the generator. You can of course provide a value yourself and it supports floats, bools, strings and integers using binary, octal, decimal and hexadecimal notation.Type Juggling
Type juggling is a big subject, maybe the hardest part of writing extensions in Go. There's nothing to do with types like int, float and bool. But for example with strings, this is not the same thing. Strings have a special memory representation in the Zend Engine and need conversion before being used in your extension. FrankenPHP provides multiple functions to deal with this, see
types.go.For this particular reason, arrays and objects are not yet supported by the extension generator. It will eventually come but in a second time. Again, the PR is already pretty big. 🙂
What does this generate?
The generator creates 6 files for a given Go file:
ext.c, containing the actual C code of the extension that will call Go for youext.go, your actual Go code, with some tweaks in order for everything to work smoothly together with the C codeext.h, with your module prototype and your constant values if you defined someext.stub.php, holding all classes and functions prototypes and definitions. This is used by a PHP tool (provided inphp-src) to generate the arginfo fileext_arginfo.h, generated by the PHP tool that has the C code to declare everything in the Zend Engine (class entries, function entries, etc.)README.md, a simple markdown file containing all exported symbols of your extensionOpaque Classes
Exported classes are opaque, which means you cannot access properties from your PHP code and you must use exported methods to interact with them. This is done because of technical limitations. Instead of rewriting all PHP rules (type checks and so on) in class hooks, let's make classes opaque. This also ensure that userland cannot corrupt internal state of our object. Note that this is something often done in extensions and a lot of internal PHP classes are opaque:
\Directory,\CurlHandle, etc.Validation
The generator does it's best to give insightful warnings when something is potentially wrong in the Go file containing the extension. For example, it validates that parameter and return types are somehow compatible between the PHP signature and the Go signature and can display messages likes this:
Compiling The Extension
After running the generator on a Go file, a
builddirectory is created next to the file. You just need to include this directory in the FrankenPHP compilation step usingxcaddy --with-module(or equivalent), and you're done! The above example would allow to write such:That's it: you created an extension only writing Go and not a line of C!