preview capabilities#418
Conversation
7346d76 to
0886266
Compare
0886266 to
4fea333
Compare
| // capability struct at populate time. | ||
| // Intended for built-in capabilities registered at startup and for | ||
| // extension-provided capabilities. | ||
| void register_capability(std::string name, uint32_t version, void *vtable, |
There was a problem hiding this comment.
Do we need to expose this yet (vs. just register_builtin_capabilities)?
| "version": "0.0.1", | ||
| "description": "VillageSQL test extension for the preview capability system", | ||
| "author": "VillageSQL Community", | ||
| "license": "GPL-2.0" |
There was a problem hiding this comment.
TODO that we want to require preview capabilities to be listed here?
There was a problem hiding this comment.
I added a TODO where we populate the capabilities to verify they are the same as what is in the manifest
| // FuncCount, TypeCount, SysVarCount, and StatusVarCount are explicit template | ||
| // parameters so that array sizes are compile-time constants without relying on | ||
| // VLAs. | ||
| // FuncCount, TypeCount, SysVarCount, StatusVarCount, and |
There was a problem hiding this comment.
Probably worth switching from enumerating each to just describing this in general at this point.
There was a problem hiding this comment.
changed to generic comment
| // of the extension (use a string literal). | ||
| const char *name; | ||
| // Version of the capability struct the extension was compiled against. | ||
| uint32_t version; |
There was a problem hiding this comment.
I'm a bit uneasy about adding yet another version field before we need it.
There was a problem hiding this comment.
Isn't this the point of the preview feature? It is versioned independently of the rest of the ABI; I asked about this in your doc.
| VEF_GENERATE_ENTRY_POINTS( | ||
| make_extension() | ||
| .func(make_func<&ping_impl>("ping").returns(INT).build()) | ||
| .preview_require_ping(g_ping)) |
There was a problem hiding this comment.
I thought it would be:
preview_require(g_ping)
(i.e., one method for all preview capabilities and one for preview require capabilities)
There was a problem hiding this comment.
I like that, but the current design followed what was outlined in the slack thread.
|
|
||
| namespace { | ||
|
|
||
| // Registry key: capability name + version. |
There was a problem hiding this comment.
Some of this is related to capabilities generally (vs. specifically to preview_capabilities).
malone-at-work
left a comment
There was a problem hiding this comment.
I will need to take another pass.
| return true; | ||
| } | ||
|
|
||
| // Populate any preview capabilities the extension requires. |
There was a problem hiding this comment.
I think that this should go after at least line 891
| // of the extension (use a string literal). | ||
| const char *name; | ||
| // Version of the capability struct the extension was compiled against. | ||
| uint32_t version; |
There was a problem hiding this comment.
Isn't this the point of the preview feature? It is versioned independently of the rest of the ABI; I asked about this in your doc.
|
|
||
| // Copy the vtable into the extension's capability struct. Both are the | ||
| // same ABI type and the registered size matches sizeof(that type). | ||
| memcpy(req.capability, it->second.vtable, it->second.vtable_size); |
There was a problem hiding this comment.
This feels awkward. How does the extension know when this is safe to access?
Could the vef_register_result_t have a func pointer for each element:
vef_register_capability(void *vtable)
Or maybe one that then gets called with each of the vtables and it dispatches them?
8fddfec to
fba36d9
Compare
|
|
||
| struct CapabilityKey { | ||
| std::string name; | ||
| uint32_t version; |
There was a problem hiding this comment.
I thought only preview capabilities would have a different version. Let's discuss versions Monday morning.
on_load now takes const vef_register_arg_t * (server context, extensible). on_unload takes no arguments (cleanup only, no server context needed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
||
| // Returns a compile-time hash of T's fully qualified type name via | ||
| // __PRETTY_FUNCTION__. Used to verify that the capability ABI struct type | ||
| // an extension was compiled against matches the type the server registered. |
There was a problem hiding this comment.
AFAICT - This doesn't include anything about the structure of the type, just the name; i.e. you could change the functions in the interface, and this would not change:
> cat t.cc
#include <iostream>
#include <string_view>
template <typename T>
constexpr std::string_view abi_hash_this() {
return __PRETTY_FUNCTION__;
}
struct Foo {
int foo_A();
int foo_B(int);
};
int main() {
std::cout << abi_hash_this<Foo>() << std::endl;
return 0;
}
> clang++ t.cc -o t
> ./t
std::string_view abi_hash_this() [T = Foo]There was a problem hiding this comment.
I asked Claude if there are C++ features that could help with this problem, C++26 seems nice, but I don't think we can use it yet:
C++26 static reflection (P2996) is exactly what's needed here.
C++26: std::meta (P2996)
Accepted into C++26, this lets you enumerate a struct's non-static data members at compile time:
#include <meta>
template <typename T>
constexpr size_t layout_hash() {
size_t h = sizeof(T);
// Iterate every non-static data member automatically
for... (auto member : std::meta::nonstatic_data_members_of(^T)) {
h = (h ^ std::meta::offset_of(member)) * 1099511628211ULL;
h = (h ^ std::meta::size_of(member)) * 1099511628211ULL;
}
return h;
}
This would catch adding/removing fields, reordering them, and changing their types — all automatically, with no
per-struct boilerplate. Clang has experimental support behind -freflection today, but it's not production-ready and
the syntax is still slightly in flux.
Practical today: Boost.PFR (C++17/20)
For aggregate structs (plain C structs like vef_preview_ping_t qualify), Boost.PFR can enumerate fields without macros
or annotations:
#include <boost/pfr.hpp>
template <typename T>
constexpr size_t layout_hash() {
size_t h = sizeof(T);
// Hashes the size of each field in order — catches count and type changes
boost::pfr::for_each_field(T{}, [&h](auto& field) {
h = (h ^ sizeof(field)) * 1099511628211ULL;
});
return h;
}
Limitation: PFR gives you field values/types but not offsetof, so it catches field count and size changes but misses
reordering of fields with the same type (e.g. swapping two uint32_ts). It also can't be constexpr in all cases.
Summary
┌───────────────────────────────┬─────────────────────┬─────────────────┬───────────────┐
│ Approach │ Catches size change │ Catches reorder │ Available now │
├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
│ Current (__PRETTY_FUNCTION__) │ No │ No │ Yes │
├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
│ + sizeof(T) │ Yes │ No │ Yes │
├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
│ Boost.PFR │ Mostly │ No │ Yes │
├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
│ C++26 std::meta │ Yes │ Yes │ Experimental │
└───────────────────────────────┴─────────────────────┴─────────────────┴───────────────┘
For VillageSQL, the sizeof(T) improvement is the pragmatic near-term fix. C++26 reflection is the right long-term
answer once it stabilizes — and since the ABI struct headers are simple C aggregates, they'd be ideal candidates for
it.
There was a problem hiding this comment.
I have a new proposal via Claude (though maybe this macros should have PREVIEW in the name?):
❯ What about a C++ macro or something that hashes the text of the struct?
⏺ Yes — the preprocessor # (stringify) operator can do this. Since fnv1a_hash is already constexpr, you can hash the
literal text of the struct body at compile time:
#define VEF_DEFINE_ABI(TypeName, ...) \
typedef struct { __VA_ARGS__ } TypeName; \
namespace villagesql::detail { \
template <> \
constexpr size_t abi_schema_hash<TypeName>() { \
constexpr const char s[] = #__VA_ARGS__; \
return fnv1a_hash(s, sizeof(s) - 1); \
} \
}
// Usage:
VEF_DEFINE_ABI(vef_preview_ping_t,
vef_ping_fn ping;
)
#__VA_ARGS__ captures "vef_ping_fn ping;" as a string literal at compile time. Add a field, reorder, change a type —
the string changes, the hash changes.
The real risk: clang-format. Macro arguments are generally left alone by clang-format, but if it ever reformats the
whitespace inside (e.g. normalizes spacing around ;), the hash silently changes and extensions built before the
reformat break at load time, even though the ABI is identical. You'd want a // clang-format off guard around uses of
the macro.
There was a problem hiding this comment.
going with the sizeof(T) improvement — it catches the practically important cases (field additions/removals, type size changes) without external dependencies. Let me know if you think it is worth going further
bd0c92d to
d625ec7
Compare
| const char *name; | ||
| // Called by the server with the capability vtable pointer if registered. | ||
| // The extension assigns the vtable to its own capability struct. | ||
| void (*receive)(void *vtable); |
There was a problem hiding this comment.
Why do we need receive AND onload? Can't we just use 1?
There was a problem hiding this comment.
receive is internal plumbing, on_load is a user facing hook. Not used currently, but the thread PR will be using on_unload to clean up
| static bool check_allow_preview_extensions(sys_var *, THD *, set_var *var) { | ||
| const bool new_value = var->save_result.ulonglong_value != 0; | ||
|
|
||
| // TODO(villagesql-beta): There is a TOCTOU race between this check and the |
There was a problem hiding this comment.
This comment applies to the false branch, can you move it down to that branch?
|
LGTM, my only concern is do we need both onload and receive. I think we should pass the vtable in onload. |
|
|
||
| } // namespace | ||
|
|
||
| void register_capability(std::string name, void *vtable, size_t abi_type_hash) { |
There was a problem hiding this comment.
Is it important to expose this if it's only used by register_builtin_capabilities?
| g_registry[std::move(name)] = {vtable, abi_type_hash}; | ||
| } | ||
|
|
||
| void unregister_capability(const std::string &name) { g_registry.erase(name); } |
There was a problem hiding this comment.
Do we ever need to unregister a capability?
There was a problem hiding this comment.
we call it, but it is a no-op currently, future use
|
|
||
| // TODO(villagesql-beta): Verify that the capabilities declared in | ||
| // vef_registration_t match those listed in the extension's manifest. | ||
| bool populate_capabilities(const vef_registration_t *reg, |
There was a problem hiding this comment.
Is it hard to do? Shoudl we have a 0.0.5 TODO target, cause I don't want to ship without this?
| #include "villagesql/sdk/include/villagesql/detail/capability_hash.h" | ||
| #include "villagesql/services/preview/ping.h" | ||
|
|
||
| bool vsql_allow_preview_extensions = false; |
There was a problem hiding this comment.
Maybe "allow_preview_capabilities", because it's the capabilities that are in preview, not the extensions themselves? (I could imagine us offering preview extensions that are only using fully-baked capabilities)
There was a problem hiding this comment.
keeping it as is for now
| // Register all server built-in capabilities. Called once at server startup. | ||
| void register_builtin_capabilities(); | ||
|
|
||
| // Populate capabilities declared in a vef_registration_t. |
There was a problem hiding this comment.
I'd mention "for one extension", to clarify the difference here.
| return h; | ||
| } | ||
|
|
||
| // Returns a compile-time hash of T's fully qualified type name and size via |
There was a problem hiding this comment.
I'm not sure the hash buys us that much? It's just the len of the vtable, not hashing the functions' signatures themselves?
There was a problem hiding this comment.
yes, does a bit more now with Mike suggestion
a4b2dfd to
6ce9777
Compare
| } vef_status_var_desc_t; | ||
|
|
||
| // Forward declaration so vef_required_capability_t can reference it. | ||
| typedef struct vef_registration_t vef_registration_t; |
There was a problem hiding this comment.
We no longer need this forward declaration.
|
|
||
| bool available() const { return abi_.ping != nullptr; } | ||
|
|
||
| // Public so that cap_receive() can access abi_ via a pointer. |
There was a problem hiding this comment.
Not necessarily in this PR, but we should make the cap_receive() a friend or something in future capabilities.
No description provided.