The blog

Writing PHP extensions in Go with FrankenPHP

Published on August 25, 2025

At PHPVerse 2025, the conference organized by JetBrains to mark the 30th anniversary of the PHP language, our cooperator Kévin Dunglas made a groundbreaking announcement: the possibility of creating PHP extensions with the Go language thanks to FrankenPHP. Although this is not really a new feature, as it has always been possible since the project's inception, today we're shedding more light on this little revolution and making it more accessible to everyone. Let's see how! A French version of this blog post is also available.

#

Early explorations

Extensions are bits of code that attach directly to the PHP interpreter and allow you to do just about anything you want. The main reason for this is that extensions are written in C, giving both high-level and low-level access to your machine. Among the best-known extensions are Parallel, which enables parallel code execution in PHP, GD for image management and PHP Redis for communication with Redis cache servers (and their alternatives, such as Snapchat KeyDB).

More recently, we've seen the emergence of extensions written in Rust and C++ (although this second choice has been around for a long time, it's not so popular). If you'd like to get an idea, Packagist lists the extensions that can be installed with PIE and the languages they are written in. However, all these languages are very low-level and not always easy to learn. This is even truer for the development of PHP extensions, which often require a fairly advanced knowledge of PHP's inner workings. As FrankenPHP is written in a higher-level language, Go, why not take the opportunity to try your hand at writing PHP extensions in this language? Indeed, FrankenPHP integrates the Go runtime, so it's perfectly legitimate to imagine using this language without any particular technical limitations other than the fact that it's never been done before.

With Kévin, we then started exploring the possibilities and working on a Proof Of Concept where a new native function would be added to PHP to execute Go code. After a few days of research, the results were in: a goroutine (a piece of code executed in parallel with the main code) was launched from PHP! The magic of all this comes from the existence of the CGO library, enabling C code to call Go code, but also to call C code from Go. From there, we're ready to develop any functionality.

#

FrankenPHP as a utility library

One of the things that stands out from these experiments is that, despite everything, we still spend a lot of time writing C code:

  • Registration of the extension within PHP is necessarily done in C, as this requires playing with some of Zend Engine's internal pointers. The good news is that this code is largely the same in all extensions;
  • Type juggling between C and Go. Many variable types are compatible between C and Go and can be used directly. This applies to integers, floating-point numbers and Booleans, for example. However, more complex structures such as strings, objects and arrays require conversion and cannot be used as they are. For example, in the PHP interpreter, strings are represented by a structure containing both the data and the size of the string. We therefore need to explore PHP's internal workings, which are sometimes poorly documented. LLMs are already excellent for explaining certain obscure mechanisms of the PHP engine, but this still requires low-level knowledge and the risk of corrupting memory remains high.
  • C code must be written to call Go code, no matter what.

FrankenPHP has a great card to play here: that of a utility library to help developers avoid these three problems. It is, of course, possible to do this by hand, and documentation exists to explain each step, providing a clear understanding of the stages involved in developing an extension from scratch. However, it is also possible to dispense with these steps.

As far as the first point is concerned, extensions written in Go will be Go modules, in this case Caddy modules. FrankenPHP uses Caddy as an integrated web server, which enables the integration of customized modules in a single line. In particular, custom modules can take the form of a Go file with an init() function that is executed when the module is started by Caddy. It's the perfect place to register an extension! Thanks to recent contributions to the project, FrankenPHP exposes a RegisterExtension() method that abstracts from all the C code required to register an extension. Without going into all the details (which are available in the documentation if you're curious!), registering an extension in Go looks like this:

package ext

/*
#include "ext.h"
*/
import "C"
import "github.com/dunglas/frankenphp"

func init() {
    frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}

Not a single line of C has been written here, and the extension is well registered!

As far as type juggling is concerned, FrankenPHP once again offers exposed functionality to abstract you from internal type mechanisms. As we saw earlier, some scalar types don't require conversion. But others do, like strings. That's why FrankenPHP offers methods such as frankenphp.GoString() (to retrieve a Go string from a C string) and frankenphp.PHPString() (to convert a Go string into a string usable by PHP) which take care of the conversions automatically. We hope to add more utility methods for other data types over time. Note that arrays are supported as well and callable are being worked on.

Once again, this article only scratches the surface of these new features, which are fully explained in the complete documentation already online.

As you can see, FrankenPHP plays a key role in facilitating the creation of PHP extensions, offering tools that greatly simplify the coding of your extensions. However, there's still one last problem to solve: writing the C code required to call the Go code. This leads us directly to the next section!

#

The extension generator

Aware of the dozens, if not hundreds, of lines of C code that would have to be written to make the “pass-through” between C and Go, we thought about the next step. Would it be possible to create a PHP extension generator, which would take a simple Go file as input, perhaps with some special syntax, and do the rest? The excellent news is that the answer is yes! After several weeks of development, a new tool has been integrated into FrankenPHP as a sub-command: the extension generator. The main objective is clear: the developer must be able to compile and integrate a PHP extension without ever writing a line of C code.

The generator defines specific Go directives. Think of Go directives as annotations or attributes in PHP. Here's an example of a Go file compatible with the extension generator:

package main

