From 7ea6e7c093682da997770740a1bcb91489b8d64c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 21:02:57 +0200 Subject: [PATCH 01/14] add ability to specify startup/shutdown functions Signed-off-by: Robert Landers --- docs/extensions.md | 25 ++++++++ internal/extgen/cfile.go | 2 + internal/extgen/generator.go | 7 +++ internal/extgen/moduleParser.go | 73 +++++++++++++++++++++++ internal/extgen/parser.go | 6 ++ internal/extgen/templates/extension.c.tpl | 16 ++++- 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 internal/extgen/moduleParser.go diff --git a/docs/extensions.md b/docs/extensions.md index 0efc34f6d..a877d4f0e 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -267,6 +267,31 @@ $user->updateInfo(null, 25, null); // Name and active are null This design ensures that your Go code has complete control over how the object's state is accessed and modified, providing better encapsulation and type safety. +### Module Initialization and Shutdown + +The generator supports defining module initialization and shutdown functions using the `//export_php:module` directive. +This allows you to perform setup and cleanup operations when your extension is loaded and unloaded. + +```go +//export_php:module init=initializeModule, shutdown=cleanupModule +``` + +The `init` parameter specifies a function that will be called when the module is initialized, and the `shutdown` parameter specifies a function that will be called when the module is shut down. Both parameters are optional; you can specify either one, both, or none. + +The specified functions should be defined in your Go code: + +```go +func initializeModule() { +// Perform initialization tasks +// For example, set up global resources, initialize data structures, etc. +} + +func cleanupModule() { +// Perform cleanup tasks +// For example, free resources, close connections, etc. +} +``` + ### Declaring Constants The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconstant` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code. diff --git a/internal/extgen/cfile.go b/internal/extgen/cfile.go index e3b7b5fd9..c8fb4c633 100644 --- a/internal/extgen/cfile.go +++ b/internal/extgen/cfile.go @@ -23,6 +23,7 @@ type cTemplateData struct { Classes []phpClass Constants []phpConstant Namespace string + Module *phpModule } func (cg *cFileGenerator) generate() error { @@ -68,6 +69,7 @@ func (cg *cFileGenerator) getTemplateContent() (string, error) { Classes: cg.generator.Classes, Constants: cg.generator.Constants, Namespace: cg.generator.Namespace, + Module: cg.generator.Module, }); err != nil { return "", err } diff --git a/internal/extgen/generator.go b/internal/extgen/generator.go index f3c31e816..93aeaa2fb 100644 --- a/internal/extgen/generator.go +++ b/internal/extgen/generator.go @@ -15,6 +15,7 @@ type Generator struct { Classes []phpClass Constants []phpConstant Namespace string + Module *phpModule } // EXPERIMENTAL @@ -86,6 +87,12 @@ func (g *Generator) parseSource() error { } g.Namespace = ns + module, err := parser.ParseModule(g.SourceFile) + if err != nil { + return fmt.Errorf("parsing module: %w", err) + } + g.Module = module + return nil } diff --git a/internal/extgen/moduleParser.go b/internal/extgen/moduleParser.go new file mode 100644 index 000000000..7f23a6fc5 --- /dev/null +++ b/internal/extgen/moduleParser.go @@ -0,0 +1,73 @@ +package extgen + +import ( + "bufio" + "os" + "regexp" + "strings" +) + +var phpModuleParser = regexp.MustCompile(`//\s*export_php:module\s*(.*)`) + +// phpModule represents a PHP module with optional init and shutdown functions +type phpModule struct { + InitFunc string // Name of the init function + ShutdownFunc string // Name of the shutdown function +} + +// ModuleParser parses PHP module directives from Go source files +type ModuleParser struct{} + +// parse parses the source file for PHP module directives +func (mp *ModuleParser) parse(filename string) (*phpModule, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if matches := phpModuleParser.FindStringSubmatch(line); matches != nil { + moduleInfo := strings.TrimSpace(matches[1]) + return mp.parseModuleInfo(moduleInfo) + } + } + + // No module directive found + return nil, nil +} + +// parseModuleInfo parses the module info string to extract init and shutdown function names +func (mp *ModuleParser) parseModuleInfo(moduleInfo string) (*phpModule, error) { + module := &phpModule{} + + // Split the module info by commas + parts := strings.Split(moduleInfo, ",") + + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + // Split each part by equals sign + keyValue := strings.SplitN(part, "=", 2) + if len(keyValue) != 2 { + continue + } + + key := strings.TrimSpace(keyValue[0]) + value := strings.TrimSpace(keyValue[1]) + + switch key { + case "init": + module.InitFunc = value + case "shutdown": + module.ShutdownFunc = value + } + } + + return module, nil +} diff --git a/internal/extgen/parser.go b/internal/extgen/parser.go index 690be2792..b8ffccc06 100644 --- a/internal/extgen/parser.go +++ b/internal/extgen/parser.go @@ -25,3 +25,9 @@ func (p *SourceParser) ParseNamespace(filename string) (string, error) { namespaceParser := NamespaceParser{} return namespaceParser.parse(filename) } + +// EXPERIMENTAL +func (p *SourceParser) ParseModule(filename string) (*phpModule, error) { + moduleParser := &ModuleParser{} + return moduleParser.parse(filename) +} diff --git a/internal/extgen/templates/extension.c.tpl b/internal/extgen/templates/extension.c.tpl index 6ae0b3474..1689b030a 100644 --- a/internal/extgen/templates/extension.c.tpl +++ b/internal/extgen/templates/extension.c.tpl @@ -167,14 +167,28 @@ PHP_MINIT_FUNCTION({{.BaseName}}) { {{- end}} {{- end}} {{- end}} + + {{if and .Module .Module.InitFunc}} + {{.Module.InitFunc}}_wrapper(); + {{end}} + + return SUCCESS; +} + +{{if .Module}} +{{if .Module.ShutdownFunc}} +PHP_MSHUTDOWN_FUNCTION({{.BaseName}}) { + {{.Module.ShutdownFunc}}_wrapper(); return SUCCESS; } +{{end}} +{{end}} zend_module_entry {{.BaseName}}_module_entry = {STANDARD_MODULE_HEADER, "{{.BaseName}}", ext_functions, /* Functions */ PHP_MINIT({{.BaseName}}), /* MINIT */ - NULL, /* MSHUTDOWN */ + {{if and .Module .Module.ShutdownFunc}}PHP_MSHUTDOWN({{.BaseName}}),{{else}}NULL,{{end}} /* MSHUTDOWN */ NULL, /* RINIT */ NULL, /* RSHUTDOWN */ NULL, /* MINFO */ From 8efbc6c1e2a96bc76e61e1bf710b11b946cc288f Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 21:04:11 +0200 Subject: [PATCH 02/14] add tests Signed-off-by: Robert Landers --- internal/extgen/moduleParser_test.go | 265 +++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 internal/extgen/moduleParser_test.go diff --git a/internal/extgen/moduleParser_test.go b/internal/extgen/moduleParser_test.go new file mode 100644 index 000000000..46bdb53ae --- /dev/null +++ b/internal/extgen/moduleParser_test.go @@ -0,0 +1,265 @@ +package extgen + +import ( + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestModuleParser(t *testing.T) { + tests := []struct { + name string + input string + expected *phpModule + }{ + { + name: "both init and shutdown", + input: `package main + +//export_php:module init=initializeModule, shutdown=cleanupModule +func initializeModule() { + // Initialization code +} + +func cleanupModule() { + // Cleanup code +}`, + expected: &phpModule{ + InitFunc: "initializeModule", + ShutdownFunc: "cleanupModule", + }, + }, + { + name: "only init function", + input: `package main + +//export_php:module init=initializeModule +func initializeModule() { + // Initialization code +}`, + expected: &phpModule{ + InitFunc: "initializeModule", + ShutdownFunc: "", + }, + }, + { + name: "only shutdown function", + input: `package main + +//export_php:module shutdown=cleanupModule +func cleanupModule() { + // Cleanup code +}`, + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "cleanupModule", + }, + }, + { + name: "with extra whitespace", + input: `package main + +//export_php:module init = initModule , shutdown = shutdownModule +func initModule() { + // Initialization code +} + +func shutdownModule() { + // Cleanup code +}`, + expected: &phpModule{ + InitFunc: "initModule", + ShutdownFunc: "shutdownModule", + }, + }, + { + name: "no module directive", + input: `package main + +func regularFunction() { + // Just a regular Go function +}`, + expected: nil, + }, + { + name: "empty module directive", + input: `package main + +//export_php:module +func someFunction() { + // Some code +}`, + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "", + }, + }, + { + name: "invalid module directive format", + input: `package main + +//export_php:module init:initFunc, shutdown:shutdownFunc +func initFunc() { + // Initialization code +} + +func shutdownFunc() { + // Cleanup code +}`, + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "", + }, + }, + { + name: "unrecognized keys", + input: `package main + +//export_php:module start=startFunc, stop=stopFunc +func startFunc() { + // Start code +} + +func stopFunc() { + // Stop code +}`, + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, tt.name+".go") + require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644)) + + parser := &ModuleParser{} + module, err := parser.parse(fileName) + require.NoError(t, err) + + if tt.expected == nil { + assert.Nil(t, module, "parse() should return nil for no module directive") + } else { + assert.NotNil(t, module, "parse() should not return nil") + assert.Equal(t, tt.expected.InitFunc, module.InitFunc, "InitFunc mismatch") + assert.Equal(t, tt.expected.ShutdownFunc, module.ShutdownFunc, "ShutdownFunc mismatch") + } + }) + } +} + +func TestParseModuleInfo(t *testing.T) { + tests := []struct { + name string + info string + expected *phpModule + }{ + { + name: "both init and shutdown", + info: "init=initializeModule, shutdown=cleanupModule", + expected: &phpModule{ + InitFunc: "initializeModule", + ShutdownFunc: "cleanupModule", + }, + }, + { + name: "only init", + info: "init=initializeModule", + expected: &phpModule{ + InitFunc: "initializeModule", + ShutdownFunc: "", + }, + }, + { + name: "only shutdown", + info: "shutdown=cleanupModule", + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "cleanupModule", + }, + }, + { + name: "with extra whitespace", + info: " init = initModule , shutdown = shutdownModule ", + expected: &phpModule{ + InitFunc: "initModule", + ShutdownFunc: "shutdownModule", + }, + }, + { + name: "empty string", + info: "", + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "", + }, + }, + { + name: "invalid format - no equals sign", + info: "init initFunc, shutdown shutdownFunc", + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "", + }, + }, + { + name: "invalid format - wrong separator", + info: "init=initFunc; shutdown=shutdownFunc", + expected: &phpModule{ + InitFunc: "initFunc", + ShutdownFunc: "", + }, + }, + { + name: "unrecognized keys", + info: "start=startFunc, stop=stopFunc", + expected: &phpModule{ + InitFunc: "", + ShutdownFunc: "", + }, + }, + { + name: "mixed valid and invalid", + info: "init=initFunc, invalid, shutdown=shutdownFunc", + expected: &phpModule{ + InitFunc: "initFunc", + ShutdownFunc: "shutdownFunc", + }, + }, + { + name: "reversed order", + info: "shutdown=shutdownFunc, init=initFunc", + expected: &phpModule{ + InitFunc: "initFunc", + ShutdownFunc: "shutdownFunc", + }, + }, + } + + parser := &ModuleParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + module, err := parser.parseModuleInfo(tt.info) + + assert.NoError(t, err, "parseModuleInfo() unexpected error") + assert.NotNil(t, module, "parseModuleInfo() should not return nil") + assert.Equal(t, tt.expected.InitFunc, module.InitFunc, "InitFunc mismatch") + assert.Equal(t, tt.expected.ShutdownFunc, module.ShutdownFunc, "ShutdownFunc mismatch") + }) + } +} + +func TestModuleParserFileErrors(t *testing.T) { + parser := &ModuleParser{} + + // Test with non-existent file + module, err := parser.parse("non_existent_file.go") + assert.Error(t, err, "parse() should return error for non-existent file") + assert.Nil(t, module, "parse() should return nil for non-existent file") +} \ No newline at end of file From 327a20ce63af4d4fe0c542aeb3b64b996483fdd7 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 21:24:22 +0200 Subject: [PATCH 03/14] update to handle tagging specific functions Signed-off-by: Robert Landers --- docs/extensions.md | 24 +-- internal/extgen/gofile.go | 2 + internal/extgen/moduleParser.go | 103 ++++++++--- internal/extgen/moduleParser_test.go | 203 +++++++++------------ internal/extgen/templates/extension.c.tpl | 4 +- internal/extgen/templates/extension.go.tpl | 12 ++ 6 files changed, 195 insertions(+), 153 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index a877d4f0e..09b4e1a00 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -272,26 +272,28 @@ This design ensures that your Go code has complete control over how the object's The generator supports defining module initialization and shutdown functions using the `//export_php:module` directive. This allows you to perform setup and cleanup operations when your extension is loaded and unloaded. -```go -//export_php:module init=initializeModule, shutdown=cleanupModule -``` - -The `init` parameter specifies a function that will be called when the module is initialized, and the `shutdown` parameter specifies a function that will be called when the module is shut down. Both parameters are optional; you can specify either one, both, or none. - -The specified functions should be defined in your Go code: +To define an initialization function, tag it with `//export_php:module init`: ```go +//export_php:module init func initializeModule() { -// Perform initialization tasks -// For example, set up global resources, initialize data structures, etc. + // Perform initialization tasks + // For example, set up global resources, initialize data structures, etc. } +``` + +To define a shutdown function, tag it with `//export_php:module shutdown`: +```go +//export_php:module shutdown func cleanupModule() { -// Perform cleanup tasks -// For example, free resources, close connections, etc. + // Perform cleanup tasks + // For example, free resources, close connections, etc. } ``` +You can define either one, both, or none of these functions. The initialization function will be called when the PHP module is loaded, and the shutdown function will be called when the PHP module is unloaded. + ### Declaring Constants The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconstant` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code. diff --git a/internal/extgen/gofile.go b/internal/extgen/gofile.go index d90883bff..2e2166e54 100644 --- a/internal/extgen/gofile.go +++ b/internal/extgen/gofile.go @@ -25,6 +25,7 @@ type goTemplateData struct { InternalFunctions []string Functions []phpFunction Classes []phpClass + Module *phpModule } func (gg *GoFileGenerator) generate() error { @@ -62,6 +63,7 @@ func (gg *GoFileGenerator) buildContent() (string, error) { InternalFunctions: internalFunctions, Functions: gg.generator.Functions, Classes: classes, + Module: gg.generator.Module, }) if err != nil { diff --git a/internal/extgen/moduleParser.go b/internal/extgen/moduleParser.go index 7f23a6fc5..5671f347d 100644 --- a/internal/extgen/moduleParser.go +++ b/internal/extgen/moduleParser.go @@ -2,17 +2,20 @@ package extgen import ( "bufio" + "fmt" "os" "regexp" "strings" ) -var phpModuleParser = regexp.MustCompile(`//\s*export_php:module\s*(.*)`) +var phpModuleParser = regexp.MustCompile(`//\s*export_php:module\s+(init|shutdown)`) // phpModule represents a PHP module with optional init and shutdown functions type phpModule struct { InitFunc string // Name of the init function + InitCode string // Code of the init function ShutdownFunc string // Name of the shutdown function + ShutdownCode string // Code of the shutdown function } // ModuleParser parses PHP module directives from Go source files @@ -27,47 +30,93 @@ func (mp *ModuleParser) parse(filename string) (*phpModule, error) { defer file.Close() scanner := bufio.NewScanner(file) + module := &phpModule{} + var currentDirective string + var lineNumber int + for scanner.Scan() { + lineNumber++ line := strings.TrimSpace(scanner.Text()) + if matches := phpModuleParser.FindStringSubmatch(line); matches != nil { - moduleInfo := strings.TrimSpace(matches[1]) - return mp.parseModuleInfo(moduleInfo) + directiveType := matches[1] + currentDirective = directiveType + continue + } + + // If we have a current directive and encounter a non-comment line + // that doesn't start with "func ", reset the current directive + if currentDirective != "" && (line == "" || (!strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "func "))) { + currentDirective = "" + continue + } + + if currentDirective != "" && strings.HasPrefix(line, "func ") { + funcName, funcCode, err := mp.extractGoFunction(scanner, line) + if err != nil { + return nil, fmt.Errorf("extracting Go function at line %d: %w", lineNumber, err) + } + + switch currentDirective { + case "init": + module.InitFunc = funcName + module.InitCode = funcCode + case "shutdown": + module.ShutdownFunc = funcName + module.ShutdownCode = funcCode + } + + currentDirective = "" } } - // No module directive found - return nil, nil + // If we found no module functions, return nil + if module.InitFunc == "" && module.ShutdownFunc == "" { + return nil, nil + } + + return module, scanner.Err() } -// parseModuleInfo parses the module info string to extract init and shutdown function names -func (mp *ModuleParser) parseModuleInfo(moduleInfo string) (*phpModule, error) { - module := &phpModule{} +// extractGoFunction extracts the function name and code from a function declaration +func (mp *ModuleParser) extractGoFunction(scanner *bufio.Scanner, firstLine string) (string, string, error) { + // Extract function name from the first line + funcNameRegex := regexp.MustCompile(`func\s+([a-zA-Z0-9_]+)`) + matches := funcNameRegex.FindStringSubmatch(firstLine) + if len(matches) < 2 { + return "", "", fmt.Errorf("could not extract function name from line: %s", firstLine) + } + funcName := matches[1] - // Split the module info by commas - parts := strings.Split(moduleInfo, ",") + // Collect the function code + goFunc := firstLine + "\n" + braceCount := 0 - for _, part := range parts { - part = strings.TrimSpace(part) - if part == "" { - continue + // Count opening braces in the first line + for _, char := range firstLine { + if char == '{' { + braceCount++ } + } + + // Continue reading until we find the matching closing brace + for braceCount > 0 && scanner.Scan() { + line := scanner.Text() + goFunc += line + "\n" - // Split each part by equals sign - keyValue := strings.SplitN(part, "=", 2) - if len(keyValue) != 2 { - continue + for _, char := range line { + switch char { + case '{': + braceCount++ + case '}': + braceCount-- + } } - key := strings.TrimSpace(keyValue[0]) - value := strings.TrimSpace(keyValue[1]) - - switch key { - case "init": - module.InitFunc = value - case "shutdown": - module.ShutdownFunc = value + if braceCount == 0 { + break } } - return module, nil + return funcName, goFunc, nil } diff --git a/internal/extgen/moduleParser_test.go b/internal/extgen/moduleParser_test.go index 46bdb53ae..1b615dcd7 100644 --- a/internal/extgen/moduleParser_test.go +++ b/internal/extgen/moduleParser_test.go @@ -1,9 +1,11 @@ package extgen import ( + "bufio" "github.com/stretchr/testify/require" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -19,11 +21,12 @@ func TestModuleParser(t *testing.T) { name: "both init and shutdown", input: `package main -//export_php:module init=initializeModule, shutdown=cleanupModule +//export_php:module init func initializeModule() { // Initialization code } +//export_php:module shutdown func cleanupModule() { // Cleanup code }`, @@ -36,7 +39,7 @@ func cleanupModule() { name: "only init function", input: `package main -//export_php:module init=initializeModule +//export_php:module init func initializeModule() { // Initialization code }`, @@ -49,7 +52,7 @@ func initializeModule() { name: "only shutdown function", input: `package main -//export_php:module shutdown=cleanupModule +//export_php:module shutdown func cleanupModule() { // Cleanup code }`, @@ -62,11 +65,12 @@ func cleanupModule() { name: "with extra whitespace", input: `package main -//export_php:module init = initModule , shutdown = shutdownModule +//export_php:module init func initModule() { // Initialization code } +//export_php:module shutdown func shutdownModule() { // Cleanup code }`, @@ -85,51 +89,63 @@ func regularFunction() { expected: nil, }, { - name: "empty module directive", + name: "functions with braces", input: `package main -//export_php:module -func someFunction() { - // Some code +//export_php:module init +func initModule() { + if true { + // Do something + } + for i := 0; i < 10; i++ { + // Loop + } +} + +//export_php:module shutdown +func shutdownModule() { + if true { + // Do something else + } }`, expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "", + InitFunc: "initModule", + ShutdownFunc: "shutdownModule", }, }, { - name: "invalid module directive format", + name: "multiple functions between directives", input: `package main -//export_php:module init:initFunc, shutdown:shutdownFunc -func initFunc() { - // Initialization code +//export_php:module init +func initModule() { + // Init code } -func shutdownFunc() { - // Cleanup code +func someOtherFunction() { + // This should be ignored +} + +//export_php:module shutdown +func shutdownModule() { + // Shutdown code }`, expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "", + InitFunc: "initModule", + ShutdownFunc: "shutdownModule", }, }, { - name: "unrecognized keys", + name: "directive without function", input: `package main -//export_php:module start=startFunc, stop=stopFunc -func startFunc() { - // Start code -} +//export_php:module init +// No function follows -func stopFunc() { - // Stop code +func regularFunction() { + // This should be ignored }`, - expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "", - }, + expected: nil, }, } @@ -149,108 +165,69 @@ func stopFunc() { assert.NotNil(t, module, "parse() should not return nil") assert.Equal(t, tt.expected.InitFunc, module.InitFunc, "InitFunc mismatch") assert.Equal(t, tt.expected.ShutdownFunc, module.ShutdownFunc, "ShutdownFunc mismatch") + + // Check that function code was extracted + if tt.expected.InitFunc != "" { + assert.Contains(t, module.InitCode, "func "+tt.expected.InitFunc, "InitCode should contain function declaration") + assert.True(t, strings.HasSuffix(module.InitCode, "}\n"), "InitCode should end with closing brace") + } + + if tt.expected.ShutdownFunc != "" { + assert.Contains(t, module.ShutdownCode, "func "+tt.expected.ShutdownFunc, "ShutdownCode should contain function declaration") + assert.True(t, strings.HasSuffix(module.ShutdownCode, "}\n"), "ShutdownCode should end with closing brace") + } } }) } } -func TestParseModuleInfo(t *testing.T) { +func TestExtractGoFunction(t *testing.T) { tests := []struct { - name string - info string - expected *phpModule + name string + input string + firstLine string + expectedName string + expectedPrefix string + expectedSuffix string }{ { - name: "both init and shutdown", - info: "init=initializeModule, shutdown=cleanupModule", - expected: &phpModule{ - InitFunc: "initializeModule", - ShutdownFunc: "cleanupModule", - }, - }, - { - name: "only init", - info: "init=initializeModule", - expected: &phpModule{ - InitFunc: "initializeModule", - ShutdownFunc: "", - }, - }, - { - name: "only shutdown", - info: "shutdown=cleanupModule", - expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "cleanupModule", - }, - }, - { - name: "with extra whitespace", - info: " init = initModule , shutdown = shutdownModule ", - expected: &phpModule{ - InitFunc: "initModule", - ShutdownFunc: "shutdownModule", - }, - }, - { - name: "empty string", - info: "", - expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "", - }, - }, - { - name: "invalid format - no equals sign", - info: "init initFunc, shutdown shutdownFunc", - expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "", - }, - }, - { - name: "invalid format - wrong separator", - info: "init=initFunc; shutdown=shutdownFunc", - expected: &phpModule{ - InitFunc: "initFunc", - ShutdownFunc: "", - }, + name: "simple function", + input: "func testFunc() {\n\t// Some code\n}\n", + firstLine: "func testFunc() {", + expectedName: "testFunc", + expectedPrefix: "func testFunc() {", + expectedSuffix: "}\n", }, { - name: "unrecognized keys", - info: "start=startFunc, stop=stopFunc", - expected: &phpModule{ - InitFunc: "", - ShutdownFunc: "", - }, - }, - { - name: "mixed valid and invalid", - info: "init=initFunc, invalid, shutdown=shutdownFunc", - expected: &phpModule{ - InitFunc: "initFunc", - ShutdownFunc: "shutdownFunc", - }, + name: "function with parameters", + input: "func initModule(param1 string, param2 int) {\n\t// Init code\n}\n", + firstLine: "func initModule(param1 string, param2 int) {", + expectedName: "initModule", + expectedPrefix: "func initModule(param1 string, param2 int) {", + expectedSuffix: "}\n", }, { - name: "reversed order", - info: "shutdown=shutdownFunc, init=initFunc", - expected: &phpModule{ - InitFunc: "initFunc", - ShutdownFunc: "shutdownFunc", - }, + name: "function with nested braces", + input: "func complexFunc() {\n\tif true {\n\t\t// Nested code\n\t}\n}\n", + firstLine: "func complexFunc() {", + expectedName: "complexFunc", + expectedPrefix: "func complexFunc() {", + expectedSuffix: "}\n", }, } - parser := &ModuleParser{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - module, err := parser.parseModuleInfo(tt.info) + parser := &ModuleParser{} + scanner := bufio.NewScanner(strings.NewReader(tt.input)) + scanner.Scan() // Read the first line + + name, code, err := parser.extractGoFunction(scanner, tt.firstLine) - assert.NoError(t, err, "parseModuleInfo() unexpected error") - assert.NotNil(t, module, "parseModuleInfo() should not return nil") - assert.Equal(t, tt.expected.InitFunc, module.InitFunc, "InitFunc mismatch") - assert.Equal(t, tt.expected.ShutdownFunc, module.ShutdownFunc, "ShutdownFunc mismatch") + assert.NoError(t, err) + assert.Equal(t, tt.expectedName, name) + assert.True(t, strings.HasPrefix(code, tt.expectedPrefix), "Function code should start with the declaration") + assert.True(t, strings.HasSuffix(code, tt.expectedSuffix), "Function code should end with closing brace") }) } } diff --git a/internal/extgen/templates/extension.c.tpl b/internal/extgen/templates/extension.c.tpl index 1689b030a..7a922c8f0 100644 --- a/internal/extgen/templates/extension.c.tpl +++ b/internal/extgen/templates/extension.c.tpl @@ -169,7 +169,7 @@ PHP_MINIT_FUNCTION({{.BaseName}}) { {{- end}} {{if and .Module .Module.InitFunc}} - {{.Module.InitFunc}}_wrapper(); + {{.Module.InitFunc}}(); {{end}} return SUCCESS; @@ -178,7 +178,7 @@ PHP_MINIT_FUNCTION({{.BaseName}}) { {{if .Module}} {{if .Module.ShutdownFunc}} PHP_MSHUTDOWN_FUNCTION({{.BaseName}}) { - {{.Module.ShutdownFunc}}_wrapper(); + {{.Module.ShutdownFunc}}(); return SUCCESS; } {{end}} diff --git a/internal/extgen/templates/extension.go.tpl b/internal/extgen/templates/extension.go.tpl index 39e7ae031..acee12565 100644 --- a/internal/extgen/templates/extension.go.tpl +++ b/internal/extgen/templates/extension.go.tpl @@ -20,6 +20,18 @@ const {{.Name}} = {{.Value}} {{.}} {{- end}} +{{- if .Module}} +{{- if .Module.InitFunc}} +//export {{.Module.InitFunc}} +{{.Module.InitCode}} +{{- end}} + +{{- if .Module.ShutdownFunc}} +//export {{.Module.ShutdownFunc}} +{{.Module.ShutdownCode}} +{{- end}} +{{- end}} + {{- range .Functions}} //export {{.Name}} {{.GoFunction}} From 74e9e9aa194064346dbc7c239ccd2cfde57cfa39 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 21:28:02 +0200 Subject: [PATCH 04/14] handle init function case Signed-off-by: Robert Landers --- internal/extgen/gofile.go | 13 +++++++++++++ internal/extgen/templates/extension.go.tpl | 2 ++ 2 files changed, 15 insertions(+) diff --git a/internal/extgen/gofile.go b/internal/extgen/gofile.go index 2e2166e54..36e8eb982 100644 --- a/internal/extgen/gofile.go +++ b/internal/extgen/gofile.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "path/filepath" + "strings" "text/template" "github.com/Masterminds/sprig/v3" @@ -26,6 +27,7 @@ type goTemplateData struct { Functions []phpFunction Classes []phpClass Module *phpModule + HasInitFunction bool } func (gg *GoFileGenerator) generate() error { @@ -55,6 +57,16 @@ func (gg *GoFileGenerator) buildContent() (string, error) { classes := make([]phpClass, len(gg.generator.Classes)) copy(classes, gg.generator.Classes) + // Check if there's already an init() function in the source file + hasInitFunction := false + for _, fn := range internalFunctions { + if strings.HasPrefix(fn, "func init()") { + hasInitFunction = true + fmt.Printf("Warning: An init() function already exists in the source file. Make sure to call frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) in your init function.\n") + break + } + } + templateContent, err := gg.getTemplateContent(goTemplateData{ PackageName: SanitizePackageName(gg.generator.BaseName), BaseName: gg.generator.BaseName, @@ -64,6 +76,7 @@ func (gg *GoFileGenerator) buildContent() (string, error) { Functions: gg.generator.Functions, Classes: classes, Module: gg.generator.Module, + HasInitFunction: hasInitFunction, }) if err != nil { diff --git a/internal/extgen/templates/extension.go.tpl b/internal/extgen/templates/extension.go.tpl index acee12565..22f2d6b9d 100644 --- a/internal/extgen/templates/extension.go.tpl +++ b/internal/extgen/templates/extension.go.tpl @@ -10,9 +10,11 @@ import "runtime/cgo" import {{.}} {{- end}} +{{if not .HasInitFunction}} func init() { frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) } +{{end}} {{range .Constants}} const {{.Name}} = {{.Value}} {{- end}} From 0d9dda91e9796f7b0cead4d3d5f642c388ab3f1e Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sun, 3 Aug 2025 21:50:10 +0200 Subject: [PATCH 05/14] handle file.close error Signed-off-by: Robert Landers --- internal/extgen/moduleParser.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/internal/extgen/moduleParser.go b/internal/extgen/moduleParser.go index 5671f347d..17ff681fa 100644 --- a/internal/extgen/moduleParser.go +++ b/internal/extgen/moduleParser.go @@ -22,22 +22,27 @@ type phpModule struct { type ModuleParser struct{} // parse parses the source file for PHP module directives -func (mp *ModuleParser) parse(filename string) (*phpModule, error) { +func (mp *ModuleParser) parse(filename string) (module *phpModule, err error) { file, err := os.Open(filename) if err != nil { return nil, err } - defer file.Close() + defer func() { + e := file.Close() + if err == nil { + err = e + } + }() scanner := bufio.NewScanner(file) - module := &phpModule{} + module = &phpModule{} var currentDirective string var lineNumber int for scanner.Scan() { lineNumber++ line := strings.TrimSpace(scanner.Text()) - + if matches := phpModuleParser.FindStringSubmatch(line); matches != nil { directiveType := matches[1] currentDirective = directiveType @@ -87,23 +92,23 @@ func (mp *ModuleParser) extractGoFunction(scanner *bufio.Scanner, firstLine stri return "", "", fmt.Errorf("could not extract function name from line: %s", firstLine) } funcName := matches[1] - + // Collect the function code goFunc := firstLine + "\n" braceCount := 0 - + // Count opening braces in the first line for _, char := range firstLine { if char == '{' { braceCount++ } } - + // Continue reading until we find the matching closing brace for braceCount > 0 && scanner.Scan() { line := scanner.Text() goFunc += line + "\n" - + for _, char := range line { switch char { case '{': @@ -112,11 +117,11 @@ func (mp *ModuleParser) extractGoFunction(scanner *bufio.Scanner, firstLine stri braceCount-- } } - + if braceCount == 0 { break } } - + return funcName, goFunc, nil } From 31e045bb751982012034e1a24f4f0db4ce780e51 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Tue, 5 Aug 2025 19:02:59 +0200 Subject: [PATCH 06/14] remove import that causes issues Signed-off-by: Robert Landers --- internal/extgen/templates/extension.go.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/extgen/templates/extension.go.tpl b/internal/extgen/templates/extension.go.tpl index 22f2d6b9d..fe82eeeeb 100644 --- a/internal/extgen/templates/extension.go.tpl +++ b/internal/extgen/templates/extension.go.tpl @@ -5,7 +5,6 @@ package {{.PackageName}} #include "{{.BaseName}}.h" */ import "C" -import "runtime/cgo" {{- range .Imports}} import {{.}} {{- end}} From 6c208e275300f906f5301569e9fc779326970664 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Wed, 6 Aug 2025 19:27:38 +0200 Subject: [PATCH 07/14] Revert "remove import that causes issues" This reverts commit 3d9d672248778852ffa47c9bad238b2587832077. --- internal/extgen/templates/extension.go.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/extgen/templates/extension.go.tpl b/internal/extgen/templates/extension.go.tpl index fe82eeeeb..22f2d6b9d 100644 --- a/internal/extgen/templates/extension.go.tpl +++ b/internal/extgen/templates/extension.go.tpl @@ -5,6 +5,7 @@ package {{.PackageName}} #include "{{.BaseName}}.h" */ import "C" +import "runtime/cgo" {{- range .Imports}} import {{.}} {{- end}} From f76fd14c3f5a2128082d978b70f30e4b7f8b125d Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Sat, 9 Aug 2025 22:10:56 +0200 Subject: [PATCH 08/14] only import runtime/cgo when it needs to Signed-off-by: Robert Landers --- internal/extgen/gofile_test.go | 159 +++++++++++++++++++++ internal/extgen/templates/extension.go.tpl | 2 + 2 files changed, 161 insertions(+) diff --git a/internal/extgen/gofile_test.go b/internal/extgen/gofile_test.go index 11e962f3d..02df9ce57 100644 --- a/internal/extgen/gofile_test.go +++ b/internal/extgen/gofile_test.go @@ -732,3 +732,162 @@ func testGoFileInternalFunctions(t *testing.T, content string) { t.Log("No internal functions found (this may be expected)") } } + +func TestGoFileGenerator_RuntimeCgoImportGating(t *testing.T) { + tests := []struct { + name string + baseName string + sourceFile string + functions []phpFunction + classes []phpClass + expectCgoImport bool + description string + }{ + { + name: "extension without classes should not include runtime/cgo import", + baseName: "no_classes", + sourceFile: createTempSourceFile(t, `package main + +//export_php: simpleFunc(): void +func simpleFunc() { + // simple function +}`), + functions: []phpFunction{ + { + Name: "simpleFunc", + ReturnType: phpVoid, + GoFunction: "func simpleFunc() {\n\t// simple function\n}", + }, + }, + classes: nil, + expectCgoImport: false, + description: "Extensions with only functions should not import runtime/cgo", + }, + { + name: "extension with classes should include runtime/cgo import", + baseName: "with_classes", + sourceFile: createTempSourceFile(t, `package main + +//export_php:class TestClass +type TestStruct struct { + name string +} + +//export_php:method TestClass::getName(): string +func (ts *TestStruct) GetName() string { + return ts.name +}`), + functions: nil, + classes: []phpClass{ + { + Name: "TestClass", + GoStruct: "TestStruct", + Methods: []phpClassMethod{ + { + Name: "GetName", + PhpName: "getName", + ClassName: "TestClass", + Signature: "getName(): string", + ReturnType: phpString, + Params: []phpParameter{}, + GoFunction: "func (ts *TestStruct) GetName() string {\n\treturn ts.name\n}", + }, + }, + }, + }, + expectCgoImport: true, + description: "Extensions with classes should import runtime/cgo for handle management", + }, + { + name: "extension with functions and classes should include runtime/cgo import", + baseName: "mixed", + sourceFile: createTempSourceFile(t, `package main + +//export_php: utilFunc(): string +func utilFunc() string { + return "utility" +} + +//export_php:class MixedClass +type MixedStruct struct { + value int +} + +//export_php:method MixedClass::getValue(): int +func (ms *MixedStruct) GetValue() int { + return ms.value +}`), + functions: []phpFunction{ + { + Name: "utilFunc", + ReturnType: phpString, + GoFunction: "func utilFunc() string {\n\treturn \"utility\"\n}", + }, + }, + classes: []phpClass{ + { + Name: "MixedClass", + GoStruct: "MixedStruct", + Methods: []phpClassMethod{ + { + Name: "GetValue", + PhpName: "getValue", + ClassName: "MixedClass", + Signature: "getValue(): int", + ReturnType: phpInt, + Params: []phpParameter{}, + GoFunction: "func (ms *MixedStruct) GetValue() int {\n\treturn ms.value\n}", + }, + }, + }, + }, + expectCgoImport: true, + description: "Extensions with both functions and classes should import runtime/cgo", + }, + { + name: "empty extension should not include runtime/cgo import", + baseName: "empty", + sourceFile: createTempSourceFile(t, `package main + +// Empty extension for testing +`), + functions: nil, + classes: nil, + expectCgoImport: false, + description: "Empty extensions should not import runtime/cgo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + generator := &Generator{ + BaseName: tt.baseName, + SourceFile: tt.sourceFile, + Functions: tt.functions, + Classes: tt.classes, + } + + goGen := GoFileGenerator{generator} + content, err := goGen.buildContent() + require.NoError(t, err) + + cgoImportPresent := strings.Contains(content, `import "runtime/cgo"`) + + if tt.expectCgoImport { + assert.True(t, cgoImportPresent, "Extension should import runtime/cgo: %s", tt.description) + // Verify that cgo functions are also present when import is included + assert.Contains(t, content, "cgo.NewHandle", "Should contain cgo.NewHandle usage when runtime/cgo is imported") + assert.Contains(t, content, "cgo.Handle", "Should contain cgo.Handle usage when runtime/cgo is imported") + } else { + assert.False(t, cgoImportPresent, "Extension should not import runtime/cgo: %s", tt.description) + // Verify that cgo functions are not present when import is not included + assert.NotContains(t, content, "cgo.NewHandle", "Should not contain cgo.NewHandle usage when runtime/cgo is not imported") + assert.NotContains(t, content, "cgo.Handle", "Should not contain cgo.Handle usage when runtime/cgo is not imported") + } + + // Ensure exactly one C import is always present + cImportCount := strings.Count(content, `import "C"`) + assert.Equal(t, 1, cImportCount, "Should have exactly one C import") + }) + } +} diff --git a/internal/extgen/templates/extension.go.tpl b/internal/extgen/templates/extension.go.tpl index 22f2d6b9d..57dceed32 100644 --- a/internal/extgen/templates/extension.go.tpl +++ b/internal/extgen/templates/extension.go.tpl @@ -5,7 +5,9 @@ package {{.PackageName}} #include "{{.BaseName}}.h" */ import "C" +{{- if .Classes}} import "runtime/cgo" +{{- end}} {{- range .Imports}} import {{.}} {{- end}} From 50208aa818a2d8d5ab59b963b92998d4b0f5fb4a Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 11 Aug 2025 19:33:20 +0200 Subject: [PATCH 09/14] fix the regex Signed-off-by: Robert Landers --- internal/extgen/moduleParser.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/extgen/moduleParser.go b/internal/extgen/moduleParser.go index 17ff681fa..eb0336ad2 100644 --- a/internal/extgen/moduleParser.go +++ b/internal/extgen/moduleParser.go @@ -8,7 +8,10 @@ import ( "strings" ) -var phpModuleParser = regexp.MustCompile(`//\s*export_php:module\s+(init|shutdown)`) +var ( + phpModuleParser = regexp.MustCompile(`//\s*export_php:module\s+(init|shutdown)`) + funcNameRegex = regexp.MustCompile(`func\s+([a-zA-Z0-9_]+)`) +) // phpModule represents a PHP module with optional init and shutdown functions type phpModule struct { @@ -86,7 +89,6 @@ func (mp *ModuleParser) parse(filename string) (module *phpModule, err error) { // extractGoFunction extracts the function name and code from a function declaration func (mp *ModuleParser) extractGoFunction(scanner *bufio.Scanner, firstLine string) (string, string, error) { // Extract function name from the first line - funcNameRegex := regexp.MustCompile(`func\s+([a-zA-Z0-9_]+)`) matches := funcNameRegex.FindStringSubmatch(firstLine) if len(matches) < 2 { return "", "", fmt.Errorf("could not extract function name from line: %s", firstLine) From 17eba05bcd2f2d84cb3ed70aebb086ac2470a48f Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 11 Aug 2025 19:33:34 +0200 Subject: [PATCH 10/14] remove EXPERIMENTAL Signed-off-by: Robert Landers --- internal/extgen/parser.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/extgen/parser.go b/internal/extgen/parser.go index b8ffccc06..e579f31ef 100644 --- a/internal/extgen/parser.go +++ b/internal/extgen/parser.go @@ -2,31 +2,26 @@ package extgen type SourceParser struct{} -// EXPERIMENTAL func (p *SourceParser) ParseFunctions(filename string) ([]phpFunction, error) { functionParser := &FuncParser{} return functionParser.parse(filename) } -// EXPERIMENTAL func (p *SourceParser) ParseClasses(filename string) ([]phpClass, error) { classParser := classParser{} return classParser.parse(filename) } -// EXPERIMENTAL func (p *SourceParser) ParseConstants(filename string) ([]phpConstant, error) { constantParser := &ConstantParser{} return constantParser.parse(filename) } -// EXPERIMENTAL func (p *SourceParser) ParseNamespace(filename string) (string, error) { namespaceParser := NamespaceParser{} return namespaceParser.parse(filename) } -// EXPERIMENTAL func (p *SourceParser) ParseModule(filename string) (*phpModule, error) { moduleParser := &ModuleParser{} return moduleParser.parse(filename) From 36cdb72536ad7c340828ff756959bbc9945d4d5c Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 11 Aug 2025 19:34:32 +0200 Subject: [PATCH 11/14] fix newlines Signed-off-by: Robert Landers --- internal/extgen/moduleParser_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/extgen/moduleParser_test.go b/internal/extgen/moduleParser_test.go index 1b615dcd7..f09cd0243 100644 --- a/internal/extgen/moduleParser_test.go +++ b/internal/extgen/moduleParser_test.go @@ -165,13 +165,13 @@ func regularFunction() { assert.NotNil(t, module, "parse() should not return nil") assert.Equal(t, tt.expected.InitFunc, module.InitFunc, "InitFunc mismatch") assert.Equal(t, tt.expected.ShutdownFunc, module.ShutdownFunc, "ShutdownFunc mismatch") - + // Check that function code was extracted if tt.expected.InitFunc != "" { assert.Contains(t, module.InitCode, "func "+tt.expected.InitFunc, "InitCode should contain function declaration") assert.True(t, strings.HasSuffix(module.InitCode, "}\n"), "InitCode should end with closing brace") } - + if tt.expected.ShutdownFunc != "" { assert.Contains(t, module.ShutdownCode, "func "+tt.expected.ShutdownFunc, "ShutdownCode should contain function declaration") assert.True(t, strings.HasSuffix(module.ShutdownCode, "}\n"), "ShutdownCode should end with closing brace") @@ -221,9 +221,9 @@ func TestExtractGoFunction(t *testing.T) { parser := &ModuleParser{} scanner := bufio.NewScanner(strings.NewReader(tt.input)) scanner.Scan() // Read the first line - + name, code, err := parser.extractGoFunction(scanner, tt.firstLine) - + assert.NoError(t, err) assert.Equal(t, tt.expectedName, name) assert.True(t, strings.HasPrefix(code, tt.expectedPrefix), "Function code should start with the declaration") @@ -234,9 +234,9 @@ func TestExtractGoFunction(t *testing.T) { func TestModuleParserFileErrors(t *testing.T) { parser := &ModuleParser{} - + // Test with non-existent file module, err := parser.parse("non_existent_file.go") assert.Error(t, err, "parse() should return error for non-existent file") assert.Nil(t, module, "parse() should return nil for non-existent file") -} \ No newline at end of file +} From 44d58e3590d34f4ca585f96f38397ae07b500191 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 11 Aug 2025 19:52:11 +0200 Subject: [PATCH 12/14] use literals Signed-off-by: Robert Landers --- internal/extgen/gofile_test.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/internal/extgen/gofile_test.go b/internal/extgen/gofile_test.go index 02df9ce57..41ee5395d 100644 --- a/internal/extgen/gofile_test.go +++ b/internal/extgen/gofile_test.go @@ -103,7 +103,9 @@ func test() { { Name: "test", ReturnType: phpVoid, - GoFunction: "func test() {\n\t// simple function\n}", + GoFunction: `func test() { + // simple function +}`, }, }, contains: []string{ @@ -213,7 +215,9 @@ func TestGoFileGenerator_PackageNameSanitization(t *testing.T) { for _, tt := range tests { t.Run(tt.baseName, func(t *testing.T) { - sourceFile := createTempSourceFile(t, "package main\n//export_php: test(): void\nfunc test() {}") + sourceFile := createTempSourceFile(t, `package main +//export_php: test(): void +func test() {}`) generator := &Generator{ BaseName: tt.baseName, @@ -251,7 +255,8 @@ func TestGoFileGenerator_ErrorHandling(t *testing.T) { }, { name: "valid file", - sourceFile: createTempSourceFile(t, "package main\nfunc test() {}"), + sourceFile: createTempSourceFile(t, `package main +func test() {}`), expectErr: false, }, } @@ -756,7 +761,9 @@ func simpleFunc() { { Name: "simpleFunc", ReturnType: phpVoid, - GoFunction: "func simpleFunc() {\n\t// simple function\n}", + GoFunction: `func simpleFunc() { + // simple function +}`, }, }, classes: nil, @@ -790,7 +797,9 @@ func (ts *TestStruct) GetName() string { Signature: "getName(): string", ReturnType: phpString, Params: []phpParameter{}, - GoFunction: "func (ts *TestStruct) GetName() string {\n\treturn ts.name\n}", + GoFunction: `func (ts *TestStruct) GetName() string { + return ts.name +}`, }, }, }, @@ -821,7 +830,9 @@ func (ms *MixedStruct) GetValue() int { { Name: "utilFunc", ReturnType: phpString, - GoFunction: "func utilFunc() string {\n\treturn \"utility\"\n}", + GoFunction: `func utilFunc() string { + return "utility" +}`, }, }, classes: []phpClass{ @@ -836,7 +847,9 @@ func (ms *MixedStruct) GetValue() int { Signature: "getValue(): int", ReturnType: phpInt, Params: []phpParameter{}, - GoFunction: "func (ms *MixedStruct) GetValue() int {\n\treturn ms.value\n}", + GoFunction: `func (ms *MixedStruct) GetValue() int { + return ms.value +}`, }, }, }, From c57e1c6d2a87f0746df6fd229c5898f1ebcc50b4 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 11 Aug 2025 19:52:24 +0200 Subject: [PATCH 13/14] combine var Signed-off-by: Robert Landers --- internal/extgen/moduleParser.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/extgen/moduleParser.go b/internal/extgen/moduleParser.go index eb0336ad2..d61608fe6 100644 --- a/internal/extgen/moduleParser.go +++ b/internal/extgen/moduleParser.go @@ -39,8 +39,10 @@ func (mp *ModuleParser) parse(filename string) (module *phpModule, err error) { scanner := bufio.NewScanner(file) module = &phpModule{} - var currentDirective string - var lineNumber int + var ( + currentDirective string + lineNumber int + ) for scanner.Scan() { lineNumber++ From 12311107f44572a9561fd4cfef4498b1e9c35db6 Mon Sep 17 00:00:00 2001 From: Robert Landers Date: Mon, 11 Aug 2025 19:57:23 +0200 Subject: [PATCH 14/14] error when there is more than one module:init or module:shutdown Signed-off-by: Robert Landers --- internal/extgen/moduleParser.go | 6 +++ internal/extgen/moduleParser_test.go | 79 ++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/internal/extgen/moduleParser.go b/internal/extgen/moduleParser.go index d61608fe6..e3d1f3ad5 100644 --- a/internal/extgen/moduleParser.go +++ b/internal/extgen/moduleParser.go @@ -69,9 +69,15 @@ func (mp *ModuleParser) parse(filename string) (module *phpModule, err error) { switch currentDirective { case "init": + if module.InitFunc != "" { + return nil, fmt.Errorf("duplicate init directive found at line %d: init function '%s' already defined", lineNumber, module.InitFunc) + } module.InitFunc = funcName module.InitCode = funcCode case "shutdown": + if module.ShutdownFunc != "" { + return nil, fmt.Errorf("duplicate shutdown directive found at line %d: shutdown function '%s' already defined", lineNumber, module.ShutdownFunc) + } module.ShutdownFunc = funcName module.ShutdownCode = funcCode } diff --git a/internal/extgen/moduleParser_test.go b/internal/extgen/moduleParser_test.go index f09cd0243..d715ac148 100644 --- a/internal/extgen/moduleParser_test.go +++ b/internal/extgen/moduleParser_test.go @@ -232,6 +232,85 @@ func TestExtractGoFunction(t *testing.T) { } } +func TestModuleParserErrors(t *testing.T) { + tests := []struct { + name string + input string + expectedErr string + }{ + { + name: "duplicate init directives", + input: `package main + +//export_php:module init +func firstInit() { + // First init function +} + +//export_php:module init +func secondInit() { + // Second init function - should error +}`, + expectedErr: "duplicate init directive", + }, + { + name: "duplicate shutdown directives", + input: `package main + +//export_php:module shutdown +func firstShutdown() { + // First shutdown function +} + +//export_php:module shutdown +func secondShutdown() { + // Second shutdown function - should error +}`, + expectedErr: "duplicate shutdown directive", + }, + { + name: "multiple duplicates", + input: `package main + +//export_php:module init +func firstInit() { + // First init function +} + +//export_php:module init +func secondInit() { + // Duplicate init - should error +} + +//export_php:module shutdown +func firstShutdown() { + // First shutdown function +} + +//export_php:module shutdown +func secondShutdown() { + // Duplicate shutdown - should error +}`, + expectedErr: "duplicate init directive", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + fileName := filepath.Join(tmpDir, tt.name+".go") + require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644)) + + parser := &ModuleParser{} + module, err := parser.parse(fileName) + + assert.Error(t, err, "parse() should return error for duplicate directives") + assert.Contains(t, err.Error(), tt.expectedErr, "error message should contain expected text") + assert.Nil(t, module, "parse() should return nil when there's an error") + }) + } +} + func TestModuleParserFileErrors(t *testing.T) { parser := &ModuleParser{}