Skip to content

Commit 711fc10

Browse files
dbentley-vsqlclaude
andcommitted
sdk: typed prerun/postrun hooks
Introduce typed C++ wrappers for the per-statement lifecycle hooks (prerun and postrun). Extension authors no longer touch raw ABI structs to write these hooks; only the typed signatures are accepted. Why --- Extension authors writing prerun/postrun previously had to: void my_prerun(vef_context_t *, vef_prerun_args_t *args, vef_prerun_result_t *result) { if (args->arg_count == 0) { result->type = VEF_RESULT_ERROR; snprintf(result->error_msg, VEF_MAX_ERROR_LEN, "..."); return; } result->result_buffer_size = N; result->user_data = new MyState{}; } Two problems: raw ABI in the user-facing API (against the SDK's direction; see #548) and bespoke error-reporting plumbing. After this PR: void my_prerun(vsql::PrerunArgs args, vsql::PrerunResult out) { if (args.size() == 0) { out.error("at least one argument required"); return; } out.request_buffer_size(N); out.set_user_data(new MyState{}); } void my_postrun(vsql::PostrunArgs args) { args.delete_state<MyState>(); } void my_vdf(MyState &state, IntResult out) { ... } State lifetime stays explicit in this PR: prerun stashes the pointer, postrun frees it. A follow-up will add auto-cleanup (see "Not in this PR" below). Where to start reviewing ------------------------ Read the layers in this order: 1. villagesql/sdk/include/villagesql/vsql/pre_post_run.h New public types: PrerunArgs, PrerunArgType, PrerunResult, PostrunArgs. This is what extension authors see. 2. villagesql/examples/vsql-simple/src/extension.cc The ba_call_index demo: set_user_data in prerun, mutable State& in the VDF, delete_state in postrun. 3. mysql-test/suite/villagesql/extension/t/extension_simple_type_usage.test What it looks like at the SQL surface. 4. villagesql/sdk/include/villagesql/extension.h Refreshed user-facing docs for the typed hook shape. The remaining file is SDK plumbing: 5. villagesql/sdk/include/villagesql/vsql/func_builder.h - .prerun<>() / .postrun<>() are typed-only; static_assert rejects raw vef_prerun_func_t / vef_postrun_func_t. - WrapperTypedState / WrapperVoidState dispatch on the first param of the VDF: State& / const State& / void* selects how user_data is forwarded. - typed_prerun_thunk / typed_postrun_thunk adapt the user's typed signature to the raw ABI shape the server invokes. Not in this PR -------------- - Auto-cleanup for SDK-allocated state. Requires an ABI side channel (e.g. a user_data_deleter slot on vef_prerun_result_t) so the SDK can register a destructor independent of the user_data pointer that extensions also use for raw/custom allocations. Marked TODO(villagesql-beta) in pre_post_run.h. - A `.state<T>()` builder that records State at the template level and routes typed signatures of the form `void(T&, ...)`. Will land with the auto-cleanup mechanism above. - Typed VarArgs/AnyArg wrappers for varargs VDFs (the other raw-ABI place in extension code, only reached via PR #254). - vef_postrun_result_t is empty in the ABI today, so there is no PostrunResult wrapper. Will add when the struct grows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e76fe56 commit 711fc10

6 files changed

Lines changed: 40 additions & 111 deletions

File tree

villagesql/examples/vsql-simple/src/extension.cc

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,24 +113,27 @@ void odd_chars(CustomArg in, CustomResult out) {
113113
}
114114

115115
// BA_CALL_INDEX: return the 1-based index of this call within the current
116-
// statement. Demonstrates the typed prerun + State& pattern: prerun
117-
// allocates a counter via emplace_state<T> and each row reads-and-
118-
// increments. No postrun needed — the SDK's installed deleter frees the
119-
// CallCounter after the last row.
116+
// statement. Demonstrates the typed prerun/postrun + State& pattern: prerun
117+
// allocates a counter, each row reads-and-increments via the VDF's State&
118+
// first parameter, and postrun frees.
120119

121120
struct CallCounter {
122121
long long n = 0;
123122
};
124123

125124
void ba_call_index_prerun(PrerunArgs, PrerunResult out) {
126-
out.emplace_state<CallCounter>();
125+
out.set_user_data(new CallCounter{});
127126
}
128127

129128
void ba_call_index(CallCounter &state, IntResult out) {
130129
state.n++;
131130
out.set(state.n);
132131
}
133132

133+
void ba_call_index_postrun(PostrunArgs args) {
134+
args.delete_state<CallCounter>();
135+
}
136+
134137
// BA_CONCAT: concatenate two bytearrays (returns STRING with 16 bytes)
135138
void ba_concat(CustomArg a, CustomArg b, StringResult out) {
136139
if (a.is_null() || b.is_null()) {
@@ -176,4 +179,5 @@ VEF_GENERATE_ENTRY_POINTS(make_extension()
176179
.func(make_func<&ba_call_index>("ba_call_index")
177180
.returns(INT)
178181
.prerun<&ba_call_index_prerun>()
182+
.postrun<&ba_call_index_postrun>()
179183
.build()))

villagesql/sdk/include/villagesql/abi/types.h

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -519,16 +519,6 @@ typedef struct {
519519
// Extension-allocated state. Set this to pass data to vdf and postrun.
520520
// Caller initializes to NULL.
521521
void *user_data;
522-
523-
// Optional SDK-managed deleter for user_data. If non-NULL, the server
524-
// calls user_data_deleter(user_data) after the postrun returns (or after
525-
// the last vdf call, if no postrun was registered). Used by the C++ SDK's
526-
// PrerunResult::emplace_state<T> to install typed_delete<T> so extensions
527-
// do not have to write a postrun just to free state.
528-
//
529-
// Raw C ABI users typically leave this NULL and free user_data themselves
530-
// in their postrun.
531-
void (*user_data_deleter)(void *user_data);
532522
} vef_prerun_result_t;
533523

534524
typedef void (*vef_prerun_func_t)(vef_context_t *ctx, vef_prerun_args_t *args,

villagesql/sdk/include/villagesql/extension.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,12 @@
120120
//
121121
// void my_prerun(vsql::PrerunArgs args, vsql::PrerunResult out) {
122122
// // validate args.type_at(i), call out.error(...) on failure,
123-
// // out.emplace_state<MyState>(...) for per-statement state, etc.
123+
// // request a larger result buffer with out.request_buffer_size(N),
124+
// // and/or stash per-statement state with out.set_user_data(...).
124125
// }
125126
// void my_postrun(vsql::PostrunArgs args) {
126-
// // optional; only needed if you used set_user_data with a non-C++
127-
// // allocator. emplace_state<T> is auto-freed by the SDK.
127+
// // pair with prerun's set_user_data to free state, e.g.
128+
// // args.delete_state<MyState>().
128129
// }
129130
//
130131
// make_func<&my_impl>("my_func")

villagesql/sdk/include/villagesql/vsql/pre_post_run.h

Lines changed: 21 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -29,50 +29,33 @@
2929
// compile time. Extensions that need to drop to the raw ABI for these
3030
// hooks would have to skip the vsql builder entirely.
3131
//
32-
// State lifetime:
33-
// emplace_state<T>(args...) — SDK-owned. The SDK installs a deleter via
34-
// vef_prerun_result_t::user_data_deleter and
35-
// the server calls it after the user's
36-
// postrun. No leak even if the extension
37-
// does not write a postrun.
38-
// set_user_data(void *) — caller-owned. The SDK does not install a
39-
// deleter; the extension is responsible for
40-
// freeing in its own postrun.
32+
// State lifetime is explicit: if prerun stores a pointer via set_user_data,
33+
// postrun is responsible for freeing it. PostrunArgs::delete_state<T>() is
34+
// the typed convenience for the common case of `new T{}` + `delete`.
4135
//
4236
// vef_postrun_result_t is currently empty in the ABI, so there is no
4337
// PostrunResult wrapper. If/when the result struct gains fields, a typed
4438
// wrapper class will be added and the postrun signature will become
4539
// `void(PostrunArgs, PostrunResult)`.
4640
//
47-
// TODO(villagesql-beta): add a `.state<T>()` builder method on FuncBuilder
48-
// that records the State type at the template level, auto-installs the
49-
// prerun (default-constructed T) and routes typed prerun/postrun/VDF
50-
// signatures of the form `void(T&, ...)`. Once this lands, the explicit
51-
// emplace_state<T>/state<T>/delete_state<T> pattern becomes optional sugar.
41+
// TODO(villagesql-beta): add a typed-state mechanism (working name
42+
// `.state<T>()` on FuncBuilder) so extensions can declare a per-statement
43+
// state type and have the SDK manage its lifetime automatically — no
44+
// matched prerun/postrun pair required just to free state. The shape is
45+
// blocked on adding an ABI side channel (e.g. a user_data_deleter slot on
46+
// vef_prerun_result_t) so the SDK can register the destructor without
47+
// reserving the user_data pointer slot for SDK use.
5248

5349
#include <cstddef>
5450
#include <cstring>
5551
#include <optional>
5652
#include <string_view>
57-
#include <type_traits>
5853
#include <utility>
5954

6055
#include <villagesql/abi/types.h>
6156

6257
namespace vsql {
6358

64-
namespace detail {
65-
66-
// Type-erased deleter installed by PrerunResult::emplace_state<T>. Stored in
67-
// vef_prerun_result_t::user_data_deleter so the server can free the
68-
// SDK-owned state without knowing T.
69-
template <typename T>
70-
inline void typed_delete(void *p) {
71-
delete static_cast<T *>(p);
72-
}
73-
74-
} // namespace detail
75-
7659
// =============================================================================
7760
// Prerun
7861
// =============================================================================
@@ -129,37 +112,10 @@ class PrerunResult {
129112
public:
130113
explicit PrerunResult(vef_prerun_result_t *r) : r_(r) {}
131114

132-
// Heap-allocate a T as the per-statement state, accessible from the VDF
133-
// and postrun via PostrunArgs::state<T>() (or as a `T&` first param on the
134-
// VDF/postrun). The SDK installs a deleter via the ABI's
135-
// user_data_deleter slot, so the server frees the T automatically after
136-
// postrun — no postrun is required just to clean up state.
137-
//
138-
// Allocator boundary: both the `new T` here and the matching `delete` in
139-
// the SDK-installed deleter run inside the extension's .so via the
140-
// extension's allocator. The server only invokes the deleter through a
141-
// function pointer; it never touches extension-heap memory directly. So
142-
// it is safe for the extension to link a different malloc/free than
143-
// mysqld.
144-
//
145-
// For non-C++ allocations (malloc/arena/pool) or polymorphic state, use
146-
// set_user_data instead and own the lifetime yourself.
147-
template <typename T, typename... Args>
148-
T *emplace_state(Args &&...args) {
149-
static_assert(!std::is_pointer_v<T>,
150-
"emplace_state<T> allocates a T on the heap. To store an "
151-
"existing pointer in user_data, call set_user_data(p) "
152-
"instead.");
153-
auto *p = new T(std::forward<Args>(args)...);
154-
r_->user_data = p;
155-
r_->user_data_deleter = &detail::typed_delete<T>;
156-
return p;
157-
}
158-
159-
// Stash an arbitrary void* the extension owns. Use this for non-C++
160-
// allocations (malloc/arena/pool), polymorphic state, or anything that
161-
// doesn't fit the typed emplace_state pattern. Postrun reads back via
162-
// PostrunArgs::user_data().
115+
// Stash a pointer the extension owns. The VDF and postrun read it back
116+
// via PostrunArgs::user_data() or PostrunArgs::state<T>(). The SDK does
117+
// not free anything stashed here; pair this with a postrun that calls
118+
// PostrunArgs::delete_state<T>() (or free()) on the same pointer.
163119
void set_user_data(void *p) { r_->user_data = p; }
164120

165121
// Request that the per-row result buffer be at least n bytes. The server
@@ -188,22 +144,20 @@ class PostrunArgs {
188144
public:
189145
explicit PostrunArgs(const vef_postrun_args_t *a) : a_(a) {}
190146

191-
// Read the raw user_data slot. Use this when prerun set it via
192-
// set_user_data (raw / non-C++ allocation).
147+
// Read the raw user_data slot — whatever prerun stored via set_user_data.
148+
// Use this for non-C++ allocations (malloc/arena/pool) or anything that
149+
// doesn't fit a single typed T.
193150
void *user_data() const { return a_->user_data; }
194151

195-
// Convenience: cast user_data to a T*. Use this when prerun set it via
196-
// emplace_state<T>().
152+
// Convenience: static_cast user_data to a T*. Pair with prerun's
153+
// set_user_data(new T{...}).
197154
template <typename T>
198155
T *state() const {
199156
return static_cast<T *>(a_->user_data);
200157
}
201158

202-
// Convenience: `delete state<T>()` in one call. Use this only when prerun
203-
// called set_user_data with a heap-allocated T and you want a typed free.
204-
// Do NOT call this if prerun used emplace_state<T> — the SDK already
205-
// frees the state via the installed deleter, and calling delete_state
206-
// would be a double-free.
159+
// Convenience: `delete state<T>()` in one call. Pair with prerun's
160+
// set_user_data(new T{...}).
207161
template <typename T>
208162
void delete_state() const {
209163
delete state<T>();

villagesql/vdf/vdf_handler.cc

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -195,26 +195,18 @@ bool vdf_handler::fix_fields(THD *thd [[maybe_unused]],
195195
prerun_result.error_msg = error_msg;
196196
prerun_result.result_buffer_size = 0;
197197
prerun_result.user_data = nullptr;
198-
prerun_result.user_data_deleter = nullptr;
199198

200199
m_udf->vdf_func_desc->prerun(&m_context, &prerun_args, &prerun_result);
201200

202201
if (prerun_result.type == VEF_RESULT_WARNING ||
203202
prerun_result.type == VEF_RESULT_ERROR) {
204-
// Prerun reported failure. If it had already installed user_data and a
205-
// deleter (e.g. allocation succeeded but a later validation failed),
206-
// honor the deleter so we don't leak before propagating the error.
207-
if (prerun_result.user_data_deleter != nullptr) {
208-
prerun_result.user_data_deleter(prerun_result.user_data);
209-
}
210203
my_error(ER_CANT_INITIALIZE_UDF, MYF(0), m_udf->name.str,
211204
error_msg[0] ? error_msg : "prerun failed");
212205
return true;
213206
}
214207

215208
// Store user_data for subsequent calls
216209
m_vdf_args.user_data = prerun_result.user_data;
217-
m_user_data_deleter = prerun_result.user_data_deleter;
218210

219211
// Handle buffer size request
220212
if (prerun_result.result_buffer_size > m_result_buffer_size) {
@@ -274,21 +266,12 @@ void vdf_handler::accumulate(bool *null_value) {
274266
}
275267

276268
void vdf_handler::cleanup() {
277-
if (m_active) {
278-
// Run the user's postrun first so it can inspect state before we free it.
279-
if (m_udf->vdf_func_desc->postrun) {
280-
vef_postrun_args_t postrun_args{};
281-
postrun_args.user_data = m_vdf_args.user_data;
282-
vef_postrun_result_t postrun_result{};
283-
m_udf->vdf_func_desc->postrun(&m_context, &postrun_args, &postrun_result);
284-
}
285-
// SDK-installed deleter (from PrerunResult::emplace_state<T>) frees the
286-
// typed state without requiring the extension to write a postrun.
287-
if (m_user_data_deleter != nullptr) {
288-
m_user_data_deleter(m_vdf_args.user_data);
289-
m_user_data_deleter = nullptr;
290-
m_vdf_args.user_data = nullptr;
291-
}
269+
// Call postrun if VDF was active and postrun exists
270+
if (m_active && m_udf->vdf_func_desc->postrun) {
271+
vef_postrun_args_t postrun_args{};
272+
postrun_args.user_data = m_vdf_args.user_data;
273+
vef_postrun_result_t postrun_result{};
274+
m_udf->vdf_func_desc->postrun(&m_context, &postrun_args, &postrun_result);
292275
}
293276
m_active = false;
294277
}

villagesql/vdf/vdf_handler.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ class vdf_handler {
7474
size_t m_result_buffer_size{0};
7575
char *m_error_msg{nullptr};
7676
bool m_active{false};
77-
// Optional SDK-managed deleter for m_vdf_args.user_data, captured from
78-
// prerun_result.user_data_deleter and invoked after postrun in cleanup().
79-
void (*m_user_data_deleter)(void *){nullptr};
8077
const villagesql::TypeContext *m_return_type_context{nullptr};
8178

8279
// Marshal arguments into m_invalues array based on declared parameter types

0 commit comments

Comments
 (0)