Skip to content

Commit fcb34c3

Browse files
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>
1 parent e4feeaa commit fcb34c3

38 files changed

Lines changed: 9244 additions & 1 deletion

caddy/extinit.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package caddy
2+
3+
import (
4+
"errors"
5+
"github.com/dunglas/frankenphp/internal/extgen"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
caddycmd "github.com/caddyserver/caddy/v2/cmd"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func init() {
16+
caddycmd.RegisterCommand(caddycmd.Command{
17+
Name: "extension-init",
18+
Usage: "go_extension.go [--verbose]",
19+
Short: "(Experimental) Initializes a PHP extension from a Go file",
20+
Long: `
21+
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.`,
22+
CobraFunc: func(cmd *cobra.Command) {
23+
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
24+
25+
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
26+
},
27+
})
28+
}
29+
30+
func cmdInitExtension(fs caddycmd.Flags) (int, error) {
31+
if len(os.Args) < 3 {
32+
return 1, errors.New("the path to the Go source is required")
33+
}
34+
35+
sourceFile := os.Args[2]
36+
37+
baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
38+
39+
baseName = extgen.SanitizePackageName(baseName)
40+
41+
sourceDir := filepath.Dir(sourceFile)
42+
buildDir := filepath.Join(sourceDir, "build")
43+
44+
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
45+
46+
if err := generator.Generate(); err != nil {
47+
return 1, err
48+
}
49+
50+
log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
51+
52+
return 0, nil
53+
}

internal/extgen/arginfo.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package extgen
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strings"
9+
)
10+
11+
type arginfoGenerator struct {
12+
generator *Generator
13+
}
14+
15+
func (ag *arginfoGenerator) generate() error {
16+
genStubPath := os.Getenv("GEN_STUB_SCRIPT")
17+
if genStubPath == "" {
18+
genStubPath = "/usr/local/src/php/build/gen_stub.php"
19+
}
20+
21+
if _, err := os.Stat(genStubPath); err != nil {
22+
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)
23+
}
24+
25+
stubFile := ag.generator.BaseName + ".stub.php"
26+
cmd := exec.Command("php", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))
27+
28+
if err := cmd.Run(); err != nil {
29+
return fmt.Errorf("running gen_stub script: %w", err)
30+
}
31+
32+
return ag.fixArginfoFile(stubFile)
33+
}
34+
35+
func (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {
36+
arginfoFile := strings.TrimSuffix(stubFile, ".stub.php") + "_arginfo.h"
37+
arginfoPath := filepath.Join(ag.generator.BuildDir, arginfoFile)
38+
39+
content, err := ReadFile(arginfoPath)
40+
if err != nil {
41+
return fmt.Errorf("reading arginfo file: %w", err)
42+
}
43+
44+
// TODO: Fix the zend_register_internal_class_with_flags issue
45+
fixedContent := strings.ReplaceAll(content,
46+
"zend_register_internal_class_with_flags(&ce, NULL, 0)",
47+
"zend_register_internal_class(&ce)")
48+
49+
return WriteFile(arginfoPath, fixedContent)
50+
}

internal/extgen/cfile.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package extgen
2+
3+
import (
4+
"bytes"
5+
_ "embed"
6+
"path/filepath"
7+
"strings"
8+
"text/template"
9+
)
10+
11+
//go:embed templates/extension.c.tpl
12+
var cFileContent string
13+
14+
type cFileGenerator struct {
15+
generator *Generator
16+
}
17+
18+
type cTemplateData struct {
19+
BaseName string
20+
Functions []phpFunction
21+
Classes []phpClass
22+
Constants []phpConstant
23+
Version string
24+
}
25+
26+
func (cg *cFileGenerator) generate() error {
27+
filename := filepath.Join(cg.generator.BuildDir, cg.generator.BaseName+".c")
28+
content, err := cg.buildContent()
29+
if err != nil {
30+
return err
31+
}
32+
return WriteFile(filename, content)
33+
}
34+
35+
func (cg *cFileGenerator) buildContent() (string, error) {
36+
var builder strings.Builder
37+
38+
templateContent, err := cg.getTemplateContent()
39+
if err != nil {
40+
return "", err
41+
}
42+
builder.WriteString(templateContent)
43+
44+
for _, fn := range cg.generator.Functions {
45+
fnGen := PHPFuncGenerator{paramParser: &ParameterParser{}}
46+
builder.WriteString(fnGen.generate(fn))
47+
}
48+
49+
return builder.String(), nil
50+
}
51+
52+
func (cg *cFileGenerator) getTemplateContent() (string, error) {
53+
tmpl, err := template.New("cfile").Funcs(template.FuncMap{
54+
"inc": func(i int) int {
55+
return i + 1
56+
},
57+
}).Parse(cFileContent)
58+
59+
if err != nil {
60+
return "", err
61+
}
62+
63+
data := cTemplateData{
64+
BaseName: cg.generator.BaseName,
65+
Functions: cg.generator.Functions,
66+
Classes: cg.generator.Classes,
67+
Constants: cg.generator.Constants,
68+
Version: "1.0.0",
69+
}
70+
71+
var buf bytes.Buffer
72+
err = tmpl.Execute(&buf, data)
73+
if err != nil {
74+
return "", err
75+
}
76+
77+
return buf.String(), nil
78+
}

0 commit comments

Comments
 (0)