Skip to content

Backend fixes: Pd message-object hot inlet, Python stable ABI, Max CRT guard#181

Merged
jcelerier merged 4 commits into
mainfrom
fix/pd-hot-inlet-python-abi
Jul 22, 2026
Merged

Backend fixes: Pd message-object hot inlet, Python stable ABI, Max CRT guard#181
jcelerier merged 4 commits into
mainfrom
fix/pd-hot-inlet-python-abi

Conversation

@jcelerier

@jcelerier jcelerier commented Jul 22, 2026

Copy link
Copy Markdown
Member

Fixes to the Pd, Python and Max backends. Validated with a full local build of a 26-object add-on across all three backends (Windows / MSVC, CPython 3.13).

Pd — restore the message object's hot inlet

A message object's leftmost inlet is hot: a message sent to it should set the first parameter and run a processing pass so the object emits output (the Max backend now does the same). process_inputs() was consuming every parameter message — the first inlet's included — as set-only and returning before default_process() could run the object, so the hot inlet produced nothing. process_inputs() gains a skip_first flag so a message to the first parameter falls through to default_process() (set + process + output); cold inlets stay set-only.

Validated: a message to inlet 0 with no trailing bang now produces output; the pre-fix build produces nothing.

Max — register objects under the external filename, and run the hot inlet

Two issues that left most Max objects unusable:

  • Loading: ext_main registered the class under the object's c_name() metadata, but Max looks an object box up by the external's filename. When they differ the file loads yet no class of that name exists, so the object never instantiates. The external's C_NAME is now passed into the metaclasses (as Pd already does) and used for class_new; the audio and jitter metaclasses take the same parameter so ext_main passes it uniformly.
  • Hot inlet: a named message on inlet 0 was consumed as set-only, returning before the object ran, so a [Name v1 v2( message produced no output. It now runs a processing pass after setting, like the other inlet-0 paths.

Python — opt-in stable-ABI build; use Development.Module

Adds an opt-in stable-ABI build (AVND_PYTHON_STABLE_ABI, OFF by default) that links the limited API (Py_LIMITED_API, python3.dll) via Python_add_library(USE_SABI). It is off by default because pybind11 uses the full CPython C API and does not compile under Py_LIMITED_API, so the stable-ABI path is only usable with a limited-API-capable binding; when enabled it needs the Development.SABIModule component to link python3.lib, and the decision is cached so avnd_make_python() (called from the consumer's scope) sees it. The default keeps the working version-specific build; switching from full Development to Development.Module also stops the module linking the interpreter library.

Validated: stable ABI on → link switches to python3.lib but pybind11 fails to compile (as noted); default version-specific build imports cleanly on the CPython it is built against (26/26 on 3.13).

Max — warn when the static-CRT request is silently dropped

MSVC_RUNTIME_LIBRARY (/MT, required by the Max SDK) only takes effect when CMP0091 is NEW in the scope creating the target, which a function here cannot change. Emit a warning at target creation when it is not NEW, so a consuming project that pins an older policy — and would silently ship /MD externals that crash Max — is caught. (The corresponding consumer-side fix is to require CMake >= 3.15.)

🤖 Generated with Claude Code

jcelerier and others added 3 commits July 22, 2026 15:25
A message object's leftmost inlet is hot: a message sent to it must set
the first parameter and immediately run the object so it emits output
(this is what the Max backend already does via proxy_getinlet). process_inputs()
was consuming every parameter message -- including the first inlet's -- as
set-only and returning before default_process() could run the object, so the
hot inlet went silent.

Give process_inputs() a skip_first flag, set by the message processor, so a
message addressed to the first parameter falls through to default_process()
(set + process + output) while the cold inlets (params 1..n, with their own
Pd inlets) stay set-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Version-specific extension modules only import on the exact CPython they were
built against (python3XY.dll). Add an opt-in stable-ABI build
(AVND_PYTHON_STABLE_ABI) linking the limited API / stable ABI (Py_LIMITED_API,
python3.dll) through Python_add_library(USE_SABI), for one module that loads on
any CPython >= the configured version.

It is 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 is only usable with a limited-API-capable
binding. When enabled it is wired correctly: it needs the Development.SABIModule
component to link python3.lib, and the decision is cached so avnd_make_python()
-- called from the consumer's scope -- sees it. The default keeps the working
version-specific build, which loads on the Python it is built for; switching
from full Development to Development.Module also stops the module linking the
interpreter library.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Max externals must link the static CRT (/MT); the MSVC_RUNTIME_LIBRARY
property only delivers that when policy CMP0091 is NEW in the scope that
creates the target, which a function in this module cannot change. If the
consuming project pins an older policy (cmake_minimum_required < 3.15, or an
explicit OLD), the request is dropped and the external builds /MD, which
corrupts Max's heap on load.

Emit a warning at Max-target creation when CMP0091 is not NEW, so the
misconfiguration is caught rather than shipped as a broken external.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jcelerier
jcelerier force-pushed the fix/pd-hot-inlet-python-abi branch from 4641580 to 5ca1f26 Compare July 22, 2026 19:25
Two fixes to the Max message backend that left most objects unusable.

Registration: ext_main registered the class under the object's c_name()
metadata, but Max looks an object box up by the external's filename. When the
two differ the file loads yet no class of that name exists, so the object never
instantiates. Pass the external's C_NAME into the metaclasses -- as the Pd
backend already does -- and register under it; the audio and jitter metaclasses
take the same parameter so ext_main passes it uniformly.

Hot inlet: a message matching a port name on the leftmost (hot) inlet was
consumed by process_inputs() as set-only, returning before the object ran, so a
[Name v1 v2( message set the value but produced no output. Run a processing pass
after setting, matching the other inlet-0 paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jcelerier
jcelerier merged commit 6d6ac70 into main Jul 22, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant