From e7b3cfe2f97195be5baff4704d91d8134b95c3d7 Mon Sep 17 00:00:00 2001
From: Gavin Hayes <gavin@dylibso.com>
Date: Wed, 27 Nov 2024 16:09:25 -0500
Subject: [PATCH] feat: compiled plugin

---
 Extism/lib/Extism.pm                |  1 +
 Extism/lib/Extism/CompiledPlugin.pm | 58 +++++++++++++++++++++++++++++
 Extism/lib/Extism/Plugin.pm         | 42 ++++++++-------------
 Extism/lib/Extism/XS.pm             |  3 ++
 Extism/lib/Extism/XS.xs             | 30 +++++++++++++++
 Extism/t/02-extism.t                | 19 +++++++++-
 6 files changed, 126 insertions(+), 27 deletions(-)
 create mode 100644 Extism/lib/Extism/CompiledPlugin.pm

diff --git a/Extism/lib/Extism.pm b/Extism/lib/Extism.pm
index eb3aecb..143f233 100644
--- a/Extism/lib/Extism.pm
+++ b/Extism/lib/Extism.pm
@@ -4,6 +4,7 @@ use 5.016;
 use strict;
 use warnings;
 use Extism::XS qw(version log_file);
+use Extism::CompiledPlugin;
 use Extism::Plugin;
 use Extism::Function ':all';
 use Exporter 'import';
