Skip to content

preview capabilities#418

Merged
tomas-villagesql merged 7 commits intomainfrom
tomas/preview-abi
May 5, 2026
Merged

preview capabilities#418
tomas-villagesql merged 7 commits intomainfrom
tomas/preview-abi

Conversation

@tomas-villagesql
Copy link
Copy Markdown
Member

No description provided.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
// 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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to expose this yet (vs. just register_builtin_capabilities)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved it out

"version": "0.0.1",
"description": "VillageSQL test extension for the preview capability system",
"author": "VillageSQL Community",
"license": "GPL-2.0"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO that we want to require preview capabilities to be listed here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth switching from enumerating each to just describing this in general at this point.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit uneasy about adding yet another version field before we need it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed version

VEF_GENERATE_ENTRY_POINTS(
make_extension()
.func(make_func<&ping_impl>("ping").returns(INT).build())
.preview_require_ping(g_ping))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it would be:

preview_require(g_ping)

(i.e., one method for all preview capabilities and one for preview require capabilities)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that, but the current design followed what was outlined in the slack thread.


namespace {

// Registry key: capability name + version.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of this is related to capabilities generally (vs. specifically to preview_capabilities).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split it up

Copy link
Copy Markdown
Member

@malone-at-work malone-at-work left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need to take another pass.

Comment thread villagesql/veb/veb_file.cc Outdated
return true;
}

// Populate any preview capabilities the extension requires.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this should go after at least line 891

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

// of the extension (use a string literal).
const char *name;
// Version of the capability struct the extension was compiled against.
uint32_t version;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


struct CapabilityKey {
std::string name;
uint32_t version;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought only preview capabilities would have a different version. Let's discuss versions Monday morning.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed version

tomas-villagesql and others added 2 commits May 4, 2026 18:37
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need receive AND onload? Can't we just use 1?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread villagesql/sql/initialize.cc Outdated
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment applies to the false branch, can you move it down to that branch?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

@malone-at-work
Copy link
Copy Markdown
Member

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it important to expose this if it's only used by register_builtin_capabilities?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

g_registry[std::move(name)] = {vtable, abi_type_hash};
}

void unregister_capability(const std::string &name) { g_registry.erase(name); }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever need to unregister a capability?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it hard to do? Shoudl we have a 0.0.5 TODO target, cause I don't want to ship without this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed TODO

#include "villagesql/sdk/include/villagesql/detail/capability_hash.h"
#include "villagesql/services/preview/ping.h"

bool vsql_allow_preview_extensions = false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd mention "for one extension", to clarify the difference here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return h;
}

// Returns a compile-time hash of T's fully qualified type name and size via
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the hash buys us that much? It's just the len of the vtable, not hashing the functions' signatures themselves?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, does a bit more now with Mike suggestion

@tomas-villagesql tomas-villagesql enabled auto-merge (squash) May 5, 2026 18:37
} vef_status_var_desc_t;

// Forward declaration so vef_required_capability_t can reference it.
typedef struct vef_registration_t vef_registration_t;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily in this PR, but we should make the cap_receive() a friend or something in future capabilities.

@tomas-villagesql tomas-villagesql merged commit 639a1ea into main May 5, 2026
3 checks passed
@tomas-villagesql tomas-villagesql deleted the tomas/preview-abi branch May 5, 2026 18:52
@github-actions github-actions Bot locked and limited conversation to collaborators May 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants