From 8ac05a4c20f98d7d60ca704baf2ff61a1a8d3989 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Sun, 5 Mar 2023 13:01:28 +0000 Subject: [PATCH 1/5] set exe bit for streaming examples --- examples/streaming-bench.php | 0 examples/streaming.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/streaming-bench.php mode change 100644 => 100755 examples/streaming.php diff --git a/examples/streaming-bench.php b/examples/streaming-bench.php old mode 100644 new mode 100755 diff --git a/examples/streaming.php b/examples/streaming.php old mode 100644 new mode 100755 From 1796d420369213f68a47689de9e946c439c8fe32 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Sat, 8 Apr 2023 14:03:39 +0100 Subject: [PATCH 2/5] experiment with direct callbacks --- src/FFI.php | 7 ++-- src/GObject.php | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Image.php | 14 ++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/FFI.php b/src/FFI.php index c701bf9..1241255 100644 --- a/src/FFI.php +++ b/src/FFI.php @@ -422,12 +422,12 @@ private static function init(): void void g_object_get_property (GObject* object, const char* name, GValue* value); -typedef void (*GCallback)(void); +typedef void (*GCallback)(void*, void*, void*); typedef void (*GClosureNotify)(void* data, struct _GClosure *); long g_signal_connect_data (GObject* object, const char* detailed_signal, GCallback c_handler, - void* data, + gpointer data, GClosureNotify destroy_data, int connect_flags); @@ -490,7 +490,6 @@ private static function init(): void # the whole libvips API, mostly adapted from pyvips $vips_decls = $typedefs . << self::$vips->type("VipsOperation*"), "VipsImage" => self::$vips->type("VipsImage*"), "VipsInterpolate" => self::$vips->type("VipsInterpolate*"), + "VipsProgress" => self::$vips->type("VipsProgress*"), "VipsConnection" => self::$vips->type("VipsConnection*"), "VipsSource" => self::$vips->type("VipsSource*"), "VipsSourceCustom" => self::$vips->type("VipsSourceCustom*"), @@ -816,6 +816,7 @@ private static function init(): void ]; self::$gtypes = [ + "gpointer" => self::$gobject->g_type_from_name("gpointer"), "gboolean" => self::$gobject->g_type_from_name("gboolean"), "gint" => self::$gobject->g_type_from_name("gint"), "gint64" => self::$gobject->g_type_from_name("gint64"), diff --git a/src/GObject.php b/src/GObject.php index 8a747fc..85a773b 100644 --- a/src/GObject.php +++ b/src/GObject.php @@ -60,6 +60,25 @@ abstract class GObject */ private CData $pointer; + /** + * Upstream objects we must keep alive. + * + * A set of references to other php objects which this object refers to + * via ffi, ie. references which the php GC cannot find automatically. + * + * @internal + */ + private array $_references = []; + + /** + * Marshalling functions for signal_connect. + * + * We build these once on first use, see get_marshal. + * + * @internal + */ + private static ?array $_marshal = null; + /** * Wrap a GObject around an underlying vips resource. The GObject takes * ownership of the pointer and will unref it on finalize. @@ -97,6 +116,72 @@ public function unref(): void FFI::gobject()->g_object_unref($this->pointer); } + private static function get_marshal(string $name): ?Closure + { + if (self::$_marshal == null) { + $progress = function (CData $image_pointer, + CData $progress_pointer, + ?CData $user) { + $image = new Image($image_pointer); + // Image() will unref on gc, so we must ref + FFI::gobject()->g_object_ref($image_pointer); + $progress = \FFI::cast(FFI::ctypes("VipsProgress"), + $progress_pointer); + Closure $callback = $user; + + echo "marshal progress:\n"; + echo " image = $image\n"; + echo " progress->run = $progress->run\n"; + echo " progress->eta = $progress->eta\n"; + echo " progress->tpels = $progress->tpels\n"; + echo " progress->npels = $progress->npels\n"; + echo " progress->percent = $progress->percent\n"; + echo " data = "; + print_r($user); + echo "\n"; + }; + + self::$_marshal = [ + "preeval" => $progress, + "eval" => $progress, + "posteval" => $progress, + ]; + } + + if (!array_key_exists($name, self::$_marshal)) { + throw new Exception("unsupported signal $name"); + } + + return self::$_marshal[$name]; + } + + public function signal_connect2(string $name, $callback): void + { + $marshal = self::get_marshal($name); + if ($marshal === null) { + throw new Exception("unsupported signal $name"); + } + + // php-ffi is expecting a void* for the user pointer + //$user = \FFI::cast("void*", $callback); + + $id = FFI::gobject()->g_signal_connect_data($this->pointer, + $name, + $marshal, + //$user, + null, + null, + 0); + if ($id === 0) { + throw new Exception("unable to connect signal $name"); + } + + // the closure we were passed may well be dynamically created and + // contain objects which must not be GCd ... we must hold a reference + // to it to keep it alive + $this->_references[] = $callback; + } + /** * Connect to a signal on this object. * The callback will be triggered every time this signal is issued on this instance. diff --git a/src/Image.php b/src/Image.php index a7b53e1..e68ad04 100644 --- a/src/Image.php +++ b/src/Image.php @@ -1296,6 +1296,20 @@ public function remove(string $name): void } } + /** + * Enable progress reporting on an image. + * + * When progress reporting is enabled, evaluation of the most downstream + * image from this image will report progress using the ::preeval, ::eval, + * and ::posteval signals. + * + * @param bool $progress True to enable progress reporting + */ + public function setProgress($progress): void + { + FFI::vips()->vips_image_set_progress($this->pointer, $progress); + } + /** * Makes a string-ified version of the Image. * From 30174d060eed5dc6d41dd05f516112a80245c089 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Sat, 8 Apr 2023 14:05:46 +0100 Subject: [PATCH 3/5] comment out some dbg code --- src/GObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GObject.php b/src/GObject.php index 85a773b..edd0f76 100644 --- a/src/GObject.php +++ b/src/GObject.php @@ -127,7 +127,7 @@ private static function get_marshal(string $name): ?Closure FFI::gobject()->g_object_ref($image_pointer); $progress = \FFI::cast(FFI::ctypes("VipsProgress"), $progress_pointer); - Closure $callback = $user; + // Closure $callback = $user; echo "marshal progress:\n"; echo " image = $image\n"; From c3109d434d754769ad9471305f2057cd4d8535ce Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Sat, 8 Apr 2023 16:22:11 +0100 Subject: [PATCH 4/5] use a closure to hold the callback sigh --- src/FFI.php | 1 + src/GObject.php | 144 +++++++++++++++--------------------------------- 2 files changed, 44 insertions(+), 101 deletions(-) diff --git a/src/FFI.php b/src/FFI.php index 1241255..04a81b2 100644 --- a/src/FFI.php +++ b/src/FFI.php @@ -802,6 +802,7 @@ private static function init(): void self::$ctypes = [ "GObject" => self::$gobject->type("GObject*"), "GClosure" => self::$gobject->type("GClosure"), + "GCallback" => self::$gobject->type("GCallback"), "GParamSpec" => self::$gobject->type("GParamSpec*"), "VipsObject" => self::$vips->type("VipsObject*"), "VipsOperation" => self::$vips->type("VipsOperation*"), diff --git a/src/GObject.php b/src/GObject.php index edd0f76..208dd9c 100644 --- a/src/GObject.php +++ b/src/GObject.php @@ -70,15 +70,6 @@ abstract class GObject */ private array $_references = []; - /** - * Marshalling functions for signal_connect. - * - * We build these once on first use, see get_marshal. - * - * @internal - */ - private static ?array $_marshal = null; - /** * Wrap a GObject around an underlying vips resource. The GObject takes * ownership of the pointer and will unref it on finalize. @@ -116,87 +107,65 @@ public function unref(): void FFI::gobject()->g_object_unref($this->pointer); } - private static function get_marshal(string $name): ?Closure - { - if (self::$_marshal == null) { - $progress = function (CData $image_pointer, - CData $progress_pointer, - ?CData $user) { - $image = new Image($image_pointer); - // Image() will unref on gc, so we must ref - FFI::gobject()->g_object_ref($image_pointer); - $progress = \FFI::cast(FFI::ctypes("VipsProgress"), - $progress_pointer); - // Closure $callback = $user; - - echo "marshal progress:\n"; - echo " image = $image\n"; - echo " progress->run = $progress->run\n"; - echo " progress->eta = $progress->eta\n"; - echo " progress->tpels = $progress->tpels\n"; - echo " progress->npels = $progress->npels\n"; - echo " progress->percent = $progress->percent\n"; - echo " data = "; - print_r($user); - echo "\n"; - }; - - self::$_marshal = [ - "preeval" => $progress, - "eval" => $progress, - "posteval" => $progress, - ]; - } - - if (!array_key_exists($name, self::$_marshal)) { - throw new Exception("unsupported signal $name"); - } - - return self::$_marshal[$name]; - } - - public function signal_connect2(string $name, $callback): void + /** + * Connect to a signal on this object. + * + * The callback will be triggered every time this signal is issued on this + * instance. + * + * @throws Exception + */ + public function signalConnect(string $name, Closure $callback): void { - $marshal = self::get_marshal($name); - if ($marshal === null) { - throw new Exception("unsupported signal $name"); - } - - // php-ffi is expecting a void* for the user pointer - //$user = \FFI::cast("void*", $callback); + $marshal = self::buildMarshal($name, $callback); $id = FFI::gobject()->g_signal_connect_data($this->pointer, $name, $marshal, - //$user, - null, + null, null, 0); if ($id === 0) { throw new Exception("unable to connect signal $name"); } - // the closure we were passed may well be dynamically created and - // contain objects which must not be GCd ... we must hold a reference - // to it to keep it alive - $this->_references[] = $callback; + // the marshaller must not be GCed + // FIXME ... _references must be copied to all child objects in call() + $this->_references[] = $marshal; } - /** - * Connect to a signal on this object. - * The callback will be triggered every time this signal is issued on this instance. - * @throws Exception + /* Ideally, we'd use the "user" pointer of signal_connect to hold the + * callback closure, but unfortunately php-ffi has no way (I think) to + * turn a C pointer to function back into a php function, so we have to + * build a custom closure for every signal connect with the callback + * embedded within it. */ - public function signalConnect(string $name, Closure $callback): void + private static function buildMarshal(string $name, Closure $callback): + Closure { - $marshaler = self::getMarshaler($name, $callback); - if ($marshaler === null) { - throw new Exception("unsupported signal $name"); - } + switch ($name) { + case 'preeval': + case 'eval': + case 'posteval': + return static function ( + CData $image_pointer, + CData $progress_pointer, + ?CData $user + ) use (&$callback): void { + $image = new Image($image_pointer); + // Image() will unref on gc, so we must ref + FFI::gobject()->g_object_ref($image_pointer); + + // FIXME ... maybe wrap VipsProgress as a php class? + $progress = \FFI::cast(FFI::ctypes("VipsProgress"), + $progress_pointer); + + $callback($image, $progress); + }; - $gc = FFI::gobject()->g_closure_new_simple(\FFI::sizeof(FFI::ctypes('GClosure')), null); - $gc->marshal = $marshaler; - FFI::gobject()->g_signal_connect_closure($this->pointer, $name, $gc, 0); + default: + throw new Exception("unsupported signal $name"); + } } private static function getMarshaler(string $name, Closure $callback): ?Closure @@ -229,34 +198,7 @@ private static function getMarshaler(string $name, Closure $callback): ?Closure ); $callback($image, $pr); }; - case 'read': - if (FFI::atLeast(8, 9)) { - return static function ( - CData $gClosure, - CData $returnValue, - int $numberOfParams, - CData $params, - CData $hint, - ?CData $data - ) use (&$callback): void { - assert($numberOfParams === 4); - /* - * Signature: gint64(VipsSourceCustom* source, void* buffer, gint64 length, void* handle) - */ - $bufferLength = (int)FFI::gobject()->g_value_get_int64(\FFI::addr($params[2])); - $returnBuffer = $callback($bufferLength); - $returnBufferLength = 0; - if ($returnBuffer !== null) { - $returnBufferLength = strlen($returnBuffer); - $bufferPointer = FFI::gobject()->g_value_get_pointer(\FFI::addr($params[1])); - \FFI::memcpy($bufferPointer, $returnBuffer, $returnBufferLength); - } - FFI::gobject()->g_value_set_int64($returnValue, $returnBufferLength); - }; - } - - return null; case 'seek': if (FFI::atLeast(8, 9)) { return static function ( From 01da7e6524b667e6972001327db826e206214a24 Mon Sep 17 00:00:00 2001 From: John Cupitt Date: Tue, 11 Apr 2023 14:09:58 +0100 Subject: [PATCH 5/5] more fiddling --- src/FFI.php | 9 +++- src/GObject.php | 107 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/FFI.php b/src/FFI.php index 04a81b2..4ff4a87 100644 --- a/src/FFI.php +++ b/src/FFI.php @@ -422,8 +422,12 @@ private static function init(): void void g_object_get_property (GObject* object, const char* name, GValue* value); -typedef void (*GCallback)(void*, void*, void*); typedef void (*GClosureNotify)(void* data, struct _GClosure *); + +typedef void (*GCallback)(void); +typedef void (*GCallback_progress)(void*,void*,void*); +typedef gint64 (*GCallback_read)(void*,void*,gint64,void*); + long g_signal_connect_data (GObject* object, const char* detailed_signal, GCallback c_handler, @@ -803,6 +807,9 @@ private static function init(): void "GObject" => self::$gobject->type("GObject*"), "GClosure" => self::$gobject->type("GClosure"), "GCallback" => self::$gobject->type("GCallback"), + "GCallback_read" => self::$gobject->type("GCallback_read"), + "GCallback_progress" => self::$gobject->type("GCallback_progress"), + "GType" => self::$gobject->type("GType"), "GParamSpec" => self::$gobject->type("GParamSpec*"), "VipsObject" => self::$vips->type("VipsObject*"), "VipsOperation" => self::$vips->type("VipsOperation*"), diff --git a/src/GObject.php b/src/GObject.php index 208dd9c..c3c2301 100644 --- a/src/GObject.php +++ b/src/GObject.php @@ -53,6 +53,11 @@ */ abstract class GObject { + /** + */ + private static array $handleTable = []; + private static int $nextIndex = 0; + /** * A pointer to the underlying GObject. * @@ -117,21 +122,26 @@ public function unref(): void */ public function signalConnect(string $name, Closure $callback): void { - $marshal = self::buildMarshal($name, $callback); + echo "signalConnect:\n"; + + $marshal = self::getMarshal($name); + $handle = self::getHandle($callback); + $sig = \FFI::arrayType(FFI::ctypes("GCallback"), [1]); + $c_callback = \FFI::new($sig); + $c_callback[0] = $marshal; + $id = FFI::gobject()->g_signal_connect_data($this->pointer, $name, - $marshal, - null, + $c_callback[0], + $handle, null, 0); if ($id === 0) { throw new Exception("unable to connect signal $name"); } - // the marshaller must not be GCed - // FIXME ... _references must be copied to all child objects in call() - $this->_references[] = $marshal; + echo "signalConnect: done\n"; } /* Ideally, we'd use the "user" pointer of signal_connect to hold the @@ -140,32 +150,71 @@ public function signalConnect(string $name, Closure $callback): void * build a custom closure for every signal connect with the callback * embedded within it. */ - private static function buildMarshal(string $name, Closure $callback): - Closure + private static function getMarshal(string $name) : CData { switch ($name) { case 'preeval': case 'eval': case 'posteval': - return static function ( - CData $image_pointer, - CData $progress_pointer, - ?CData $user - ) use (&$callback): void { - $image = new Image($image_pointer); + $marshal = static function ( + CData $imagePointer, + CData $progressPointer, + CData $handle) : void { + $image = new Image($imagePointer); // Image() will unref on gc, so we must ref - FFI::gobject()->g_object_ref($image_pointer); + FFI::gobject()->g_object_ref($imagePointer); // FIXME ... maybe wrap VipsProgress as a php class? $progress = \FFI::cast(FFI::ctypes("VipsProgress"), - $progress_pointer); + $progressPointer); + + $callback = self::fromHandle($handle); $callback($image, $progress); }; - default: - throw new Exception("unsupported signal $name"); + $sig = \FFI::arrayType(FFI::ctypes("GCallback_progress"), [1]); + $c_callback = \FFI::new($sig); + $c_callback[0] = $marshal; + + return \FFI::cast(FFI::ctypes("GCallback"), $c_callback[0]); + + case 'read': + if (FFI::atLeast(8, 9)) { + $marshal = function ( + CData $sourcePointer, + CData $bufferPointer, + int $bufferLength, + CData $handle) : int { + echo "hello from read marshal!\n"; + $callback = self::fromHandle($handle); + $result = 0; + + $returnBuffer = $callback($bufferLength); + if ($returnBuffer !== null) { + $result = strlen($returnBuffer); + \FFI::memcpy($bufferPointer, + $returnBuffer, + $result + ); + } + + return $result; + }; + + $sig = \FFI::arrayType(FFI::ctypes("GCallback_read"), [1]); + $c_callback = \FFI::new($sig); + $c_callback[0] = $marshal; + + echo "c_callback[0] = "; + print_r($c_callback[0]); + echo "\n"; + + return \FFI::cast(FFI::ctypes("GCallback"), $c_callback[0]); + } } + + throw new Exception("unsupported signal $name"); } private static function getMarshaler(string $name, Closure $callback): ?Closure @@ -285,6 +334,28 @@ private static function getMarshaler(string $name, Closure $callback): ?Closure return null; } } + + private static function getHandle($object) : CData + { + $index = self::$nextIndex; + self::$nextIndex += 1; + + self::$handleTable[$index] = $object; + + // hide the index inside a void* + $x = \FFI::new(FFI::ctypes("GType")); + $x->cdata = $index; + + return \FFI::cast("void*", $x); + } + + private static function fromHandle($handle) + { + // recover the index from a void* + $index = $handle->cdata; + + return self::$handleTable[$index]; + } } /*