Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmake/avendish.max.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ function(avnd_make_max)
set_source_files_properties("${CMAKE_BINARY_DIR}/${MAIN_OUT_FILE}_max.cpp" PROPERTIES COMPILE_FLAGS -Wno-unreachable-code)
endif()

# Max externals must link the static CRT (/MT), which the MSVC_RUNTIME_LIBRARY
# property below only delivers when CMP0091 is NEW in the scope that creates
# the target -- i.e. in the consumer's CMakeLists, which a function here cannot
# change. If the consumer pins an older policy (cmake_minimum_required < 3.15,
# or an explicit OLD), the request is silently ignored and the external ships
# /MD, which corrupts Max's heap at load. Warn loudly rather than fail quietly.
if(MSVC)
cmake_policy(GET CMP0091 _avnd_cmp0091)
if(NOT _avnd_cmp0091 STREQUAL "NEW")
message(WARNING
"Avendish Max backend: policy CMP0091 is not NEW in this scope, so the "
"static-CRT (/MT) request for '${AVND_C_NAME}' will be ignored and the "
"external will build /MD -- which crashes Max on load. Raise the "
"consuming project's cmake_minimum_required to >= 3.15 (3.25 recommended) "
"or set(CMP0091 NEW) before creating targets.")
endif()
endif()

set(AVND_FX_TARGET "${AVND_TARGET}_max")
add_library(${AVND_FX_TARGET} MODULE)

Expand Down
103 changes: 65 additions & 38 deletions cmake/avendish.python.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,48 @@ if(CMAKE_SYSTEM_NAME MATCHES "WAS.*")
return()
endif()

find_package(Python COMPONENTS Interpreter Development GLOBAL)
# Opt-in stable-ABI build: one module that loads on any CPython >= the version
# below (Py_LIMITED_API / python3.dll) instead of a version-locked build that
# only imports on the exact interpreter it was compiled against (python3XY.dll).
#
# OFF by default: pybind11 uses the full CPython C API (PyFrameObject /
# PyCodeObject internals, PyList_GET_ITEM, ...) and does not compile under
# Py_LIMITED_API, so the stable-ABI path only works with a limited-API-capable
# binding. When on and available it is wired correctly (links python3.lib); the
# default keeps the working version-specific build. Build the module against the
# Python version you ship for.
option(AVND_PYTHON_STABLE_ABI "Build Python modules against the stable ABI (needs a limited-API-capable binding; pybind11 is not)" OFF)
set(AVND_PYTHON_SABI_VERSION "3.12" CACHE STRING "Minimum CPython version for the stable-ABI build (Python_add_library USE_SABI value)")

# Development.Module (not full Development) is what a loadable extension needs.
# Development.SABIModule (CMake >= 3.26) additionally provides the stable-ABI
# import library that Python_add_library(USE_SABI) links against; without it
# USE_SABI silently falls back to the version-specific library.
find_package(Python COMPONENTS Interpreter Development.Module
OPTIONAL_COMPONENTS Development.SABIModule GLOBAL)
if(NOT Python_Development.Module_FOUND)
find_package(Python COMPONENTS Interpreter Development GLOBAL)
endif()
find_package(pybind11 CONFIG GLOBAL)
if(NOT TARGET pybind11::module)
if(NOT TARGET pybind11::headers)
function(avnd_make_python)
endfunction()

return()
endif()
# A true stable-ABI build needs Python_add_library(USE_SABI ...) so the module
# links the stable import library (python3.lib -> python3.dll) rather than the
# version-specific one; that command is available from CMake 3.26.
# Stored in the cache so avnd_make_python() sees it: that function is called from
# the consumer's scope, which the plain variable set here does not reach.
set(_avnd_python_can_sabi FALSE CACHE INTERNAL "")
if(AVND_PYTHON_STABLE_ABI AND Python_Development.SABIModule_FOUND
AND NOT CMAKE_VERSION VERSION_LESS "3.26")
set(_avnd_python_can_sabi TRUE CACHE INTERNAL "")
endif()

function(avnd_make_python)
if(NOT TARGET pybind11::module)
if(NOT TARGET pybind11::headers)
return()
endif()

Expand All @@ -37,45 +69,40 @@ function(avnd_make_python)
)

set(AVND_FX_TARGET "${AVND_TARGET}_python")
add_library(${AVND_FX_TARGET} SHARED)
set(_srcs
"${AVND_MAIN_FILE}"
"${CMAKE_BINARY_DIR}/${MAIN_OUT_FILE}_python.cpp")

