Skip to content

Commit c4cacba

Browse files
jkaliasclaude
andauthored
Add compile-time and runtime checks for reflectable record layout (#35)
* Reject polymorphic reflectable records and document the layout constraint (#21) Member offsets are derived by OffsetFromStart (reinterprets the leading bytes of a pointer-to-member as a byte offset) and DEFINE_MEMBER (offsetof(struct REFLECTABLE, R)). Both are only valid for simple, non-inherited, non-polymorphic structs; a record that violates this (e.g. given a virtual function or a base class) computes wrong offsets silently - data corruption, not a compile or runtime error. Add static_assert(!std::is_polymorphic<REFLECTABLE>::value, ...) right after the macro-generated struct definition in include/reflection.h. This is the portable, always-safe guard: unlike std::is_standard_layout, it doesn't depend on whether std::wstring/TimePoint happen to be standard-layout on a given standard library (verified true on libstdc++ here; unverified on libc++/MSVC), so it can't fail to compile for a currently-valid record on any supported platform. Verified directly: a record with a FUNC(virtual ...) declaration - the realistic way a user would introduce a vtable into a REFLECTABLE struct - now fails to compile with a clear message, where previously offsetof would have only emitted a -Winvalid-offsetof warning and silently computed nonsense. Also verified (Linux/GCC/libstdc++, C++11 and C++20) that is_standard_layout does currently hold for wstring, TimePoint, and all four representative test records, matching the issue's measurement - but since that's unverified on libc++ (macOS) and MSVC (Windows) and this environment can't build for those toolchains, the stronger is_standard_layout assert is deferred to a separate, easily-isolated follow-up commit so CI can decide per-platform rather than guessing. Document the constraint in README.md ("Defining records") and with a comment block next to the REFLECTABLE struct definition. Add tests/reflection_test.cc with static_assert-based compile-time checks (plus a mirroring runtime test) that Person/Pet/Company/DatetimeContainer - one representative of each storage class - satisfy !is_polymorphic, to guard against regression. Full suite passes locally on Ubuntu/GCC in both C++11 and C++20 (75/75 tests); this is itself part of the proof the guard doesn't reject any currently-valid record. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Attempt the stronger is_standard_layout guard, isolated for CI (#21) Adds static_assert(std::is_standard_layout<REFLECTABLE>::value, ...) immediately after the polymorphic guard, as its own commit so a CI failure on any platform isolates cleanly to this one static_assert. is_standard_layout on REFLECTABLE is only true if std::wstring and TimePoint both happen to be standard-layout on the active standard library, which is implementation-defined. Verified true here on Ubuntu/GCC/libstdc++ in both C++11 and C++20 (matching the issue's own measurement), for all four representative test records (Person, Pet, Company, DatetimeContainer). NOT verified in this environment on libc++ (macOS) or MSVC (Windows) - this sandbox can only build for Linux/GCC. If CI is green on macOS and Windows (both C++11 and C++20) for this commit, #21 is fully closed. If it fails on either, revert just this commit, keeping the polymorphic guard from the prior commit, and note here that the standard-layout assert is blocked pending #25 (moving the text representation off std::wstring). Also adds a mirroring ReflectionTest.ReflectableRecordsAreStandardLayout runtime test alongside the compile-time check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Revert the is_standard_layout guard: fails on MSVC (#21) CI confirms static_assert(std::is_standard_layout<REFLECTABLE>::value, ...) fails to compile on Windows Latest MSVC, in both C++11 and C++20, for every existing test record - std::wstring and/or sqlite_reflection::TimePoint are not standard-layout under MSVC's standard library. It compiled cleanly on Ubuntu/GCC/libstdc++ and macOS/Clang/libc++ (both C++11 and C++20). This confirms the exact risk the issue called out and the reason this guard was committed separately from the polymorphic one: is_standard_layout on a record is implementation-defined (true only if every member type, including wstring/TimePoint, is itself standard-layout), so it cannot be portably enforced today without breaking a real, currently-valid record on a supported compiler. Revert the assert and its mirroring test, leaving a comment recording what was tried and why, and keep only the portable static_assert(!std::is_polymorphic<REFLECTABLE>::value, ...) from the prior commit. #21 is closed by the polymorphic guard; the standard-layout strengthening is deferred pending #25 (moving the text representation off std::wstring). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 11812e4 commit c4cacba

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ Supported field macros:
135135
| `sqlite_reflection::TimePoint` | `MEMBER_DATETIME(name)` |
136136
| member function declaration | `FUNC(signature)` |
137137

138+
**Layout constraint.** Reflectable records must be simple, standard-layout structs: no base
139+
classes, no virtual functions, no virtual/multiple inheritance. Member access is computed from
140+
`offsetof`/pointer-to-member byte offsets, which are only well-defined for such types; a struct
141+
outside these bounds is rejected at compile time via a `static_assert` if it's polymorphic (has a
142+
vtable), but other standard-layout violations are not otherwise detectable across all supported
143+
compilers and would silently compute wrong member offsets instead of failing to compile. Stick to
144+
plain data members declared through the `MEMBER_*` macros and you're always within these bounds.
145+
138146
Make sure each reflected record header is included by your program before `Database::Initialize()` is called. During initialization, the library creates one table for each registered record type if that table does not already exist.
139147

140148
## Opening and closing the database

include/reflection.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <map>
3030
#include <stdexcept>
3131
#include <string>
32+
#include <type_traits>
3233
#include <typeinfo>
3334
#include <vector>
3435

@@ -153,6 +154,11 @@ REFLECTION_EXPORT char* GetMemberAddress(void* p, const Reflection& record, size
153154
#pragma warning(push)
154155
#pragma warning(disable : 4002) // "too many actual parameters for macro 'MEMBER'"
155156

157+
/// Reflectable records must be simple, standard-layout structs: no base classes, no virtual
158+
/// functions, no virtual inheritance. Member access is computed via offsetof/pointer-to-member
159+
/// byte offsets (see OffsetFromStart and DEFINE_MEMBER above), which only give correct answers
160+
/// for such types; a struct outside these bounds gets WRONG member offsets silently (data
161+
/// corruption, not a compile or runtime error) unless caught by the static_assert below.
156162
struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
157163
// member declaration according to the order given in source code
158164
#define MEMBER_DECLARE(L, R) L R;
@@ -188,6 +194,24 @@ struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
188194
#undef FUNC
189195
};
190196

197+
// Reject the realistic footgun that actually breaks OffsetFromStart's pointer-to-member byte
198+
// hack and offsetof's standard-layout requirement: giving a record a vtable via a virtual
199+
// function or virtual/multiple inheritance. This does not depend on standard-library string
200+
// layout (a struct with wstring members is never polymorphic on its own), so it holds on every
201+
// supported compiler/platform.
202+
static_assert(!std::is_polymorphic<REFLECTABLE>::value,
203+
"sqlite-reflection: reflectable records must not be polymorphic "
204+
"(no virtual functions or virtual/multiple inheritance).");
205+
206+
// A stronger static_assert(std::is_standard_layout<REFLECTABLE>::value, ...) was attempted here
207+
// and confirmed via CI to fail to compile on MSVC (Windows), in both C++11 and C++20, for the
208+
// existing test records - std::wstring and/or sqlite_reflection::TimePoint are not
209+
// standard-layout on that standard library. It compiled fine on GCC/libstdc++ (Linux) and
210+
// Clang/libc++ (macOS). Since is_standard_layout is implementation-defined and this project
211+
// supports MSVC, that guard is not enforceable portably today; it's deferred pending #25 (moving
212+
// the text representation off std::wstring). The polymorphic guard above remains the enforced,
213+
// portable constraint.
214+
191215
/// Provide a static registration function for each reflectable struct
192216
static std::string CAT(Register, REFLECTABLE)() {
193217
std::string type_id = typeid(REFLECTABLE).name();

tests/reflection_test.cc

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2026 Ioannis Kaliakatsos
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
#include "reflection.h"
24+
25+
#include <gtest/gtest.h>
26+
27+
#include <type_traits>
28+
29+
#include "company.h"
30+
#include "datetime_container.h"
31+
#include "person.h"
32+
#include "pet.h"
33+
34+
using namespace sqlite_reflection;
35+
36+
// Reflectable records must not be polymorphic (see include/reflection.h's static_assert next to
37+
// the REFLECTABLE struct definition). These compile-time checks pin that guarantee for every
38+
// storage class the test records exercise (TEXT/wstring, INT, REAL, BOOL, DATETIME/TimePoint),
39+
// so a regression that reintroduces a virtual function/inheritance into the macro-generated
40+
// struct fails the build here rather than silently corrupting member offsets at runtime.
41+
static_assert(!std::is_polymorphic<Person>::value, "Person must not be polymorphic");
42+
static_assert(!std::is_polymorphic<Pet>::value, "Pet must not be polymorphic");
43+
static_assert(!std::is_polymorphic<Company>::value, "Company must not be polymorphic");
44+
static_assert(!std::is_polymorphic<DatetimeContainer>::value, "DatetimeContainer must not be polymorphic");
45+
46+
// Mirrors the static_asserts above as ordinary runtime expectations, so the guarantee is also
47+
// visible in normal test output rather than only enforced silently at compile time.
48+
//
49+
// A stronger ReflectableRecordsAreStandardLayout test (mirroring
50+
// static_assert(std::is_standard_layout<REFLECTABLE>::value, ...)) was attempted alongside this
51+
// one and confirmed via CI to fail on MSVC (Windows), in both C++11 and C++20 - std::wstring
52+
// and/or TimePoint are not standard-layout on that standard library - so it was removed; see the
53+
// comment in include/reflection.h next to the REFLECTABLE struct definition.
54+
TEST(ReflectionTest, ReflectableRecordsAreNotPolymorphic) {
55+
EXPECT_FALSE(std::is_polymorphic<Person>::value);
56+
EXPECT_FALSE(std::is_polymorphic<Pet>::value);
57+
EXPECT_FALSE(std::is_polymorphic<Company>::value);
58+
EXPECT_FALSE(std::is_polymorphic<DatetimeContainer>::value);
59+
}

0 commit comments

Comments
 (0)