diff --git a/CHANGELOG.md b/CHANGELOG.md
index 950805b..f2d86ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to `:vips` will be documented in this file.
 ## master
 
 - improve FFI startup [West14]
+- revise example use of composer [jcupitt]
 
 ## 2.1.1 - 2022-11-13
 
diff --git a/examples/addconst.php b/examples/addconst.php
index 971d757..e9d330e 100755
--- a/examples/addconst.php
+++ b/examples/addconst.php
@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 
 use Jcupitt\Vips;
 
diff --git a/examples/bench.php b/examples/bench.php
index bd41e8c..e262ac7 100755
--- a/examples/bench.php
+++ b/examples/bench.php
@@ -1,10 +1,15 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 
 use Jcupitt\Vips;
 
+if (count($argv) != 3) {
+    echo("usage: ./bench.php input-image output-image\n");
+    exit(1);
+}
+
 $im = Vips\Image::newFromFile($argv[1], ['access' => Vips\Access::SEQUENTIAL]);
 
 $im = $im->crop(100, 100, $im->width - 200, $im->height - 200);
diff --git a/examples/class.php b/examples/class.php
index c98b72a..47c5b3b 100755
--- a/examples/class.php
+++ b/examples/class.php
@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 
 use Jcupitt\Vips;
 
diff --git a/examples/composer.json b/examples/composer.json
deleted file mode 100644
index 46f0eba..0000000
--- a/examples/composer.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "require": {
-        "jcupitt/vips": "2.1.1"
-    }
-}
diff --git a/examples/sig.php b/examples/sig.php
index 2660867..8b295c7 100755
--- a/examples/sig.php
+++ b/examples/sig.php
@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 
 use Jcupitt\Vips;
 
@@ -141,6 +141,11 @@ function sigLAB(Vips\Image $image, bool $sharpen, float $midpoint, float $contra
     ]);
 }
 
+if (count($argv) != 3) {
+    echo("usage: ./sig.php input-image output-image\n");
+    exit(1);
+}
+
 $im = Vips\Image::newFromFile($argv[1], ['access' => Vips\Access::SEQUENTIAL]);
 
 /**
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
diff --git a/examples/vips-magick.php b/examples/vips-magick.php
index 5d355c6..42e6691 100755
--- a/examples/vips-magick.php
+++ b/examples/vips-magick.php
@@ -1,10 +1,15 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 
 use Jcupitt\Vips;
 
+if (count($argv) != 3) {
+    echo("usage: ./vips-magick.php input-image output-image\n");
+    exit(1);
+}
+
 /* Load an image with libvips, render to a large memory buffer, wrap a imagick
  * image around that, then use imagick to save as another file.
  */
diff --git a/examples/watermark-image.php b/examples/watermark-image.php
index 4e1ee83..c4986c5 100755
--- a/examples/watermark-image.php
+++ b/examples/watermark-image.php
@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 use Jcupitt\Vips;
 
 #Vips\Config::setLogger(new Vips\DebugLogger());
@@ -41,4 +41,4 @@
 $image = null;
 $watermark = null;
 
-Vips\Config::shutDown();
+Vips\FFI::shutDown();
diff --git a/examples/watermark-text.php b/examples/watermark-text.php
index 9b57f26..8a60d6c 100755
--- a/examples/watermark-text.php
+++ b/examples/watermark-text.php
@@ -1,7 +1,7 @@
 #!/usr/bin/env php
 <?php
 
-require __DIR__ . '/vendor/autoload.php';
+require dirname(__DIR__) . '/vendor/autoload.php';
 use Jcupitt\Vips;
 
 #Vips\Config::setLogger(new Vips\DebugLogger());
diff --git a/src/FFI.php b/src/FFI.php
index c701bf9..4ff4a87 100644
--- a/src/FFI.php
+++ b/src/FFI.php
@@ -422,12 +422,16 @@ private static function init(): void
 void g_object_get_property (GObject* object,
     const char* name, GValue* value);
 
-typedef void (*GCallback)(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,
-    void* data,
+    gpointer data,
     GClosureNotify destroy_data,
     int connect_flags);
 
@@ -490,7 +494,6 @@ private static function init(): void
         # the whole libvips API, mostly adapted from pyvips
         $vips_decls = $typedefs . <<<EOS
 typedef struct _VipsImage VipsImage;
-typedef struct _VipsProgress VipsProgress;
 
 // Defined in GObject, just typedef to void
 typedef void GParamSpec;
@@ -803,11 +806,16 @@ private static function init(): void
         self::$ctypes = [
             "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*"),
             "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 +824,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..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.
      *
@@ -60,6 +65,16 @@ 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 = [];
+
     /**
      * Wrap a GObject around an underlying vips resource. The GObject takes
      * ownership of the pointer and will unref it on finalize.
@@ -99,19 +114,107 @@ public function unref(): void
 
     /**
      * Connect to a signal on this object.
-     * The callback will be triggered every time this signal is issued on this instance.
+     *
+     * The callback will be triggered every time this signal is issued on this 
+     * instance.
+     *
      * @throws Exception
      */
     public function signalConnect(string $name, Closure $callback): void
     {
-        $marshaler = self::getMarshaler($name, $callback);
-        if ($marshaler === null) {
-            throw new Exception("unsupported signal $name");
+        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, 
+                                                    $c_callback[0],
+                                                    $handle, 
+                                                    null,
+                                                    0);
+        if ($id === 0) {
+            throw new Exception("unable to connect signal $name");
+        }
+
+        echo "signalConnect: done\n";
+    }
+
+    /* 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.
+     */
+    private static function getMarshal(string $name) : CData
+    {
+        switch ($name) {
+            case 'preeval':
+            case 'eval':
+            case 'posteval':
+                $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($imagePointer);
+
+                    // FIXME ... maybe wrap VipsProgress as a php class?
+                    $progress = \FFI::cast(FFI::ctypes("VipsProgress"), 
+                                           $progressPointer);
+
+                    $callback = self::fromHandle($handle);
+
+                    $callback($image, $progress);
+                };
+
+                $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]);
+                }
         }
 
-        $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);
+        throw new Exception("unsupported signal $name");
     }
 
     private static function getMarshaler(string $name, Closure $callback): ?Closure
@@ -144,34 +247,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 (
@@ -258,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];
+    }
 }
 
 /*
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.
      *