Skip to content

Use tag types to generate attribute specific internals and avoid ODR violations - #493

Merged
DavisVaughan merged 4 commits into
mainfrom
fix/noreturn-template-initialization
May 5, 2026
Merged

DavisVaughan merged 4 commits into
mainfrom
fix/noreturn-template-initialization

Conversation

@DavisVaughan

@DavisVaughan DavisVaughan commented May 5, 2026

Copy link
Copy Markdown
Member

Closes #491
Closes #422 (because all this did was change the warning() signature to something that no longer conflicted with stop(), so it didn't fix the underlying problem)
This was the actual solution for #295

My inline documentation fully describes the problem, so I will regurgitate it here

// Tag types to force templated `struct closure` and `apply()` infrastructure shared
// across `struct function` and `struct noreturn_function` to generate different
// attribute specific `struct closure` and `apply()` variants.
//
// Consider:
//
// ```
// cpp11::stop("error: %s", message)
// cpp11::warning("warning: %s", message)
// ```
//
// These both end up constructing the exact same templated `struct closure` and `apply()`
// functions. The `args` for the underlying `Rf_errorcall()` and `Rf_warningcall()` are:
// - `R_NilValue`
// - `const char* fmt`
// - `const char* message`
//
// The only difference is that `cpp11::stop()` is marked as `[[noreturn]]` because the
// underlying `Rf_errorcall()` is also marked as `[[noreturn]]` /
// `__attribute__((noreturn))`.
//
// But this causes issues! Due to C++'s ODR (One Definition Rule), only 1 variant of
// `apply()` and `struct closure` can be created per template combination. If the
// `cpp11::stop()` variant is linked in first, then some compilers use the `[[noreturn]]`
// hint on `cpp11::stop()` and `operator()` of `noreturn_function` to assert that the
// `apply()` function also cannot return, and returning is deemed unreachable. So then
// when `cpp11::warning()` tries to return from its call to `apply()`, a crash occurs. We
// see this output under ASAN: `execution reached an unreachable program point`.
//
// We've seen this issue on macOS and Linux under clang (gcc does not seem to reproduce
// this). To reproduce, you must have `cpp11::stop()` and `cpp11::warning()` calls in
// different translation units / files and the file containing `cpp11::stop()` must be
// linked first. Putting it first alphabetically seems to be enough, which is why we have
// `template-1-stop.cpp` and `template-2-warn.cpp` in our tests, along with
// `test-template.R` to test this exact issue. You also need to compile with `-O0`,
// otherwise you'll just get a hang rather than a crash.
//
// Adding the tag into the template definition forces `safe[fn]()` and
// `safe.noreturn[fn]()` calls to generate different `apply()` variants, avoiding this
// issue.
//
// https://github.com/r-lib/cpp11/issues/491
// https://github.com/r-lib/cpp11/issues/295
struct return_tag {};
struct no_return_tag {};

@DavisVaughan
DavisVaughan merged commit cd33947 into main May 5, 2026
16 checks passed
@DavisVaughan
DavisVaughan deleted the fix/noreturn-template-initialization branch May 5, 2026 18:26
@DavisVaughan

Copy link
Copy Markdown
Member Author

Some additional prior issues / prs related to this

#85
#87

My theory is that these just didn't go far enough in making sure the internals were also attribute specific

@DavisVaughan

Copy link
Copy Markdown
Member Author

@jimhester - @jennybc and I thought you'd enjoy the resolution to this gnarly issue that has caused cpp11::warning() to crash for unexplainable reasons over the years

krlmlr added a commit to krlmlr/cpp11 that referenced this pull request Sep 12, 2026
Upstream fixed the root cause in r-lib#493,
so this patch has nothing left to do
and the conflict resolves to removing the feature.

This branch worked around r-lib#295 --
`cpp11::warning(const char*)` crashed where
`cpp11::warning(const std::string&)` did not --
by deleting the `const char*` overloads
and taking the format argument by value,
routing every caller through the path that did not crash.

r-lib#493 removes the cause rather than the symptom:
`Rf_errorcall()` and `Rf_warningcall()` share a function type,
so `stop()` and `warning()` collapsed into one
`detail::closure` / `detail::apply()` instantiation,
and the `[[noreturn]]` flavour could render `warning()`'s
return path unreachable.
Upstream now tags the templates with `detail::return_tag`
and `detail::no_return_tag`,
and ships a regression test that calls
`cpp11::warning("%s", "warning")` -- the `const char*` overload --
from a translation unit separate from one calling `cpp11::stop()`,
which is exactly the r-lib#295 scenario.
Its NEWS bullet cites r-lib#295 by number.

Keeping the workaround would now cost something for nothing:
it removes public `const char*` overloads from the header
and constructs a `std::string` on every warning call.

The upstream pull request carrying this patch,
r-lib#422, was closed unmerged on the day r-lib#493 landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8MneV8KqHYUuC8fWV3X5Q
krlmlr added a commit to krlmlr/cpp11 that referenced this pull request Sep 12, 2026
Upstream implemented the feature itself in r-lib#493's sibling
r-lib#492, so the conflict resolves to removing this patch.

This branch vectorized `cpp_source()`'s `file` argument:
`all(file.exists(file))` instead of `file.exists(file)`,
`vapply(file, generate_cpp_name, ...)` instead of one name,
and a shared-library name derived from the first file
rather than the last.

r-lib#492 teaches `cpp_source()` to source multiple files
outright, and covers each of those points:
it checks `all(file.exists(file))` and reports the first missing path,
maps `generate_cpp_name()` over `file` with `vcapply()`,
and derives the library name from `generate_package_name()`,
which sidesteps the naming problem this patch worked around.
It also adds fixtures and a test, which this patch never had.

Davis Vaughan closed the upstream pull request carrying this patch,
r-lib#337, with "Closed via r-lib#492 for a similar debugging use case".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8MneV8KqHYUuC8fWV3X5Q
krlmlr added a commit to krlmlr/cpp11 that referenced this pull request Sep 12, 2026
Three files conflicted,
and only one of them belongs to this branch.

`R/source.R` and `inst/include/cpp11/protect.hpp` conflicted because
this branch was cut from an older integration branch
and carries five squash commits belonging to other patches.
Those are resolved to upstream's version:
they are not this patch's to carry,
and both of the patches behind them are themselves superseded
(r-lib#492 and r-lib#493).

`R/vendor.R` is the real conflict.
Upstream reformatted `cpp_vendor()` with Air,
so the `date` and `overwrite` arguments are re-applied
on top of that new shape rather than reverting it.

`man/cpp_vendor.Rd` and the roxygen block gain the two `@param`
entries the original patch never wrote.
Without them `R CMD check` reports undocumented arguments,
so the branch could not have passed a check as it stood.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8MneV8KqHYUuC8fWV3X5Q
@krlmlr

krlmlr commented Sep 12, 2026

Copy link
Copy Markdown
Member

Wonderful, thanks for looking into this, Davis!

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.

Behavior change in cpp11::warning(fmt, args...) between 0.5.3 and 0.5.4 (clang only)

2 participants