xp-runners / evolution

XP Runners implemented in GO
0 stars 0 forks source link

Test PHP bindings for Go #1

Open thekid opened 6 years ago

thekid commented 6 years ago

Instead of using exec, use https://github.com/deuill/go-php

/cc @mikey179

thekid commented 6 years ago
package main

import (
    "fmt"
    php "github.com/deuill/go-php"
)

func main() {
    engine, _ := php.New()
    context, _ := engine.NewContext()

    var str string = "Hello"
    context.Bind("var", str)

    val, _ := context.Eval("return $var.' World';")
    fmt.Printf("%s", val.Interface())
    // Prints 'Hello World' back to the user.

    engine.Destroy()
}
friebe@LWKA-BWYWZF2:/.../go/src/github.com/xp-runners/evolution$ ./eval
Hello World

To make this work, I needed to install both php-embed and php-dev; and to create a file php7-ubuntu.go with:

package php

// #cgo CFLAGS: -I/usr/include/php/20170718 -Iinclude/php7 -Isrc/php7
// #cgo CFLAGS: -I/usr/include/php/20170718/main -I/usr/include/php/20170718/Zend
// #cgo CFLAGS: -I/usr/include/php/20170718/TSRM
// #cgo LDFLAGS: -lphp7.2
import "C"
thekid commented 6 years ago

Needs a small patch to class-main.php:

--- /usr/bin/class-main.php     2018-09-06 20:08:05.000000000 +0200
+++ class-main.php      2018-10-01 23:57:14.613855000 +0200
@@ -71,7 +71,7 @@
 $home= getenv('HOME');
 $cwd= '.';

-if ('cgi' === PHP_SAPI || 'cgi-fcgi' === PHP_SAPI) {
+if ('cgi' === PHP_SAPI || 'cgi-fcgi' === PHP_SAPI || 'gophp-engine' === PHP_SAPI) {
   ini_set('html_errors', 0);
   define('STDIN', fopen('php://stdin', 'rb'));
   define('STDOUT', fopen('php://stdout', 'wb'));
thekid commented 6 years ago

OK, here's a first running version:

package main

import (
    "fmt"
    "os"
    "io"
    php "github.com/deuill/go-php"
)

type Log struct {
    Writer io.Writer
}

func (l Log) Write(p []byte) (n int, err error) {
    os.Stderr.WriteString("> ")
    n, err = l.Writer.Write(p)
    os.Stderr.WriteString("\n")
    return
}

func main() {
    engine, _ := php.New()
    context, _ := engine.NewContext()

    context.Bind("argv", []string { "", "xp.runtime.Version" })
    context.Output = os.Stdout
    context.Log = &Log{os.Stderr}

    val, err := context.Eval(`
        set_include_path('.:/mnt/c/Tools/cygwin/home/friebe/devel/xp/core::.');
        ini_set('date.timezone', 'Europe/Berlin');

        try {
            include 'class-main.php';
        } catch (\Throwable $e) {
            echo $e;
        }
    `)
    if err != nil {
        fmt.Printf("Could not execute entry point: %v", err)
        os.Exit(1)
    }

    fmt.Printf("%+v", val.Interface())

    var exit int
    switch val.Kind() {
    case php.Long:
        exit = int(val.Int())
    case php.String:
        fmt.Println(val.String())
    }

    engine.Destroy()
    os.Exit(exit)
}

image

thekid commented 6 years ago

Wow, an exit() call from PHP shuts down the go binary with it! After another patch to the entry point script, this goes away; but it really needs to be handled inside Go code:

--- class-main.php      2018-10-01 21:32:43.574316300 +0200
+++ /usr/bin/class-main.php     2018-09-06 20:08:05.000000000 +0200
@@ -371,8 +371,8 @@
 }

 try {
-  return $class::main(array_slice($argv, 1));
+  exit($class::main(array_slice($argv, 1)));
 } catch (\lang\SystemExit $e) {
   if ($message= $e->getMessage()) echo $message, "\n";
-  return $e->getCode();
+  exit($e->getCode());
 }
thekid commented 6 years ago

Wow, an exit() call from PHP shuts down the go binary with it!

Addressed with https://github.com/deuill/go-php/pull/59 - now giving us:

package main

import (
    "fmt"
    "os"
    "io"
    php "github.com/deuill/go-php"
)

type Log struct {
    Writer io.Writer
}

func (l Log) Write(p []byte) (n int, err error) {
    os.Stderr.WriteString("> ")
    n, err = l.Writer.Write(p)
    os.Stderr.WriteString("\n")
    return
}

func main() {
    engine, err := php.New()
    if err != nil {
        fmt.Printf("Could not create a new engine: %v", err)
        os.Exit(1)
    }
    defer engine.Destroy()

    context, err := engine.NewContext()
    if err != nil {
        fmt.Printf("Could not create a new context: %v", err)
        os.Exit(1)
    }
    defer context.Destroy()

    context.Bind("argv", os.Args)
    context.Output = os.Stdout
    context.Log = &Log{os.Stderr}

    _, err = context.Eval(`
        set_include_path('.:/mnt/c/Tools/cygwin/home/friebe/devel/xp/core::.');
        ini_set('date.timezone', 'Europe/Berlin');

        try {
            include 'class-main.php';
        } catch (\Throwable $e) {
            echo $e;
            exit(255);
        }
    `)

    if err != nil {
        if exit, ok := err.(*php.ExitError); ok {
            os.Exit(exit.Status)
        }

        fmt.Printf("Could not execute entry point: %v", err)
        os.Exit(1)
    }
}
thekid commented 6 years ago

With https://github.com/deuill/go-php/pull/61, we can also move the set_include_path and ini_set calls out of the eval'ed code and inside go setters:

context.Ini("include_path", ".:/mnt/c/Tools/cygwin/home/friebe/devel/xp/core::.")
context.Ini("date.timezone", "Europe/Berlin")

Now the only thing missing would be the installation of an uncaught exception handler for errors from class-main.php (see https://github.com/deuill/go-php/issues/31)

thekid commented 6 years ago

image

thekid commented 6 years ago

One of the things we could do is expose functionality such as https://github.com/fsnotify/fsnotify to PHP by the following:

type Watcher struct {
    backing *notify.Watcher
}

func NewWatcher(args []interface{}) interface{} {
    backing, err := notify.NewWatcher()
    if err != nil {
        return nil
    }
    return &Watcher{backing}
}

func (w *Watcher) Add(path string) error {

    // Add directory
    if err := w.backing.Add(path); err != nil {
        return err
    }

    // Add subdirectories
    return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if info.IsDir() {
            name := info.Name()
            if name != "." && name != ".." {
                if err := w.backing.Add(path); err != nil {
                    return err
                }
            }
        }
        return nil
    })
}

func (w *Watcher) Select() (interface{}, string) {
    select {
    case event, ok := <-w.backing.Events:
        if !ok {
            return nil, "Closed"
        }
        return event.Op.String(), event.Name
    case err := <-w.backing.Errors:
        return nil, err.Error()
    }
}

func (w *Watcher) Close() {
    w.backing.Close()
}

// Later on;
engine.Define("Watcher", NewWatcher)
$ go build run.go && ./run xp.runtime.Evaluate \
  '$w= new Watcher(); $w->Add("."); var_dump($w, $w->Select()); $w->Close();'
object(Watcher)#19 (0) {
}
array(2) {
  [0]=>
  string(5) "WRITE"
  [1]=>
  string(8) "./xp.ini"
}