diff --git a/Extism/lib/Extism/CompiledPlugin.pm b/Extism/lib/Extism/CompiledPlugin.pm
new file mode 100644
index 0000000..5ce4114
--- /dev/null
+++ b/Extism/lib/Extism/CompiledPlugin.pm
@@ -0,0 +1,58 @@
+package Extism::CompiledPlugin;
+
+use 5.016;
+use strict;
+use warnings;
+use Carp qw(croak);
+use Extism::XS qw(
+    compiled_plugin_new
+    compiled_plugin_free
+);
+use Exporter 'import';
+use Data::Dumper qw(Dumper);
+use Devel::Peek qw(Dump);
+use version 0.77;
+our $VERSION = qv(v0.2.0);
+
+our @EXPORT_OK = qw(BuildPluginNewParams);
+
+sub BuildPluginNewParams {
+    my ($wasm, $opt) = @_;
+    my $functions = $opt->{functions} // [];
+    my @rawfunctions = map {$$_} @{$functions};
+    my %p = (
+        wasm => $wasm,
+        _functions_array => pack('Q*', @rawfunctions)
+    );
+    $p{functions} = unpack('Q', pack('P', $p{_functions_array}));
+    $p{n_functions} = scalar(@rawfunctions);
+    $p{wasi} = $opt->{wasi} // 0;
+    $p{fuel_limit} = $opt->{fuel_limit};
+    $p{errptr} = "\x00" x 8;
+    $p{errmsg} = unpack('Q', pack('P', $p{errptr}));
+    \%p
+}
+
+sub new {
+    my ($name, $wasm, $options) = @_;
+    my %opt = %{$options // {}};
+    if (defined $opt{fuel_limit}) {
+        croak "No way to set fuel for CompiledPlugins yet";
+    }
+    my $p = BuildPluginNewParams($wasm, \%opt);
+    my $compiled = compiled_plugin_new($p->{wasm}, length($p->{wasm}), $p->{functions}, $p->{n_functions}, $p->{wasi}, $p->{errmsg});
+    my %savedoptions;
+    if ($opt{allow_http_response_headers}) {
+        $savedoptions{allow_http_response_headers} = $opt{allow_http_response_headers};
+    }
+    my %obj = ( compiled => $compiled, options => \%savedoptions);
+    bless \%obj, $name
+}
+
+sub DESTROY {
+    my ($self) = @_;
+    $self->{compiled} or return;
+    compiled_plugin_free($self->{compiled});
+}
+
+1;
\ No newline at end of file
diff --git a/Extism/lib/Extism/Plugin.pm b/Extism/lib/Extism/Plugin.pm
index 1623959..d17389e 100644
--- a/Extism/lib/Extism/Plugin.pm
+++ b/Extism/lib/Extism/Plugin.pm
@@ -7,6 +7,7 @@ use Carp qw(croak);
 use Extism::XS qw(
     plugin_new
     plugin_new_with_fuel_limit
+    plugin_new_from_compiled
     plugin_allow_http_response_headers
     plugin_new_error_free
     plugin_call
@@ -22,6 +23,7 @@ use Extism::XS qw(
     );
 use Extism::Plugin::CallException;
 use Extism::Plugin::CancelHandle;
+use Extism::CompiledPlugin qw(BuildPluginNewParams);
 use Data::Dumper qw(Dumper);
 use Devel::Peek qw(Dump);
 use JSON::PP qw(encode_json);
@@ -31,38 +33,26 @@ our $VERSION = qv(v0.2.0);
 
 sub new {
     my ($name, $wasm, $options) = @_;
-    my $functions = [];
-    my $with_wasi = 0;
-    my $fuel_limit;
-    my $allow_http_response_headers;
-    if ($options) {
-        if (exists $options->{functions}) {
-            $functions = $options->{functions};
-        }
-        if (exists $options->{wasi}) {
-            $with_wasi = $options->{wasi};
-        }
-        if (exists $options->{fuel_limit}) {
-            $fuel_limit = $options->{fuel_limit};
-        }
-        if (exists $options->{allow_http_response_headers}) {
-            $allow_http_response_headers = $options->{allow_http_response_headers};
-        }
+    my ($plugin, $opt, $errptr);
+    if (!ref($wasm) || !$wasm->isa('Extism::CompiledPlugin')) {
+        $opt = defined $options ? {%$options} : {};
+        my $p = BuildPluginNewParams($wasm, $opt);
+        $plugin = ! defined $p->{fuel_limit}
+            ? plugin_new($p->{wasm}, length($p->{wasm}), $p->{functions}, $p->{n_functions}, $p->{wasi}, $p->{errmsg})
+            : plugin_new_with_fuel_limit($p->{wasm}, length($p->{wasm}), $p->{functions}, $p->{n_functions}, $p->{wasi}, $p->{fuel_limit}, $p->{errmsg});
+        $errptr = $p->{errptr};
+    } else {
+        $opt = $wasm->{options};
+        $errptr = "\x00" x 8;
+        my $errmsg = unpack('Q', pack('P', $errptr));
+        $plugin = plugin_new_from_compiled($wasm->{compiled}, $errmsg);
     }
-    my $errptr = "\x00" x 8;
-    my $errptrptr = unpack('Q', pack('P', $errptr));
-    my @rawfunctions = map {$$_} @{$functions};
-    my $functionsarray = pack('Q*', @rawfunctions);
-    my $functionsptr = unpack('Q', pack('P', $functionsarray));
-    my $plugin = ! defined $fuel_limit
-    ? plugin_new($wasm, length($wasm), $functionsptr, scalar(@rawfunctions), $with_wasi, $errptrptr)
-    : plugin_new_with_fuel_limit($wasm, length($wasm), $functionsptr, scalar(@rawfunctions), $with_wasi, $fuel_limit, $errptrptr);
     if (! $plugin) {
         my $errmsg = unpack('p', $errptr);
         plugin_new_error_free(unpack('Q', $errptr));
         croak $errmsg;
     }
-    if ($allow_http_response_headers) {
+    if ($opt->{allow_http_response_headers}) {
         plugin_allow_http_response_headers($plugin);
     }
     bless \$plugin, $name
diff --git a/Extism/lib/Extism/XS.pm b/Extism/lib/Extism/XS.pm
index 8769555..e2e8778 100644
--- a/Extism/lib/Extism/XS.pm
+++ b/Extism/lib/Extism/XS.pm
@@ -39,6 +39,9 @@ our @EXPORT_OK = qw(
     log_file
     log_custom
     log_drain
+    compiled_plugin_new
+    compiled_plugin_free
+    plugin_new_from_compiled
     CopyToPtr
 );
 
diff --git a/Extism/lib/Extism/XS.xs b/Extism/lib/Extism/XS.xs
index fff9932..d7beb4c 100644
--- a/Extism/lib/Extism/XS.xs
+++ b/Extism/lib/Extism/XS.xs
@@ -81,6 +81,8 @@ ExtismMemoryHandle T_UV
 const ExtismCancelHandle * T_PTR
 PV T_PV
 uint64_t T_UV
+ExtismCompiledPlugin * T_PTR
+const ExtismCompiledPlugin * T_PTR
 HERE
 
 const char *
@@ -90,6 +92,34 @@ version()
     OUTPUT:
         RETVAL
 
+ExtismCompiledPlugin *
+compiled_plugin_new(wasm, wasm_size, functions, n_functions, with_wasi, errmsg)
+    const uint8_t *wasm
+    ExtismSize wasm_size
+    const ExtismFunction **functions
+    ExtismSize n_functions
+    bool with_wasi
+    char **errmsg
+    CODE:
+        RETVAL = extism_compiled_plugin_new(wasm, wasm_size, functions, n_functions, with_wasi, errmsg);
+    OUTPUT:
+        RETVAL
+
+void
+compiled_plugin_free(compiled_plugin)
+    ExtismCompiledPlugin *compiled_plugin
+    CODE:
+        extism_compiled_plugin_free(compiled_plugin);
+
+ExtismPlugin *
+plugin_new_from_compiled(compiled_plugin, errmsg)
+    const ExtismCompiledPlugin *compiled_plugin
+    char **errmsg
+    CODE:
+        RETVAL = extism_plugin_new_from_compiled(compiled_plugin, errmsg);
+    OUTPUT:
+        RETVAL
+
 ExtismPlugin *
 plugin_new(wasm, wasm_size, functions, n_functions, with_wasi, errmsg)
     const uint8_t *wasm
diff --git a/Extism/t/02-extism.t b/Extism/t/02-extism.t
index 61c0a81..262f4e0 100644
--- a/Extism/t/02-extism.t
+++ b/Extism/t/02-extism.t
@@ -7,7 +7,7 @@ use Extism ':all';
 use JSON::PP qw(encode_json decode_json);
 use File::Temp qw(tempfile);
 use Devel::Peek qw(Dump);
-plan tests => 51;
+plan tests => 54;
 
 # ...
 ok(Extism::version());
@@ -261,3 +261,20 @@ ok($@->{message});
     my $plugin = Extism::Plugin->new($wasm, {wasi => 1, allow_http_response_headers => 1});
     ok($plugin);
 }
+
+# compiled plugin
+{
+    my $compiled = Extism::CompiledPlugin->new($wasm, {wasi => 1});
+    for (1..2) {
+        my $plugin = Extism::Plugin->new($compiled);
+        my $output = $plugin->call('count_vowels', "this is a test");
+        my $outputhash = decode_json($output);
+        ok($outputhash->{count} == 4);
+    }
+}
+{
+    eval {
+        Extism::CompiledPlugin->new($wasm, {wasi => 1, fuel_limit => 20});
+    };
+    ok($@);
+}