From 7f74ad1bb27efa76b0d3b9a2988553d7aeb0945d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Thu, 9 Jul 2026 12:21:35 -0400 Subject: [PATCH 1/3] pd: register the class under the external's own name (C_NAME) Pd finds a single-object external by filename and requires the registered class symbol to equal that filename. avendish names the file and the setup function after the CMake C_NAME (@AVND_C_NAME@.pd_*, @AVND_C_NAME@_setup), but the class was registered under symbol_from_name() = the introspected C++ symbol/ identifier. When those differ (e.g. an addon whose C_NAME is 'smoother' but whose struct is 'Smoother', or any object without a c_name meta), Pd loads the file, runs setup, but the class it asked for is never registered -- so its loader retries and eventually aborts with the abstraction recursion limit. This made most puara objects impossible to instantiate. Thread @AVND_C_NAME@ from the setup function into the audio/message metaclass so class_new() uses exactly the external's name (falling back to the old introspected name when the metaclass is constructed directly, e.g. in tests). Verified: an addon with C_NAME 'coolthing' and struct 'Foo' (no c_name meta) now emits coolthing_setup() -> class_new(gensym("coolthing")) and builds a coolthing.\* external; previously it registered class 'Foo' and could not be created. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014Z4KK2Rays9dTj8J2AJcFZ --- include/avnd/binding/pd/audio_processor.hpp | 11 +++++++---- include/avnd/binding/pd/message_processor.hpp | 11 +++++++---- include/avnd/binding/pd/prototype.cpp.in | 12 +++++++++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/include/avnd/binding/pd/audio_processor.hpp b/include/avnd/binding/pd/audio_processor.hpp index 6d52b56c..49f164d8 100644 --- a/include/avnd/binding/pd/audio_processor.hpp +++ b/include/avnd/binding/pd/audio_processor.hpp @@ -35,7 +35,10 @@ struct audio_processor_metaclass static inline t_class* g_class{}; static inline audio_processor_metaclass* instance{}; - audio_processor_metaclass(); + // class_name is the Pd class symbol; the setup function passes @AVND_C_NAME@ so + // the registered class matches the external's filename (nullptr -> fall back to + // the introspected name, for callers that construct the metaclass directly). + audio_processor_metaclass(t_symbol* class_name = nullptr); }; template @@ -205,7 +208,7 @@ struct audio_processor }; template -audio_processor_metaclass::audio_processor_metaclass() +audio_processor_metaclass::audio_processor_metaclass(t_symbol* class_name) { audio_processor_metaclass::instance = this; using instance = audio_processor; @@ -249,8 +252,8 @@ audio_processor_metaclass::audio_processor_metaclass() /// Class creation /// g_class = class_new( - symbol_from_name(), (t_newmethod)obj_new, (t_method)obj_free, - sizeof(audio_processor), CLASS_DEFAULT, A_GIMME, 0); + class_name ? class_name : symbol_from_name(), (t_newmethod)obj_new, + (t_method)obj_free, sizeof(audio_processor), CLASS_DEFAULT, A_GIMME, 0); // First port will receive messages CLASS_MAINSIGNALIN(g_class, audio_processor, f); diff --git a/include/avnd/binding/pd/message_processor.hpp b/include/avnd/binding/pd/message_processor.hpp index f0742ff5..0194fca0 100644 --- a/include/avnd/binding/pd/message_processor.hpp +++ b/include/avnd/binding/pd/message_processor.hpp @@ -35,7 +35,10 @@ struct message_processor_metaclass static inline t_class* g_class{}; static inline message_processor_metaclass* instance{}; - message_processor_metaclass(); + // class_name is the Pd class symbol; the setup function passes @AVND_C_NAME@ so + // the registered class matches the external's filename (nullptr -> fall back to + // the introspected name, for callers that construct the metaclass directly). + message_processor_metaclass(t_symbol* class_name = nullptr); }; template @@ -187,7 +190,7 @@ struct message_processor }; template -message_processor_metaclass::message_processor_metaclass() +message_processor_metaclass::message_processor_metaclass(t_symbol* class_name) { message_processor_metaclass::instance = this; using instance = message_processor; @@ -228,8 +231,8 @@ message_processor_metaclass::message_processor_metaclass() /// Class creation /// g_class = class_new( - symbol_from_name(), (t_newmethod)obj_new, (t_method)obj_free, - sizeof(message_processor), CLASS_DEFAULT, A_GIMME, 0); + class_name ? class_name : symbol_from_name(), (t_newmethod)obj_new, + (t_method)obj_free, sizeof(message_processor), CLASS_DEFAULT, A_GIMME, 0); // Connect our methods class_addanything(g_class, (t_method)obj_process); diff --git a/include/avnd/binding/pd/prototype.cpp.in b/include/avnd/binding/pd/prototype.cpp.in index 5cefe039..d548c6a5 100644 --- a/include/avnd/binding/pd/prototype.cpp.in +++ b/include/avnd/binding/pd/prototype.cpp.in @@ -10,17 +10,23 @@ using type = decltype(avnd::configure())::type; extern "C" AVND_EXPORTED_SYMBOL void @AVND_C_NAME@_setup() { - [] { + // Register the class under the external's own name (the file Pd just loaded is + // @AVND_C_NAME@.pd_*, and this function is @AVND_C_NAME@_setup). Pd looks up a + // single-object external by filename, so the class symbol must match it exactly; + // otherwise the object loads but the class is registered under a different name, + // Pd never finds it, and its loader retries until it hits the recursion limit. + t_symbol* const avnd_class_name = gensym("@AVND_C_NAME@"); + [avnd_class_name] { if constexpr( avnd::monophonic_audio_processor || avnd::polyphonic_audio_processor) { // If we're an audio effect, make a type with the whole DSP stuff - static const pd::audio_processor_metaclass instance{}; + static const pd::audio_processor_metaclass instance{avnd_class_name}; } else { // Simpler case which just processes messages - static const pd::message_processor_metaclass instance{}; + static const pd::message_processor_metaclass instance{avnd_class_name}; } }(); } From 5a7a677fe49b06447a1029bb8b391e558a8e3b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Thu, 9 Jul 2026 16:16:42 -0400 Subject: [PATCH 2/3] golden: static_assert that the CMake C_NAME matches the object's c_name Every object is built for the golden differential harness (SDK-free, always on), so one static_assert there enforces the name invariant for all back-ends at once, rather than duplicating it per shipping backend: when an object declares an explicit halp_meta(c_name, ...), the avnd_addon_object / avnd_make_* C_NAME must equal it (they name the same external -- the file + class Pd/Max load by). Objects that declare no c_name leave the CMake C_NAME authoritative. Verified across all 224 golden objects. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014Z4KK2Rays9dTj8J2AJcFZ --- include/avnd/binding/golden/prototype.cpp.in | 12 ++++++++++++ include/avnd/wrappers/metadatas.hpp | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/include/avnd/binding/golden/prototype.cpp.in b/include/avnd/binding/golden/prototype.cpp.in index 051786a1..e84c7421 100644 --- a/include/avnd/binding/golden/prototype.cpp.in +++ b/include/avnd/binding/golden/prototype.cpp.in @@ -17,6 +17,18 @@ struct config int main(int argc, char** argv) { using type = decltype(avnd::configure())::type; + + // Every object is built for the golden harness (SDK-free, always on), so this + // single check enforces the name invariant for all back-ends at once: when an + // object declares an explicit halp_meta(c_name, ...), the avnd_addon_object / + // avnd_make_* C_NAME must equal it -- they name the same external (the file and + // class Pd/Max load by), and a mismatch ships something that can't be + // instantiated. Objects with no c_name leave the C_NAME authoritative. + static_assert( + avnd::c_name_matches("@AVND_C_NAME@"), + "avendish: the CMake C_NAME does not match this object's halp_meta(c_name, ...). " + "Make the avnd_addon_object / avnd_make_* C_NAME equal the object's c_name."); + return golden::run(argc > 1 ? argv[1] : std::string_view{"golden.json"}); } // clang-format on diff --git a/include/avnd/wrappers/metadatas.hpp b/include/avnd/wrappers/metadatas.hpp index 42181470..6ec8537d 100644 --- a/include/avnd/wrappers/metadatas.hpp +++ b/include/avnd/wrappers/metadatas.hpp @@ -318,6 +318,21 @@ static constexpr auto get_static_symbol() return avnd::get_c_identifier(); } +// True unless the object declares an explicit c_name (halp_meta(c_name, ...)) that +// differs from the external's CMake C_NAME. The per-backend prototypes call this so +// a C_NAME that disagrees with the object's own c_name is a compile error instead of +// an external whose file name and registered class name silently differ (which Pd and +// Max load by, so it can't be instantiated). Objects with no c_name meta leave the +// CMake C_NAME authoritative and always pass. +template +consteval bool c_name_matches(std::string_view cmake_c_name) +{ + if constexpr(avnd::has_c_name) + return avnd::get_c_name() == cmake_c_name; + else + return true; +} + template From 66eff6a27fa2a44a134f8d46d95738c95b90cc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Thu, 9 Jul 2026 16:16:42 -0400 Subject: [PATCH 3/3] examples: align SampleAccurateControls C_NAME with its c_name() The object declares c_name() == "avnd_sampleaccurate" but was built as C_NAME avnd_sample_accurate_controls, which the golden static_assert rejects. Use the object's declared name. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014Z4KK2Rays9dTj8J2AJcFZ --- cmake/avendish.examples.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/avendish.examples.cmake b/cmake/avendish.examples.cmake index ccb83e5c..d4286f08 100644 --- a/cmake/avendish.examples.cmake +++ b/cmake/avendish.examples.cmake @@ -238,7 +238,7 @@ avnd_make_all( TARGET SampleAccurateControls MAIN_FILE examples/Raw/SampleAccurateControls.hpp MAIN_CLASS examples::SampleAccurateControls - C_NAME avnd_sample_accurate_controls + C_NAME avnd_sampleaccurate # must match SampleAccurateControls::c_name() ) avnd_make_all(