Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ jobs:
run: |
docker run --platform=${{ matrix.platform }} --rm \
"$(jq -r '."builder-${{ matrix.variant }}"."containerimage.config.digest"' <<< "${METADATA}")" \
sh -c 'go test -tags ${{ matrix.race }} -v ./... && cd caddy && go test -tags nobadger,nomysql,nopgx ${{ matrix.race }} -v ./...'
sh -c 'go test -tags ${{ matrix.race }} -v $(go list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen) && cd caddy && go test -tags nobadger,nomysql,nopgx ${{ matrix.race }} -v ./...'
env:
METADATA: ${{ steps.build.outputs.metadata }}
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ jobs:
- name: Build testcli binary
working-directory: internal/testcli/
run: go build
- name: Compile library tests
run: go test -race -v -x -c
- name: Run library tests
run: go test -race -v ./...
run: ./frankenphp.test -test.v
- name: Run Caddy module tests
working-directory: caddy/
run: go test -tags nobadger,nomysql,nopgx -race -v ./...
Expand Down
53 changes: 53 additions & 0 deletions caddy/extinit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package caddy

import (
"errors"
"github.com/dunglas/frankenphp/internal/extgen"
"log"
"os"
"path/filepath"
"strings"

caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/spf13/cobra"
)

func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "extension-init",
Usage: "go_extension.go [--verbose]",
Short: "(Experimental) Initializes a PHP extension from a Go file",
Long: `
Initializes a PHP extension from a Go file. This command generates the necessary C files for the extension, including the header and source files, as well as the arginfo file.`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")

cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
},
})
}

func cmdInitExtension(fs caddycmd.Flags) (int, error) {
if len(os.Args) < 3 {
return 1, errors.New("the path to the Go source is required")
}

sourceFile := os.Args[2]

baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")

baseName = extgen.SanitizePackageName(baseName)

sourceDir := filepath.Dir(sourceFile)
buildDir := filepath.Join(sourceDir, "build")

generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}

if err := generator.Generate(); err != nil {
return 1, err
}

log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)

return 0, nil
}
29 changes: 29 additions & 0 deletions ext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package frankenphp

//#include "frankenphp.h"
import "C"
import (
"sync"
"unsafe"
)

var (
extensions []*C.zend_module_entry
registerOnce sync.Once
)

// RegisterExtension registers a new PHP extension.
func RegisterExtension(me unsafe.Pointer) {
extensions = append(extensions, (*C.zend_module_entry)(me))
}

func registerExtensions() {
if len(extensions) == 0 {
return
}

registerOnce.Do(func() {
C.register_extensions(extensions[0], C.int(len(extensions)))
extensions = nil
})
}
31 changes: 31 additions & 0 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1182,3 +1182,34 @@ int frankenphp_reset_opcache(void) {
}

int frankenphp_get_current_memory_limit() { return PG(memory_limit); }

static zend_module_entry *modules = NULL;
static int modules_len = 0;
static int (*original_php_register_internal_extensions_func)(void) = NULL;

PHPAPI int register_internal_extensions(void) {
if (original_php_register_internal_extensions_func != NULL &&
original_php_register_internal_extensions_func() != SUCCESS) {
return FAILURE;
}

for (int i = 0; i < modules_len; i++) {
if (zend_register_internal_module(&modules[i]) == NULL) {
return FAILURE;
}
}

modules = NULL;
modules_len = 0;

return SUCCESS;
}

void register_extensions(zend_module_entry *m, int len) {
modules = m;
modules_len = len;

original_php_register_internal_extensions_func =
php_register_internal_extensions_func;
php_register_internal_extensions_func = register_internal_extensions;
}
2 changes: 2 additions & 0 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ func Init(options ...Option) error {
// Docker/Moby has a similar hack: https://github.com/moby/moby/blob/d828b032a87606ae34267e349bf7f7ccb1f6495a/cmd/dockerd/docker.go#L87-L90
signal.Ignore(syscall.SIGPIPE)

registerExtensions()

opt := &opt{}
for _, o := range options {
if err := o(opt); err != nil {
Expand Down
3 changes: 3 additions & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef _FRANKENPPHP_H
#define _FRANKENPPHP_H

#include <Zend/zend_modules.h>
#include <Zend/zend_types.h>
#include <stdbool.h>
#include <stdint.h>
Expand Down Expand Up @@ -92,4 +93,6 @@ void frankenphp_register_bulk(
ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
ht_key_value_pair request_uri);

void register_extensions(zend_module_entry *m, int len);

#endif
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.24.0
retract v1.0.0-rc.1 // Human error

require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/maypok86/otter v1.2.4
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
Expand All @@ -14,19 +15,29 @@ require (
)

require (
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/gammazero/deque v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
Expand Down
24 changes: 24 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand All @@ -6,10 +14,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
Expand All @@ -18,6 +32,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -32,6 +50,10 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
Expand All @@ -42,6 +64,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
Expand Down
50 changes: 50 additions & 0 deletions internal/extgen/arginfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package extgen

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)

type arginfoGenerator struct {
generator *Generator
}

func (ag *arginfoGenerator) generate() error {
genStubPath := os.Getenv("GEN_STUB_SCRIPT")
if genStubPath == "" {
genStubPath = "/usr/local/src/php/build/gen_stub.php"
}

if _, err := os.Stat(genStubPath); err != nil {
return fmt.Errorf(`the PHP "gen_stub.php" file couldn't be found under %q, you can set the "GEN_STUB_SCRIPT" environement variable to set a custom location`, genStubPath)
}

stubFile := ag.generator.BaseName + ".stub.php"
cmd := exec.Command("php", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))

if err := cmd.Run(); err != nil {
return fmt.Errorf("running gen_stub script: %w", err)
}

return ag.fixArginfoFile(stubFile)
}

func (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {
arginfoFile := strings.TrimSuffix(stubFile, ".stub.php") + "_arginfo.h"
arginfoPath := filepath.Join(ag.generator.BuildDir, arginfoFile)

content, err := ReadFile(arginfoPath)
if err != nil {
return fmt.Errorf("reading arginfo file: %w", err)
}

// FIXME: the script generate "zend_register_internal_class_with_flags" but it is not recognized by the compiler
fixedContent := strings.ReplaceAll(content,
"zend_register_internal_class_with_flags(&ce, NULL, 0)",
"zend_register_internal_class(&ce)")

return WriteFile(arginfoPath, fixedContent)
}
68 changes: 68 additions & 0 deletions internal/extgen/cfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package extgen

import (
"github.com/Masterminds/sprig/v3"

"bytes"
_ "embed"
"path/filepath"
"strings"
"text/template"
)

//go:embed templates/extension.c.tpl
var cFileContent string

type cFileGenerator struct {
generator *Generator
}

type cTemplateData struct {
BaseName string
Functions []phpFunction
Classes []phpClass
Constants []phpConstant
}

func (cg *cFileGenerator) generate() error {
filename := filepath.Join(cg.generator.BuildDir, cg.generator.BaseName+".c")
content, err := cg.buildContent()
if err != nil {
return err
}

return WriteFile(filename, content)
}

func (cg *cFileGenerator) buildContent() (string, error) {
var builder strings.Builder

templateContent, err := cg.getTemplateContent()
if err != nil {
return "", err
}
builder.WriteString(templateContent)

for _, fn := range cg.generator.Functions {
fnGen := PHPFuncGenerator{paramParser: &ParameterParser{}}
builder.WriteString(fnGen.generate(fn))
}

return builder.String(), nil
}

func (cg *cFileGenerator) getTemplateContent() (string, error) {
tmpl := template.Must(template.New("cfile").Funcs(sprig.FuncMap()).Parse(cFileContent))

var buf bytes.Buffer
if err := tmpl.Execute(&buf, cTemplateData{
BaseName: cg.generator.BaseName,
Functions: cg.generator.Functions,
Classes: cg.generator.Classes,
Constants: cg.generator.Constants,
}); err != nil {
return "", err
}

return buf.String(), nil
}
Loading