// export_php:function multiply(int $a, int $b): int
func multiply(a int64, b int64) int64 {
    return a * b
}

// export_php:function is_even(int $a): bool
func is_even(a int64) bool {
    return a%2 == 0
}

// export_php:function float_div(float $a, float $b): float
func float_div(a float64, b float64) float64 {
    return a / b
}

Thanks to the // export_php:function directives followed by the function signature, the generator will take care of defining all the intermediate C code. All that remains is to pass this file to the generator:

alex@alex-macos frankenphp % frankenphp extension-init ext-dir/ext.go
2025/06/20 09:49:09.273 INFO    PHP extension "ext" initialized successfully in "ext-dir/build"

alex@alex-macos frankenphp % ls -la ext-dir/build
total 48
drwxr-xr-x@ 8 alex  staff   256 Jun 20 11:49 .
drwxr-xr-x@ 8 alex  staff   256 Jun 20 11:49 ..
-rw-r--r--@ 1 alex  staff   418 Jun 20 11:49 README.md
-rw-r--r--@ 1 alex  staff  1673 Jun 20 11:49 ext.c
-rw-r--r--@ 1 alex  staff   396 Jun 20 11:49 ext.go
-rw-r--r--@ 1 alex  staff   226 Jun 20 11:49 ext.h
-rw-r--r--@ 1 alex  staff   168 Jun 20 11:49 ext.stub.php
-rw-r--r--@ 1 alex  staff   865 Jun 20 11:49 ext_arginfo.h

FrankenPHP has created all the files needed for the PHP extensions :

  • A README.md file containing the elements exported for the ;
  • A C file containing the intermediate code we wanted to avoid writing;
  • A Go file, very similar to the original but slightly modified to work correctly with the C code;
  • A header file containing the definition of certain functions enabling the C and Go code to work well together;
  • A stubs file with the definition of PHP function signatures;
  • An arginfo file containing all the directives needed to register the new functions in PHP's Zend Engine.

As we saw a moment ago, Caddy allows you to add additional custom modules to extend its functionality: just give Caddy the build directory and it will take care of the rest. The extension is fully functional, and not a single line of C has to be written by hand!

The strength of this generator lies in its support for other important functionalities. Here's a more complete example of a compatible file:

package main

// export_php:namespace Go\MyExtension

import (
	"C"
	"github.com/dunglas/frankenphp"
	"strings"
	"unsafe"
)

// export_php:const
const MY_GLOBAL_CONSTANT = "Hello, World!"

// export_php:classconst MySuperClass
const STR_REVERSE = iota

// export_php:classconst MySuperClass
const STR_NORMAL = iota

// export_php:class MySuperClass
type MyClass struct {
	Name     string
}

// export_php:method MySuperClass::setName(string $name): void
func (mc *MyClass) SetName(v *C.zend_string) {
	mc.Name = frankenphp.GoString(unsafe.Pointer(v))
}

// export_php:method MySuperClass::getName(): string
func (mc *MyClass) GetName() unsafe.Pointer {
	return frankenphp.PHPString(mc.Name, false)
}

// export_php:method MySuperClass::repeatName(int $count, ?int $mode): void
func (mc *MyClass) RepeatName(count int64, mode *int64) {
	str := mc.Name

	result := strings.Repeat(str, int(count))
	if mode != nil && *mode == STR_REVERSE {
		runes := []rune(result)
		for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
			runes[i], runes[j] = runes[j], runes[i]
		}
		result = string(runes)
	}

	if mode == nil || *mode == STR_NORMAL {
		// no-op, just to use the constant :)
	}

	mc.Name = result
}

Here we can see the declaration of global constants, classes, class constants and class methods. We also note the use of methods for juggling string types! It's important to note that the generator will hardly be able to cover all the possibilities offered by PHP extensions. It can, however, be a real help for an extension project without excessively advanced functionalities. Once again, we refer you to the documentation, which describes all the functions supported by the generator.

#

Why Go extensions?

It's fair to ask what's the point of extensions written in Go for PHP. After three decades of existence, it's great news to see that PHP continues to integrate so well with much newer technologies. Interfacing with Go requires FrankenPHP. Since the recent announcement by the PHP Foundation that it is officially taking FrankenPHP under its wing, its use promises to continue to grow and its long-term future is assured.

The idea behind Go extensions can be summed up in a few words: goroutines and wrappers. Firstly, goroutines are the high-performance concurrency model for which the language is famous. The ability to use goroutines within PHP code for potentially heavy and/or time-consuming operations opens up a wide range of possibilities. Secondly, wrappers for existing libraries open up just as many new possibilities. Many Go libraries are renowned for their quality, but are unfortunately not available in PHP. One example is the etcd caching system, for which our cooperator Kévin has created a complete Go extension for use with PHP. You can find this example in the extension repository. Offering an extension generator is a real step towards the democratization of PHP extension creation, and lowers the entry level required until now. Anyone can try their hand at it quickly, explore the generated code to understand how it works, and why not propose the next reference extension in the PHP ecosystem!

If you'd like to find out more about how to write your own extensions, the documentation will explain both how to use the generator, and how to write your own Go extensions without using the generator. We can't wait to see how this unique symbiosis between the two languages will be used!

The blog

Go further