Skip to content

Commit

Permalink
feat(proxy-wasm) host-managed property getters and setters
Browse files Browse the repository at this point in the history
This feature adds support for host-managed property getter and
setter functions.

These feature is implemented in three stages:

* `ngx_proxy_wasm_properties.c`: in our implementation for
  proxy-wasm properties, we now allow the host to specify a
  function which gets called whenever proxy-wasm receives a
  `set_property` or `get_property` call matching the host-defined
  namespace. Those getter and setter functions are then responsible
  to return and store the values, and are able to produce any
  desired side-effects in the host.

* `ngx_wasm_lua_ffi.c`: We then expose this functionality in our
  Lua FFI interface. This is a lightweight wrapper, which accepts
  a C function pointer directly, and associates this function
  to the request context object.

* `lib/resty/wasmx/proxy_wasm.lua`: Finally, we expose a Lua API
  for setting a Lua functions as setters and getters. The interface
  is such that the host Lua program should set one setter function
  and/or getter function, and then within that function properly
  dispatch the behavior based on the received key. Internally, these
  functions use the fact that LuaJIT is able to convert a Lua
  function into a C function pointer. Due to lifetime management,
  those generated C callbacks are a limited resource in the LuaJIT
  VM, so we produce a single pair of these and then have them
  call the user-defined Lua function, which remains as a normal
  Lua closure. (See http://luajit.org/ext_ffi_semantics.html for
  more details on LuaJIT FFI C callback managment.)

To ensure that the lifetime of strings produced by the host continue
valid by the time the getter callback finished running and the Wasm
engine reads the properties, getter/setter-produced values continue
to be stored in the rbtree for host properties that we already had
for plain `get_property`/`set_property`, introduced in da82087.

Since we need to store this data on the C side anyway, we also
add an extra feature as an optimization: the concept
of "const" host-properties. If the Lua callback returns a truthy
third-argument, that signals that that value should be flagged as
constant. Further calls to `get_property` during the same request
will get the property value directly from the ngx_wasm_module rbtree
"cache" without hitting the getter callback again. Test cases in the
test suite demonstrate this feature.

In this PR, the host namespace continues to be statically defined
at compile time via the NGX_WASM_HOST_PROPERTY_NAMESPACE define.
The regular FFI functions for `set_property` and `get_property`
introduced in 447ef13 also continue to work normally.
  • Loading branch information
hishamhm committed Oct 24, 2023
1 parent 21732b1 commit 04016dd
Show file tree
Hide file tree
Showing 9 changed files with 1,586 additions and 40 deletions.
4 changes: 4 additions & 0 deletions lib/resty/wasmx.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ ffi.cdef [[
ngx_str_t *config;
} ngx_wasm_filter_t;

typedef intptr_t ngx_int_t;
typedef unsigned char u_char;
typedef struct ngx_wavm_t ngx_wasm_vm_t;
typedef struct ngx_wasm_ops_plan_t ngx_wasm_plan_t;

typedef ngx_int_t (*ngx_wasm_host_prop_fn_t)(void *data, ngx_str_t *key,
ngx_str_t *value);

ngx_wasm_vm_t *ngx_wasm_ffi_main_vm();
]]