if(WIN32)
set_target_properties(
${AVND_FX_TARGET}
PROPERTIES
OUTPUT_NAME "py${AVND_C_NAME}"
PREFIX ""
SUFFIX ".pyd"
LIBRARY_OUTPUT_DIRECTORY python
RUNTIME_OUTPUT_DIRECTORY python
)
if(_avnd_python_can_sabi)
# WITH_SOABI stamps the extension suffix (.abi3.pyd / .abi3.so); USE_SABI
# compiles with Py_LIMITED_API and links the stable import lib. The result
# imports on any CPython >= AVND_PYTHON_SABI_VERSION.
Python_add_library(${AVND_FX_TARGET} MODULE WITH_SOABI USE_SABI "${AVND_PYTHON_SABI_VERSION}"
${_srcs})
set_target_properties(${AVND_FX_TARGET} PROPERTIES
OUTPUT_NAME "py${AVND_C_NAME}"
LIBRARY_OUTPUT_DIRECTORY python
RUNTIME_OUTPUT_DIRECTORY python)
target_link_libraries(${AVND_FX_TARGET} PRIVATE Avendish::Avendish_python pybind11::headers)
else()
# CPython convention: py<name>.so on both Linux and macOS, no lib prefix.
set_target_properties(
${AVND_FX_TARGET}
PROPERTIES
OUTPUT_NAME "py${AVND_C_NAME}"
PREFIX ""
SUFFIX ".so"
LIBRARY_OUTPUT_DIRECTORY python
RUNTIME_OUTPUT_DIRECTORY python
)
# Classic version-specific module (imports python3XY.dll). Used when the
# stable ABI is disabled or unavailable (CMake < 3.26 / no Development.Module).
add_library(${AVND_FX_TARGET} SHARED ${_srcs})
if(WIN32)
set(_pysuffix ".pyd")
else()
# CPython convention: py<name>.so on both Linux and macOS, no lib prefix.
set(_pysuffix ".so")
endif()
set_target_properties(${AVND_FX_TARGET} PROPERTIES
OUTPUT_NAME "py${AVND_C_NAME}"
PREFIX ""
SUFFIX "${_pysuffix}"
LIBRARY_OUTPUT_DIRECTORY python
RUNTIME_OUTPUT_DIRECTORY python)
target_link_libraries(${AVND_FX_TARGET} PRIVATE Avendish::Avendish_python pybind11::module)
endif()

target_sources(
${AVND_FX_TARGET}
PRIVATE
"${AVND_MAIN_FILE}"
"${CMAKE_BINARY_DIR}/${MAIN_OUT_FILE}_python.cpp"
)

target_link_libraries(
${AVND_FX_TARGET}
PRIVATE
Avendish::Avendish_python
pybind11::module
)

avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}")

# Register for avnd_addon_package() (the addon-wide packaging orchestrator).
Expand Down
9 changes: 6 additions & 3 deletions include/avnd/binding/max/audio_processor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ struct audio_processor_metaclass
static inline t_class* g_class{};
static inline audio_processor_metaclass* instance{};

audio_processor_metaclass();
// class_name is the Max class symbol; ext_main passes the external's own
// C_NAME so the registered class matches the file Max loaded.
audio_processor_metaclass(t_symbol* class_name = nullptr);
};

template <typename T>
Expand Down Expand Up @@ -346,7 +348,7 @@ struct audio_processor : processor_common<T>
};

template <typename T>
audio_processor_metaclass<T>::audio_processor_metaclass()
audio_processor_metaclass<T>::audio_processor_metaclass(t_symbol* class_name)
{
audio_processor_metaclass::instance = this;
using instance = audio_processor<T>;
Expand Down Expand Up @@ -421,7 +423,8 @@ audio_processor_metaclass<T>::audio_processor_metaclass()

/// Class creation ///
g_class = class_new(
avnd::get_c_name<T>().data(), (method)obj_new, (method)obj_free,
class_name ? class_name->s_name : avnd::get_c_name<T>().data(),
(method)obj_new, (method)obj_free,
sizeof(audio_processor<T>), 0L, A_GIMME, 0);

class_dspinit(g_class);
Expand Down
11 changes: 9 additions & 2 deletions include/avnd/binding/max/jitter_processor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,13 @@ struct jitter_processor_metaclass

static inline const auto jit_class_name = gensym(fmt::format("jit_avnd_{}", avnd::get_c_name<T>()).c_str());

jitter_processor_metaclass()
// The Max class symbol; ext_main passes the external's own C_NAME so the
// registered class matches the file Max loaded (nullptr -> introspected c_name).
static inline t_symbol* registered_class_name = nullptr;

jitter_processor_metaclass(t_symbol* class_name = nullptr)
{
registered_class_name = class_name;
instance = this;

this->jit_class_init();
Expand Down Expand Up @@ -460,7 +465,9 @@ struct jitter_processor_metaclass
using instance = max_jit_wrapper<T>;
t_class *jclass{};

static const std::string max_class_name = std::string(avnd::get_c_name<T>().data());
static const std::string max_class_name = std::string(
registered_class_name ? registered_class_name->s_name
: avnd::get_c_name<T>().data());

static constexpr auto obj_new = +[](t_symbol* s, long argc, t_atom* argv)
{
Expand Down
18 changes: 14 additions & 4 deletions include/avnd/binding/max/message_processor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ struct message_processor_metaclass
static inline std::array<t_object*, num_attributes> attributes;

static t_symbol* symbol_from_name();
message_processor_metaclass();
// class_name is the Max class symbol; ext_main passes the external's own
// C_NAME so the registered class matches the file Max loaded (nullptr ->
// fall back to the introspected c_name).
message_processor_metaclass(t_symbol* class_name = nullptr);
};

template <typename T>
Expand Down Expand Up @@ -183,9 +186,15 @@ struct message_processor : processor_common<T>
// First try to process messages handled explicitely in the object
if(inlet == 0 && messages<T>{}.process_messages(implementation, s, argc, argv))
return;
// Then try to find if any message matches the name of a port
// Then try to find if any message matches the name of a port. The leftmost
// inlet is hot, so setting a parameter through it also runs a processing
// pass -- otherwise a [Name v1 v2( message sets the value but the object
// never produces output.
if(inlet == 0 && processor_common<T>::process_inputs(this->implementation, s, argc, argv))
{
process();
return;
}

// Then some default behaviour
switch(argc)
Expand Down Expand Up @@ -224,7 +233,7 @@ struct message_processor : processor_common<T>
};

template <typename T>
message_processor_metaclass<T>::message_processor_metaclass()
message_processor_metaclass<T>::message_processor_metaclass(t_symbol* class_name)
{
message_processor_metaclass::instance = this;
using instance = message_processor<T>;
Expand Down Expand Up @@ -284,7 +293,8 @@ message_processor_metaclass<T>::message_processor_metaclass()

/// Class creation ///
g_class = class_new(
avnd::get_c_name<T>().data(), (method)obj_new, (method)obj_free,
class_name ? class_name->s_name : avnd::get_c_name<T>().data(),
(method)obj_new, (method)obj_free,
sizeof(message_processor<T>), 0L, A_GIMME, 0);

// Connect our methods
Expand Down
13 changes: 9 additions & 4 deletions include/avnd/binding/max/prototype.cpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,30 @@ extern "C" AVND_EXPORTED_SYMBOL void ext_main(void* r)
{
common_symbols_init();

[]<typename T = type> {
// Register the class under the external's own name (the file Max just loaded
// is @AVND_C_NAME@.mxe64/.mxo). Max looks up an object box by that filename,
// so the registered class symbol must match it exactly; the class's own
// c_name() metadata may differ, which would leave the object unloadable.
t_symbol* const avnd_class_name = gensym("@AVND_C_NAME@");
[avnd_class_name]<typename T = type> {
if constexpr(avnd::audio_processor<T>)
{
// If we process audio in any shape -- effect, generator or analyzer
// (audio in, control out: NOT covered by the mono/polyphonic effect
// concepts, which require matching in/out shapes) -- make a type with
// the whole DSP stuff.
static const max::audio_processor_metaclass<T> instance{};
static const max::audio_processor_metaclass<T> instance{avnd_class_name};
}
else if constexpr(max::max_jit_input_introspection<T>::size > 0
|| max::max_jit_output_introspection<T>::size > 0
)
{
static const max::jitter_processor_metaclass<T> instance{};
static const max::jitter_processor_metaclass<T> instance{avnd_class_name};
}
else
{
// Simpler case which just processes messages
static const max::message_processor_metaclass<T> instance{};
static const max::message_processor_metaclass<T> instance{avnd_class_name};
}
}();
}
Expand Down
10 changes: 9 additions & 1 deletion include/avnd/binding/pd/inputs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,14 @@ struct inputs
return false;
}

// skip_first leaves the leftmost (hot) inlet's parameter to the caller: in a
// message object that inlet is hot, so a message addressed to it must set the
// value AND run a processing pass, which the caller does through
// default_process. The cold inlets (params 1..n, which have their own Pd
// inlets) are still handled here as set-only.
bool process_inputs(
avnd::effect_container<T>& implementation, t_symbol* s, int argc, t_atom* argv)
avnd::effect_container<T>& implementation, t_symbol* s, int argc, t_atom* argv,
bool skip_first = false)
{
// FIXME create static pointer tables instead
if constexpr(avnd::parameter_input_introspection<T>::size > 0)
Expand All @@ -461,6 +467,8 @@ struct inputs
const int idx = param_idx++;
if(ok)
return;
if(skip_first && idx == 0)
return;
bool matches = false;
if constexpr(pd::has_usable_name<M>())
{
Expand Down
9 changes: 8 additions & 1 deletion include/avnd/binding/pd/message_processor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,14 @@ struct message_processor
// First try to process messages handled explicitely in the object
if(messages_setup.process_messages(implementation, s, argc, argv))
return;
if(input_setup.process_inputs(implementation, s, argc, argv))

// Cold inlets (the second and following parameters, which have their own Pd
// inlets) only store their value. The leftmost inlet is hot: a message
// addressed to the first parameter -- like raw data or a bang -- falls
// through to default_process, which stores it *and* runs a processing pass
// so the object produces output. skip_first keeps the first parameter out of
// the set-only path here.
if(input_setup.process_inputs(implementation, s, argc, argv, /* skip_first */ true))
return;

// Then some default behaviour
Expand Down
Loading