Expand Down
198 changes: 179 additions & 19 deletions lib/resty/wasmx/proxy_wasm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ local C = ffi.C
local ngx = ngx
local error = error
local type = type
local pcall = pcall
local ffi_gc = ffi.gc
local ffi_new = ffi.new
local ffi_str = ffi.string
local tostring = tostring
local subsystem = ngx.config.subsystem
local get_request = wasmx.get_request
local get_err_ptr = wasmx.get_err_ptr
Expand Down Expand Up @@ -38,23 +40,32 @@ local _M = {

if subsystem == "http" then
ffi.cdef [[
int ngx_http_wasm_ffi_plan_new(ngx_wasm_vm_t *vm,
ngx_wasm_filter_t *filters,
size_t n_filters,
ngx_wasm_plan_t **out,
u_char *err, size_t *errlen);
int ngx_http_wasm_ffi_plan_free(ngx_wasm_plan_t *plan);
int ngx_http_wasm_ffi_plan_load(ngx_wasm_plan_t *plan);
int ngx_http_wasm_ffi_plan_attach(ngx_http_request_t *r,
ngx_wasm_plan_t *plan,
unsigned int isolation);
int ngx_http_wasm_ffi_start(ngx_http_request_t *r);
int ngx_http_wasm_ffi_set_property(ngx_http_request_t *r,
ngx_str_t *key,
ngx_str_t *value);
int ngx_http_wasm_ffi_get_property(ngx_http_request_t *r,
ngx_str_t *key,
ngx_str_t *out);
ngx_int_t ngx_http_wasm_ffi_plan_new(ngx_wasm_vm_t *vm,
ngx_wasm_filter_t *filters,
size_t n_filters,
ngx_wasm_plan_t **out,
u_char *err, size_t *errlen);
ngx_int_t ngx_http_wasm_ffi_plan_free(ngx_wasm_plan_t *plan);
ngx_int_t ngx_http_wasm_ffi_plan_load(ngx_wasm_plan_t *plan);
ngx_int_t ngx_http_wasm_ffi_plan_attach(ngx_http_request_t *r,
ngx_wasm_plan_t *plan,
unsigned int isolation);
ngx_int_t ngx_http_wasm_ffi_start(ngx_http_request_t *r);
ngx_int_t ngx_http_wasm_ffi_set_property(ngx_http_request_t *r,
ngx_str_t *key,
ngx_str_t *value);
ngx_int_t ngx_http_wasm_ffi_get_property(ngx_http_request_t *r,
ngx_str_t *key,
ngx_str_t *out);
ngx_int_t ngx_http_wasm_ffi_set_host_property(ngx_http_request_t *r,
ngx_str_t *key,
ngx_str_t *value,
unsigned is_const,
unsigned retrieve);
ngx_int_t ngx_http_wasm_ffi_set_property_setter(ngx_http_request_t *r,
ngx_wasm_host_prop_fn_t fn);
ngx_int_t ngx_http_wasm_ffi_set_property_getter(ngx_http_request_t *r,
ngx_wasm_host_prop_fn_t fn);
]]

else
Expand All @@ -67,6 +78,86 @@ local ffi_ops_type = ffi.typeof("ngx_wasm_plan_t *")
local ffi_pops_type = ffi.typeof("ngx_wasm_plan_t *[1]")


local lua_prop_setter
local lua_prop_getter
local c_prop_setter
local c_prop_getter


do
-- LuaJIT C callbacks are a limited resource:
-- We can't create one of these on each request,
-- so we prepare these wrappers, which in turn call a Lua function.

local function store_c_value(r, ckey, cvalue, lvalue, is_const)
if lvalue ~= nil then
local value = tostring(lvalue)
cvalue.data = value
cvalue.len = #value

else
cvalue.data = nil
cvalue.len = 0
end

return C.ngx_http_wasm_ffi_set_host_property(r, ckey, cvalue,
is_const and 1 or 0,
1)
end

c_prop_setter = ffi.cast("ngx_wasm_host_prop_fn_t", function(r, ckey,
cvalue)
local lkey = ffi_str(ckey.data, ckey.len)
local lval
if cvalue.data ~= nil then
lval = ffi_str(cvalue.data, cvalue.len)
end

local pok, ok, lvalue, is_const = pcall(lua_prop_setter, lkey, lval)
if not pok then
ngx.log(ngx.ERR, "error setting property from Lua: ", ok)
return FFI_ERROR
end

if not ok then
local err = lvalue or "unknown error"
ngx.log(ngx.ERR, "error setting property: ", err)
return FFI_ERROR
end

return store_c_value(r, ckey, cvalue, lvalue, is_const)
end)


c_prop_getter = ffi.cast("ngx_wasm_host_prop_fn_t", function(r, ckey,
cvalue)
local lkey = ffi_str(ckey.data, ckey.len)

local pok, ok, lvalue, is_const = pcall(lua_prop_getter, lkey)
if not pok then
ngx.log(ngx.ERR, "error getting property from Lua: ", ok)
return FFI_ERROR
end

if not ok then
if lvalue then
ngx.log(ngx.ERR, "error setting property: ", lvalue)
return FFI_ERROR
end

return FFI_DECLINED
end

local rc = store_c_value(r, ckey, cvalue, lvalue, is_const)
if rc == FFI_OK and lvalue == nil then
return FFI_DECLINED
end

return rc
end)
end


function _M.new(filters)
if type(filters) ~= "table" then
error("filters must be a table", 2)
Expand Down Expand Up @@ -229,7 +320,7 @@ function _M.set_property(key, value)
error("key must be a string", 2)
end

if type(value) ~= "string" then
if value ~= nil and type(value) ~= "string" then
error("value must be a string", 2)
end

Expand All @@ -239,7 +330,7 @@ function _M.set_property(key, value)
end

local ckey = ffi_new("ngx_str_t", { data = key, len = #key })
local cvalue = ffi_new("ngx_str_t", { data = value, len = #value })
local cvalue = ffi_new("ngx_str_t", { data = value, len = value and #value or 0 })

local rc = C.ngx_http_wasm_ffi_set_property(r, ckey, cvalue)
if rc == FFI_ERROR then
Expand Down Expand Up @@ -280,4 +371,73 @@ function _M.get_property(key)
end


---
-- Define a setter function for host-managed properties.
--
-- @param setter The setter function is the handler for updating properties.
-- Its type signature is `function(string, string): boolean, string`,
-- where the inputs are the property key and value and the results are:
-- * `truthy` - success setting the property
-- * `truthy, value` - success, cache property value (useful if hosts
-- sets only a setter but not a getter)
-- * `truthy, value, truthy` - success, constant property value: further
-- requests to the same property during a request are cached by
-- ngx_wasm_module and do not invoke the Lua getter.
-- * `falsy, err` - failure, and an optional error message
-- @return true on success setting the getter, or nil and an error message.
function _M.set_property_setter(setter)
if type(setter) ~= "function" then
error("setter must be a function", 2)
end

local r = get_request()
if not r then
error("no request found", 2)
end

lua_prop_setter = setter

local rc = C.ngx_http_wasm_ffi_set_property_setter(r, c_prop_setter)
if rc == FFI_OK then
return true
end

return nil, "could not set property setter"
end


---
-- Define a getter function for host-managed properties.
--
-- @param getter The getter function is the handler for resolving properties.
-- Its type signature is `function(string): boolean, string, boolean`,
-- where the input is the property key and the results are:
-- * `truthy, value` - success, property value
-- * `truthy, value, truthy` - success, constant property value: further
-- requests to the same property during a request are cached by
-- ngx_wasm_module and do not invoke the Lua getter.
-- * `falsy` - property not found
-- * `falsy, err` - failure, error message
-- @return true on success setting the getter, or nil and an error message.
function _M.set_property_getter(getter)
if type(getter) ~= "function" then
error("getter must be a function", 2)
end

local r = get_request()
if not r then
error("no request found", 2)
end

lua_prop_getter = getter

local rc = C.ngx_http_wasm_ffi_set_property_getter(r, c_prop_getter)
if rc == FFI_OK then
return true
end

return nil, "could not set property getter"
end


return _M
58 changes: 58 additions & 0 deletions src/common/lua/ngx_wasm_lua_ffi.c
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ ngx_http_wasm_ffi_start(ngx_http_request_t *r)
}


static ngx_inline ngx_proxy_wasm_ctx_t *
get_pwctx(ngx_http_request_t *r)
{
ngx_http_wasm_req_ctx_t *rctx;

if (ngx_http_wasm_rctx(r, &rctx) != NGX_OK) {
return NULL;
}

return ngx_proxy_wasm_ctx(NULL, 0,
NGX_PROXY_WASM_ISOLATION_STREAM,
&ngx_http_proxy_wasm, rctx);
}


ngx_int_t
ngx_http_wasm_ffi_set_property(ngx_http_request_t *r,
ngx_str_t *key, ngx_str_t *value)
Expand Down Expand Up @@ -226,4 +241,47 @@ ngx_http_wasm_ffi_get_property(ngx_http_request_t *r,

return ngx_proxy_wasm_properties_get(pwctx, key, value);
}


ngx_int_t
ngx_http_wasm_ffi_set_host_property(ngx_http_request_t *r,
ngx_str_t *key, ngx_str_t *value, unsigned is_const, unsigned retrieve)
{
ngx_proxy_wasm_ctx_t *pwctx = get_pwctx(r);

if (pwctx == NULL) {
return NGX_ERROR;
}

return ngx_proxy_wasm_properties_set_host(pwctx, key, value, is_const,
retrieve);
}


ngx_int_t
ngx_http_wasm_ffi_set_property_setter(ngx_http_request_t *r,
ngx_wasm_host_prop_fn_t fn)
{
ngx_proxy_wasm_ctx_t *pwctx = get_pwctx(r);

if (pwctx == NULL) {
return NGX_ERROR;
}

return ngx_proxy_wasm_properties_set_host_prop_setter(pwctx, fn, r);
}


ngx_int_t
ngx_http_wasm_ffi_set_property_getter(ngx_http_request_t *r,
ngx_wasm_host_prop_fn_t fn)
{
ngx_proxy_wasm_ctx_t *pwctx = get_pwctx(r);

if (pwctx == NULL) {
return NGX_ERROR;
}

return ngx_proxy_wasm_properties_set_host_prop_getter(pwctx, fn, r);
}
#endif
6 changes: 6 additions & 0 deletions src/common/lua/ngx_wasm_lua_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ ngx_int_t ngx_http_wasm_ffi_set_property(ngx_http_request_t *r,
ngx_str_t *key, ngx_str_t *value);
ngx_int_t ngx_http_wasm_ffi_get_property(ngx_http_request_t *r,
ngx_str_t *key, ngx_str_t *value);
ngx_int_t ngx_http_wasm_ffi_set_host_property(ngx_http_request_t *r,
ngx_str_t *key, ngx_str_t *value, unsigned is_const, unsigned retrieve);
ngx_int_t ngx_http_wasm_ffi_set_property_setter(ngx_http_request_t *r,
ngx_wasm_host_prop_fn_t fn);
ngx_int_t ngx_http_wasm_ffi_set_property_getter(ngx_http_request_t *r,
ngx_wasm_host_prop_fn_t fn);
#endif


Expand Down
9 changes: 8 additions & 1 deletion src/common/proxy_wasm/ngx_proxy_wasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ typedef struct ngx_http_proxy_wasm_dispatch_s ngx_http_proxy_wasm_dispatch_t;

typedef ngx_str_t ngx_proxy_wasm_marshalled_map_t;

typedef ngx_int_t (*ngx_wasm_host_prop_fn_t)(void *data, ngx_str_t *key,
ngx_str_t *value);


typedef struct {
ngx_queue_t busy;
Expand Down Expand Up @@ -245,10 +248,14 @@ struct ngx_proxy_wasm_ctx_s {
ngx_uint_t call_code;
ngx_uint_t response_code;

/* host properties rbtree */
/* host properties */

ngx_rbtree_t host_props_tree;
ngx_rbtree_node_t host_props_sentinel;
ngx_wasm_host_prop_fn_t host_prop_getter;
ngx_wasm_host_prop_fn_t host_prop_setter;
void *host_prop_getter_data;
void *host_prop_setter_data;

/* flags */

Expand Down
Loading

0 comments on commit 04016dd

Please sign in to comment.