diff --git a/Makefile b/Makefile index 5c8e35151..03a5f5a85 100644 --- a/Makefile +++ b/Makefile @@ -687,6 +687,8 @@ tables-block: build/schema_test_block build/schema_test_block_asan build/schema_ ./build/schema_test_block ./build/schema_test_block_asan ./build/schema_test_block_tsan + $(MAKE) tables-block-const + $(MAKE) tables-block-const-negative-control # THE C# HALF of this two-language gate runs unless the cs leg is named in # SCHEMA_SKIP_LEGS (issue #599). A skip that reaches the leg loop and not here # is a skip that stops the chain anyway, on the same missing dotnet. @@ -696,6 +698,49 @@ else @echo "tables-block: the C# half is SKIPPED, SCHEMA_SKIP_LEGS names the cs leg" endif +# THE CONST READ PATH (docs/SPEC-TABLES.md §19.2, schema#455). A block is +# memory another build wrote, so the consumer half of the form reads bytes it +# does not own: a pinned image is loaded, held as a `const uint8_t *` from that +# moment, and opened WITH NO CAST through the const overload. The rows come +# back read-only, and the same bytes opened writable read the same values, so +# the producer's path is proved unchanged rather than assumed. +# +# It runs SANITIZED beside the plain build on #277's rule: the const view +# points into a buffer sized to the image, so a row read past the extent lands +# in a redzone. +build/schema_test_block_const: build/tables-generated/.stamp test/tables/block_const_main.cpp + @mkdir -p build + $(CXX) $(BLOCK_CXXFLAGS) $(BLOCK_INCLUDES) test/tables/block_const_main.cpp $(BLOCK_SOURCES) -o $@ + +build/schema_test_block_const_asan: build/tables-generated/.stamp test/tables/block_const_main.cpp + @mkdir -p build + $(CXX) $(BLOCK_CXXFLAGS) -fsanitize=address,undefined -fno-sanitize-recover=all \ + -fno-omit-frame-pointer -g $(BLOCK_INCLUDES) test/tables/block_const_main.cpp $(BLOCK_SOURCES) -o $@ + +.PHONY: tables-block-const +tables-block-const: build/schema_test_block_const build/schema_test_block_const_asan + ./build/schema_test_block_const + ./build/schema_test_block_const_asan + +# ITS NEGATIVE COMPILE CONTROL, and it is the half a run cannot hold: a view +# handing back a reference a consumer can WRITE through is a read-only view in +# name only, and no green run of the gate above would ever say so. The same +# program, one assignment through the const view added by -DBLOCK_CONST_WRITE, +# and the COMPILE must go red, on the const qualification and not on some +# other error, which is what the second grep is for. +.PHONY: tables-block-const-negative-control +tables-block-const-negative-control: build/tables-generated/.stamp test/tables/block_const_main.cpp + @mkdir -p build + @if $(CXX) $(BLOCK_CXXFLAGS) $(BLOCK_INCLUDES) -DBLOCK_CONST_WRITE -fsyntax-only \ + test/tables/block_const_main.cpp > build/block-const-write.log 2>&1; then \ + echo "NEGATIVE CONTROL FAILED: a write through the const row view COMPILED"; exit 1; \ + fi + @grep -qE "read-only|not assignable|const-qualified|assignment of member" build/block-const-write.log || \ + { echo "NEGATIVE CONTROL FAILED: the compile went red, but not on the const view"; \ + cat build/block-const-write.log; exit 1; } + @grep -m1 -E "read-only|not assignable|const-qualified|assignment of member" build/block-const-write.log + @echo "negative control: one write through the const row view turns the COMPILE red" + # --------------------------------------------------------------------------- # THE FORGERY FUZZER (docs/SPEC-TABLES.md §19.2, §19.5). The hand-written battery in # block_main.cpp and Program.cs is eleven forgeries, one per fact BlockOpen diff --git a/docs/SPEC-TABLES.md b/docs/SPEC-TABLES.md index e8f113f4a..6bca5db72 100644 --- a/docs/SPEC-TABLES.md +++ b/docs/SPEC-TABLES.md @@ -13623,6 +13623,26 @@ schema name, as everywhere else in that backend. so that the base's alignment can be the last clause. A block whose bytes are fewer than its prologue answers `truncated`, and a null base `unaligned_base`, on the readings §7 gives those two. + + **THE READ SIDE TAKES A CONST OVERLOAD, and the surface SPLITS the way C#'s + already does.** A block is memory another build wrote, so a consumer opens + bytes it did not produce: it reads them and writes nothing, and a read-only + mapping is not something a loader should have to `const_cast` to open. So + `bool BlockOpen( Block::Const & block, const void * base, + int64_t bytes, TableRefuseReason * reason = NULL )` sits beside the mutable + one. `Block::Const` is a MEMBER TYPE of the handle above and claims no + name of its own (§11); it carries the same three facts with `const` on both + pointers, and the row accessors overloaded on it answer the CONST VIEWS, + `TableBlockConstRows` and `TableBlockConstSpan`, whose iterators, + spans and typed base all hand back `const Row`. **THE CHECK IS ONE BODY**: + both overloads run the same clauses in the same order and name the same + reason on the same refusal, because a check reads and never writes, so the + two paths cannot drift. **The PRODUCER's path is unchanged** in every part: + `Begin`, the fill accessors and the typed base a worker indexes are what + they were, and a producer holds a mutable block exactly as before. The split + is the one C# states above with `ref readonly` and `ReadOnlySpan`, and + the one Rust has by returning `&[Row]`. **A write through a const view is a + COMPILE ERROR**, held by a negative compile control (§19.5). - **An array is ITERATED, not indexed by hand.** The accessor yields a reference to each row where it lies, at the pitch the instance gives, for `count` rows — a range-for in C++, an enumerator in C#, the equivalent per @@ -13835,6 +13855,17 @@ difference between a form and a convention. offset in the compiler's layout model and the generated asserts go red on both backends. A layout test that shares its layout model with the code it checks proves nothing, and these two are what separate them. +- **THE CONST READ PATH, and its NEGATIVE COMPILE CONTROL** (§19.2). A pinned + block image is loaded, held as a `const uint8_t *` from that moment and + opened with no cast through the const overload; its rows are walked through + `TableBlockConstRows` and `TableBlockConstSpan` and compared, value for + value, against the same bytes opened writable, so the two overloads agree + and the producer's path is proved unchanged rather than assumed. The refusal + clauses answer on the const overload too, a null base and a short buffer and + an unaligned base each naming what §19.2 gives it. The CONTROL is the half a + run cannot hold: the same program with one assignment through the const view + must FAIL TO COMPILE, on the const qualification, because a read-only view + whose writes compile is a view in name only. - **A `bool` row.** A row type carrying two `bool`s beside its scalars, whose C# size and offsets are asserted under the managed model (§19.3) — the case where the two C# layout models disagree, pinned so a port cannot pick the diff --git a/generated/bench/tables/cpp/BenchTableBlock.cpp b/generated/bench/tables/cpp/BenchTableBlock.cpp index 925d8e3e4..0f331ad5a 100644 --- a/generated/bench/tables/cpp/BenchTableBlock.cpp +++ b/generated/bench/tables/cpp/BenchTableBlock.cpp @@ -15,11 +15,19 @@ namespace benchtable { // ---- the block form of table TableEntity: the open path and the descriptors ---- -bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +// THE CHECK ITSELF, over a base it never writes through, because a check +// READS (docs/SPEC-TABLES.md §19.2). Both overloads of TableEntityBlockOpen are +// this one body, so the producer's path and the consumer's cannot drift: +// the same clauses in the same order, and the same reason on the same +// refusal, whether the caller handed over bytes it owns or bytes it may +// only read. It answers the USED EXTENT beside the verdict, which is the +// one thing each overload has to write into its own handle. +// +// It sits in an anonymous namespace and claims no name (§11). +namespace { + +bool tableentity_block_open_check( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason ) { - block.base = NULL; - block.projection = NULL; - block.bytes = 0; // a null buffer is the CALLER's defect, as an unaligned base is; a buffer // shorter than the prologue has no prologue to read and is truncated if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; } @@ -49,12 +57,43 @@ bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, // the base's alignment, LAST: the only clause that reads nothing out of // the block, and the caller's defect rather than the file's if ( ( (uintptr_t) base % 64 ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; } + *used_out = used; + return true; +} + +} // namespace + +// THE PRODUCER's overload, unchanged in what it does: the check above, and +// then a handle over bytes the caller may write. +bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !tableentity_block_open_check( base, bytes, &used, reason ) ) { return false; } block.base = (uint8_t *) base; block.projection = (TableEntityBlock::Projection *) base; block.bytes = used; return true; } +// THE CONSUMER's overload (schema#455): the same check, and then a handle +// that carries const the whole way down. A read-only mapping opens through +// it with no cast, and nothing it hands back can be written through. +bool TableEntityBlockOpen( TableEntityBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !tableentity_block_open_check( base, bytes, &used, reason ) ) { return false; } + block.base = (const uint8_t *) base; + block.projection = (const TableEntityBlock::Projection *) base; + block.bytes = used; + return true; +} + namespace { // forward declarations, so the element column can be a constant @@ -86,11 +125,19 @@ const TableBlockInfo * TableEntityBlock::Type() { return &tableentity_block_proj // ---- the block form of table TableStat: the open path and the descriptors ---- -bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +// THE CHECK ITSELF, over a base it never writes through, because a check +// READS (docs/SPEC-TABLES.md §19.2). Both overloads of TableStatBlockOpen are +// this one body, so the producer's path and the consumer's cannot drift: +// the same clauses in the same order, and the same reason on the same +// refusal, whether the caller handed over bytes it owns or bytes it may +// only read. It answers the USED EXTENT beside the verdict, which is the +// one thing each overload has to write into its own handle. +// +// It sits in an anonymous namespace and claims no name (§11). +namespace { + +bool tablestat_block_open_check( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason ) { - block.base = NULL; - block.projection = NULL; - block.bytes = 0; // a null buffer is the CALLER's defect, as an unaligned base is; a buffer // shorter than the prologue has no prologue to read and is truncated if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; } @@ -120,12 +167,43 @@ bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, Tab // the base's alignment, LAST: the only clause that reads nothing out of // the block, and the caller's defect rather than the file's if ( ( (uintptr_t) base % 64 ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; } + *used_out = used; + return true; +} + +} // namespace + +// THE PRODUCER's overload, unchanged in what it does: the check above, and +// then a handle over bytes the caller may write. +bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !tablestat_block_open_check( base, bytes, &used, reason ) ) { return false; } block.base = (uint8_t *) base; block.projection = (TableStatBlock::Projection *) base; block.bytes = used; return true; } +// THE CONSUMER's overload (schema#455): the same check, and then a handle +// that carries const the whole way down. A read-only mapping opens through +// it with no cast, and nothing it hands back can be written through. +bool TableStatBlockOpen( TableStatBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !tablestat_block_open_check( base, bytes, &used, reason ) ) { return false; } + block.base = (const uint8_t *) base; + block.projection = (const TableStatBlock::Projection *) base; + block.bytes = used; + return true; +} + namespace { // forward declarations, so the element column can be a constant diff --git a/generated/bench/tables/cpp/BenchTableBlock.h b/generated/bench/tables/cpp/BenchTableBlock.h index f70b1c64c..ed678a3f6 100644 --- a/generated/bench/tables/cpp/BenchTableBlock.h +++ b/generated/bench/tables/cpp/BenchTableBlock.h @@ -169,6 +169,48 @@ struct TableBlockSpan T & operator[]( int32_t i ) const { return rows[i]; } }; +// THE READ SIDE's own two views, and they are the same two with const on every +// pointer they hand out (docs/SPEC-TABLES.md §19.2). A block is memory another +// build wrote: a consumer opening bytes it received reads them and writes +// nothing, and a read-only mapping cannot be opened through a view that hands +// back a mutable row at all. The producer's pair above is untouched: Begin, +// the fill accessors and the typed base a worker indexes stay exactly what +// they were, and this pair is what the const overload of BlockOpen fills, so +// the split is the one C# already has (ref readonly, ReadOnlySpan). +template +struct TableBlockConstRows +{ + const uint8_t * base = NULL; + int32_t count = 0; + int32_t stride = 0; + + struct iterator + { + const uint8_t * p; + int32_t stride; + const T & operator*() const { return *(const T *) p; } + iterator & operator++() { p += stride; return *this; } + bool operator!=( const iterator & other ) const { return p != other.p; } + }; + + iterator begin() const { return iterator{ base, stride }; } + iterator end() const { return iterator{ base + (ptrdiff_t) count * stride, stride }; } + int32_t size() const { return count; } + operator const T *() const { return (const T *) base; } +}; + +template +struct TableBlockConstSpan +{ + const T * rows = NULL; + int32_t count = 0; + + const T * begin() const { return rows; } + const T * end() const { return rows + count; } + int32_t size() const { return count; } + const T & operator[]( int32_t i ) const { return rows[i]; } +}; + // ---- reflection over a block (docs/SPEC-TABLES.md §8, §19.2) ---- // // The descriptors are the mechanism, and they are what retires a hand-kept @@ -356,6 +398,20 @@ struct TableEntityBlock Projection * projection = NULL; // the projection, at offset 0 int64_t bytes = 0; // the extent in use + // THE CONSUMER's handle: this same block over bytes it may not write + // (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote, + // and a consumer that received it reads it: the const overload of + // TableEntityBlockOpen fills this from a `const void *`, so a read-only + // mapping opens with no cast, and the row accessors overloaded on it + // hand back const rows. It is a MEMBER TYPE and claims no name of its + // own (§11): the producer's handle above is unchanged in every way. + struct Const + { + const uint8_t * base = NULL; // the extent's base, 64-byte aligned + const Projection * projection = NULL; // the projection, at offset 0 + int64_t bytes = 0; // the extent in use + }; + // this table's block descriptors (docs/SPEC-TABLES.md §8, §19.2): constant // data, defined in the .cpp beside this header. static const TableBlockInfo * Type(); @@ -463,6 +519,15 @@ inline int64_t TableEntityBlockBytes( const TableEntityBlock & block ) // build that wrote it takes the wire (§3), which this same table still has. bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL ); +// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A +// block arrives from another language, another process or an mmap of a +// read-only file, and a consumer that only reads should neither need a +// const_cast to open it nor receive write access to it. It is the SAME +// CHECK (one body, the same clauses in the same order, the same reason on +// the same refusal) over a base it never writes through, and it fills the +// const handle, whose row accessors hand back const rows. +bool TableEntityBlockOpen( TableEntityBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL ); + // ---- the block form of table TableEntity: end ---- // ---- the block form of table TableStat (docs/SPEC-TABLES.md §19): begin ---- @@ -539,6 +604,20 @@ struct TableStatBlock Projection * projection = NULL; // the projection, at offset 0 int64_t bytes = 0; // the extent in use + // THE CONSUMER's handle: this same block over bytes it may not write + // (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote, + // and a consumer that received it reads it: the const overload of + // TableStatBlockOpen fills this from a `const void *`, so a read-only + // mapping opens with no cast, and the row accessors overloaded on it + // hand back const rows. It is a MEMBER TYPE and claims no name of its + // own (§11): the producer's handle above is unchanged in every way. + struct Const + { + const uint8_t * base = NULL; // the extent's base, 64-byte aligned + const Projection * projection = NULL; // the projection, at offset 0 + int64_t bytes = 0; // the extent in use + }; + // this table's block descriptors (docs/SPEC-TABLES.md §8, §19.2): constant // data, defined in the .cpp beside this header. static const TableBlockInfo * Type(); @@ -634,6 +713,15 @@ inline int64_t TableStatBlockBytes( const TableStatBlock & block ) // build that wrote it takes the wire (§3), which this same table still has. bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL ); +// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A +// block arrives from another language, another process or an mmap of a +// read-only file, and a consumer that only reads should neither need a +// const_cast to open it nor receive write access to it. It is the SAME +// CHECK (one body, the same clauses in the same order, the same reason on +// the same refusal) over a base it never writes through, and it fills the +// const handle, whose row accessors hand back const rows. +bool TableStatBlockOpen( TableStatBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL ); + // ---- the block form of table TableStat: end ---- } // namespace benchtable diff --git a/internal/codegen/cpptable/block.go b/internal/codegen/cpptable/block.go index 8a0e56fde..a48fdeb40 100644 --- a/internal/codegen/cpptable/block.go +++ b/internal/codegen/cpptable/block.go @@ -154,6 +154,48 @@ struct TableBlockSpan T & operator[]( int32_t i ) const { return rows[i]; } }; +// THE READ SIDE's own two views, and they are the same two with const on every +// pointer they hand out (docs/SPEC-TABLES.md §19.2). A block is memory another +// build wrote: a consumer opening bytes it received reads them and writes +// nothing, and a read-only mapping cannot be opened through a view that hands +// back a mutable row at all. The producer's pair above is untouched: Begin, +// the fill accessors and the typed base a worker indexes stay exactly what +// they were, and this pair is what the const overload of BlockOpen fills, so +// the split is the one C# already has (ref readonly, ReadOnlySpan). +template +struct TableBlockConstRows +{ + const uint8_t * base = NULL; + int32_t count = 0; + int32_t stride = 0; + + struct iterator + { + const uint8_t * p; + int32_t stride; + const T & operator*() const { return *(const T *) p; } + iterator & operator++() { p += stride; return *this; } + bool operator!=( const iterator & other ) const { return p != other.p; } + }; + + iterator begin() const { return iterator{ base, stride }; } + iterator end() const { return iterator{ base + (ptrdiff_t) count * stride, stride }; } + int32_t size() const { return count; } + operator const T *() const { return (const T *) base; } +}; + +template +struct TableBlockConstSpan +{ + const T * rows = NULL; + int32_t count = 0; + + const T * begin() const { return rows; } + const T * end() const { return rows + count; } + int32_t size() const { return count; } + const T & operator[]( int32_t i ) const { return rows[i]; } +}; + // ---- reflection over a block (docs/SPEC-TABLES.md §8, §19.2) ---- // // The descriptors are the mechanism, and they are what retires a hand-kept @@ -448,6 +490,18 @@ func (g *tableGen) emitBlockSurface(bl *ir.BlockLayout) { g.pf(" uint8_t * base = NULL; // the extent's base, 64-byte aligned\n") g.pf(" Projection * projection = NULL; // the projection, at offset 0\n") g.pf(" int64_t bytes = 0; // the extent in use\n") + g.pf("\n // THE CONSUMER's handle: this same block over bytes it may not write\n") + g.pf(" // (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote,\n") + g.pf(" // and a consumer that received it reads it: the const overload of\n") + g.pf(" // %sBlockOpen fills this from a `const void *`, so a read-only\n", name) + g.pf(" // mapping opens with no cast, and the row accessors overloaded on it\n") + g.pf(" // hand back const rows. It is a MEMBER TYPE and claims no name of its\n") + g.pf(" // own (§11): the producer's handle above is unchanged in every way.\n") + g.pf(" struct Const\n {\n") + g.pf(" const uint8_t * base = NULL; // the extent's base, 64-byte aligned\n") + g.pf(" const Projection * projection = NULL; // the projection, at offset 0\n") + g.pf(" int64_t bytes = 0; // the extent in use\n") + g.pf(" };\n") for _, a := range bl.Arrays { field := ir.GoExportName(a.Field.Name) g.pf("\n // %s: the constants this build asserts against. A consumer INDEXES with\n", a.Field.Name) @@ -487,6 +541,17 @@ func (g *tableGen) emitBlockSurface(bl *ir.BlockLayout) { g.pf("// build that wrote it takes the wire (§3), which this same table still has.\n") g.pf("bool %sBlockOpen( %sBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL );\n\n", name, name) + g.pf("// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A\n") + g.pf("// block arrives from another language, another process or an mmap of a\n") + g.pf("// read-only file, and a consumer that only reads should neither need a\n") + g.pf("// const_cast to open it nor receive write access to it. It is the SAME\n") + g.pf("// CHECK (one body, the same clauses in the same order, the same reason on\n") + g.pf("// the same refusal) over a base it never writes through, and it fills the\n") + g.pf("// const handle, whose row accessors hand back const rows.\n") + g.pf("bool %sBlockOpen( %sBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL );\n\n", name, name) + + g.emitBlockConstReadPath(bl) + g.pf("// ---- the block form of table %s: end ----\n\n", name) } @@ -654,6 +719,45 @@ func (g *tableGen) emitBlockFillPath(bl *ir.BlockLayout) { g.pf("// ---- block fill path: end ----\n\n") } +// emitBlockConstReadPath emits the row accessors overloaded on the CONST +// handle (docs/SPEC-TABLES.md §19.2, schema#455): the consumer's half of the +// surface the fill path above gives the producer. +// +// It sits OUTSIDE the fill-path markers on purpose: nothing here is reached at +// fill time, and the conformance refuser reads what lies between them. Each +// accessor is the same one add the producer's is, typed const, so a consumer +// of foreign bytes iterates at the pitch the instance gives and writes +// nothing. The names are the producer's own, an overload rather than a second +// spelling, so a call site written against §19.2 reads the same either way. +func (g *tableGen) emitBlockConstReadPath(bl *ir.BlockLayout) { + name := bl.Table.Name + if len(bl.Arrays) == 0 { + return + } + g.pf("// ---- the block form's CONST read path (docs/SPEC-TABLES.md §19.2) ----\n\n") + for _, a := range bl.Arrays { + field := ir.GoExportName(a.Field.Name) + g.pf("// %s, over a block a consumer RECEIVED: the same one add, the same pitch\n", a.Field.Name) + g.pf("// out of the instance, and a row reference the caller cannot write\n") + g.pf("// through. A producer holds the mutable overload above; a consumer of\n") + g.pf("// foreign bytes holds this one.\n") + g.pf("inline TableBlockConstRows<%s> %s%s( const %sBlock::Const & block )\n{\n", a.ElemName, name, field, name) + g.pf(" TableBlockConstRows<%s> rows;\n", a.ElemName) + g.pf(" rows.base = block.base + block.projection->%s.offset_of;\n", a.Field.Name) + g.pf(" rows.count = (int32_t) block.projection->%s.count;\n", a.Field.Name) + g.pf(" rows.stride = (int32_t) block.projection->%s.stride;\n", a.Field.Name) + g.pf(" return rows;\n}\n\n") + + g.pf("// %s, as a CONTIGUOUS const view, available because the pitch IS sizeof\n", a.Field.Name) + g.pf("// (§2.7), which is how a consumer's fast path is actually written.\n") + g.pf("inline TableBlockConstSpan<%s> %s%sSpan( const %sBlock::Const & block )\n{\n", a.ElemName, name, field, name) + g.pf(" TableBlockConstSpan<%s> span;\n", a.ElemName) + g.pf(" span.rows = (const %s *) ( block.base + block.projection->%s.offset_of );\n", a.ElemName, a.Field.Name) + g.pf(" span.count = (int32_t) block.projection->%s.count;\n", a.Field.Name) + g.pf(" return span;\n}\n\n") + } +} + // blockStartAlign is where one out-of-line array begins: aligned to // max( 64, alignof( element ) ) (docs/SPEC-TABLES.md §19.1). func blockStartAlign(a ir.BlockArray) int64 { @@ -674,8 +778,18 @@ func (g *tableGen) emitBlockDefinitions(bl *ir.BlockLayout) { func (g *tableGen) emitBlockOpenBody(bl *ir.BlockLayout) { name := bl.Table.Name - g.pf("bool %sBlockOpen( %sBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )\n{\n", name, name) - g.pf(" block.base = NULL;\n block.projection = NULL;\n block.bytes = 0;\n") + check := strings.ToLower(name) + "_block_open_check" + g.pf("// THE CHECK ITSELF, over a base it never writes through, because a check\n") + g.pf("// READS (docs/SPEC-TABLES.md §19.2). Both overloads of %sBlockOpen are\n", name) + g.pf("// this one body, so the producer's path and the consumer's cannot drift:\n") + g.pf("// the same clauses in the same order, and the same reason on the same\n") + g.pf("// refusal, whether the caller handed over bytes it owns or bytes it may\n") + g.pf("// only read. It answers the USED EXTENT beside the verdict, which is the\n") + g.pf("// one thing each overload has to write into its own handle.\n") + g.pf("//\n") + g.pf("// It sits in an anonymous namespace and claims no name (§11).\n") + g.pf("namespace {\n\n") + g.pf("bool %s( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason )\n{\n", check) g.pf(" // a null buffer is the CALLER's defect, as an unaligned base is; a buffer\n") g.pf(" // shorter than the prologue has no prologue to read and is truncated\n") g.pf(" if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }\n") @@ -730,10 +844,32 @@ func (g *tableGen) emitBlockOpenBody(bl *ir.BlockLayout) { g.pf(" // the base's alignment, LAST: the only clause that reads nothing out of\n") g.pf(" // the block, and the caller's defect rather than the file's\n") g.pf(" if ( ( (uintptr_t) base %% %d ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }\n", ir.BlockAlign) + g.pf(" *used_out = used;\n") + g.pf(" return true;\n}\n\n") + g.pf("} // namespace\n\n") + + g.pf("// THE PRODUCER's overload, unchanged in what it does: the check above, and\n") + g.pf("// then a handle over bytes the caller may write.\n") + g.pf("bool %sBlockOpen( %sBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )\n{\n", name, name) + g.pf(" block.base = NULL;\n block.projection = NULL;\n block.bytes = 0;\n") + g.pf(" int64_t used = 0;\n") + g.pf(" if ( !%s( base, bytes, &used, reason ) ) { return false; }\n", check) g.pf(" block.base = (uint8_t *) base;\n") g.pf(" block.projection = (%sBlock::Projection *) base;\n", name) g.pf(" block.bytes = used;\n") g.pf(" return true;\n}\n\n") + + g.pf("// THE CONSUMER's overload (schema#455): the same check, and then a handle\n") + g.pf("// that carries const the whole way down. A read-only mapping opens through\n") + g.pf("// it with no cast, and nothing it hands back can be written through.\n") + g.pf("bool %sBlockOpen( %sBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason )\n{\n", name, name) + g.pf(" block.base = NULL;\n block.projection = NULL;\n block.bytes = 0;\n") + g.pf(" int64_t used = 0;\n") + g.pf(" if ( !%s( base, bytes, &used, reason ) ) { return false; }\n", check) + g.pf(" block.base = (const uint8_t *) base;\n") + g.pf(" block.projection = (const %sBlock::Projection *) base;\n", name) + g.pf(" block.bytes = used;\n") + g.pf(" return true;\n}\n\n") } // emitBlockDescriptors emits one table's block reflection: the projection diff --git a/test/tables/block_const_main.cpp b/test/tables/block_const_main.cpp new file mode 100644 index 000000000..04c2603e5 --- /dev/null +++ b/test/tables/block_const_main.cpp @@ -0,0 +1,191 @@ +/* + THE BLOCK FORM's CONST READ PATH (docs/SPEC-TABLES.md §19.2, schema#455). + + A block is memory another build wrote, and a consumer of foreign bytes + reads it. This is the leg that holds the READ side to that: a pinned block + image is loaded into a 64-byte aligned buffer, held as a `const uint8_t *` + from that moment on, and opened WITH NO CAST. Every row it reads comes back + read-only, so the write access a producer needs never reaches a consumer + that only reads. + + It is one program with two halves, and the Makefile runs both: + + * the DEFAULT build opens the const region, walks its rows through the + const view and compares them against the same bytes opened writable, so + the two overloads agree value for value and the producer's path is + proved unchanged; + * `-DBLOCK_CONST_WRITE` adds ONE assignment through the const view, and + that build MUST NOT COMPILE. A read-only view whose writes compile is a + view in name only, which is what the negative compile control holds. + + Prints OK and exits 0: no test framework, exit code is the verdict. +*/ + +#include "RenderBlock.h" + +#include +#include +#include +#include + +using namespace blockdemo; + +static int failures = 0; + +static void check( bool ok, const char * what ) +{ + if ( !ok ) + { + printf( "FAILED: %s\n", what ); + failures++; + } +} + +// The image, read off disk into a 64-byte aligned buffer, which is §19.1's +// base alignment and BlockOpen's last clause. The allocation is handed +// back beside the base so the caller frees what it was given rather than the +// pointer it aligned. +static uint8_t * load_image( const char * path, int64_t * bytes, void ** allocation ) +{ + FILE * f = fopen( path, "rb" ); + if ( f == NULL ) { return NULL; } + fseek( f, 0, SEEK_END ); + const long length = ftell( f ); + fseek( f, 0, SEEK_SET ); + if ( length <= 0 ) { fclose( f ); return NULL; } + void * raw = malloc( (size_t) length + 63 ); + if ( raw == NULL ) { fclose( f ); return NULL; } + uint8_t * base = (uint8_t *) ( ( (uintptr_t) raw + 63 ) & ~(uintptr_t) 63 ); + const size_t read = fread( base, 1, (size_t) length, f ); + fclose( f ); + if ( read != (size_t) length ) { free( raw ); return NULL; } + *bytes = (int64_t) length; + *allocation = raw; + return base; +} + +int main( int argc, char ** argv ) +{ + const char * path = argc > 1 ? argv[1] : "testdata/wire/tables/block_render.bin"; + + int64_t bytes = 0; + void * allocation = NULL; + uint8_t * loaded = load_image( path, &bytes, &allocation ); + if ( loaded == NULL ) + { + printf( "FAILED: could not read the block image %s\n", path ); + return 1; + } + + // FROM HERE THE BYTES ARE CONST, and nothing below casts that away. This + // is the consumer an mmap of a read-only file hands its bytes to (#455). + const uint8_t * region = loaded; + + RenderFrameBlock::Const block; + TableRefuseReason reason = ok; + check( RenderFrameBlockOpen( block, region, bytes, &reason ), + "a const region opens with no cast (docs/SPEC-TABLES.md §19.2)" ); + check( reason == ok, "a matched open writes no reason" ); + check( block.base == region, "the const handle points at the bytes it was given" ); + check( block.projection != NULL, "the const handle carries the projection" ); + check( block.bytes > 0 && block.bytes <= bytes, "the used extent lies inside the caller's bytes" ); + + // THE ROWS, READ-ONLY. The iteration is §19.2's, at the pitch the instance + // gives and never spelled at the call site, and what it yields is a + // reference a consumer cannot write through. + int32_t iterated = 0; + uint32_t checksum = 0; + for ( const RenderShip & ship : RenderFrameShips( block ) ) + { + iterated++; + checksum += ship.object_id; + } + TableBlockConstRows rows = RenderFrameShips( block ); + check( iterated == rows.size(), "the const iteration visits every row of the array" ); + check( rows.size() == (int32_t) block.projection->ships.count, + "and the count it walks is the INSTANCE's, never a constant of this build (§19.2)" ); + + TableBlockConstSpan span = RenderFrameShipsSpan( block ); + check( span.size() == rows.size(), "the contiguous const view carries the same count" ); + uint32_t span_checksum = 0; + for ( int32_t i = 0; i < span.size(); i++ ) { span_checksum += span[i].object_id; } + check( span_checksum == checksum, "and the same rows: the pitch IS sizeof (§2.7)" ); + + const RenderShip * base_pointer = span.begin(); + check( base_pointer == (const RenderShip *) ( region + block.projection->ships.offset_of ), + "the const span begins at the array's own offset_of" ); + + // THE PRODUCER'S PATH IS UNCHANGED, and that is half the claim: the same + // bytes, opened through the mutable overload, are the same rows. A copy is + // taken because a mutable open is a mutable base, and the const region + // above must not be reachable from it. + void * mutable_allocation = NULL; + int64_t mutable_bytes = 0; + uint8_t * writable = load_image( path, &mutable_bytes, &mutable_allocation ); + if ( writable == NULL ) + { + printf( "FAILED: could not read the block image a second time\n" ); + free( allocation ); + return 1; + } + RenderFrameBlock mutable_block; + check( RenderFrameBlockOpen( mutable_block, writable, mutable_bytes ), + "the mutable overload still opens the same bytes" ); + TableBlockRows mutable_rows = RenderFrameShips( mutable_block ); + check( mutable_rows.size() == rows.size(), "both overloads read the same count" ); + int mismatches = 0; + for ( int32_t i = 0; i < rows.size(); i++ ) + { + if ( memcmp( &rows[i], &mutable_rows[i], sizeof( RenderShip ) ) != 0 ) { mismatches++; } + } + check( mismatches == 0, "and the same row bytes, value for value" ); + + // A NULL base and a short buffer answer on the const overload exactly as + // they do on the mutable one: one check, two entry points (§19.2). + RenderFrameBlock::Const refused; + reason = ok; + check( !RenderFrameBlockOpen( refused, (const void *) NULL, bytes, &reason ), "a null const base refuses" ); + check( reason == unaligned_base, "and names the caller's own defect" ); + reason = ok; + check( !RenderFrameBlockOpen( refused, region, 8, &reason ), "a const buffer shorter than the projection refuses" ); + check( reason == truncated, "and names it truncated" ); + // the base's alignment is the LAST clause (§19.2), so the image is moved + // whole to a base eight bytes off the sixty-four: every clause before it + // reads exactly what it read above, and only the alignment is wrong. + void * offset_allocation = malloc( (size_t) bytes + 128 ); + if ( offset_allocation == NULL ) + { + printf( "FAILED: could not allocate the unaligned image\n" ); + free( allocation ); + return 1; + } + uint8_t * offset_base = (uint8_t *) ( ( (uintptr_t) offset_allocation + 63 ) & ~(uintptr_t) 63 ) + 8; + memcpy( offset_base, region, (size_t) bytes ); + reason = ok; + check( !RenderFrameBlockOpen( refused, (const uint8_t *) offset_base, bytes, &reason ), + "an unaligned const base refuses" ); + check( reason == unaligned_base, "and names the caller's own defect, last (§19.2)" ); + free( offset_allocation ); + +#if defined( BLOCK_CONST_WRITE ) + // THE NEGATIVE COMPILE CONTROL. One assignment through the const view, and + // this build must not compile: a read-only view whose writes compile is a + // view in name only. + RenderFrameShipsSpan( block )[0].object_id = 1; + // and the rows view's two other doors, the typed base and the iterator, + // each held read-only by the same build + RenderFrameShips( block )[0].object_id = 1; + for ( RenderShip & ship : RenderFrameShips( block ) ) { ship.object_id = 2; } +#endif + + free( mutable_allocation ); + free( allocation ); + + if ( failures > 0 ) + { + printf( "%d failed\n", failures ); + return 1; + } + printf( "OK: a const block region opens with no cast, and its rows read back read-only (docs/SPEC-TABLES.md §19.2)\n" ); + return 0; +} diff --git a/testdata/golden/wide/cpp/CaptionBlock.cpp b/testdata/golden/wide/cpp/CaptionBlock.cpp index d4ac28b6b..1f50abfbf 100644 --- a/testdata/golden/wide/cpp/CaptionBlock.cpp +++ b/testdata/golden/wide/cpp/CaptionBlock.cpp @@ -15,11 +15,19 @@ namespace wide { // ---- the block form of table Stamp: the open path and the descriptors ---- -bool StampBlockOpen( StampBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +// THE CHECK ITSELF, over a base it never writes through, because a check +// READS (docs/SPEC-TABLES.md §19.2). Both overloads of StampBlockOpen are +// this one body, so the producer's path and the consumer's cannot drift: +// the same clauses in the same order, and the same reason on the same +// refusal, whether the caller handed over bytes it owns or bytes it may +// only read. It answers the USED EXTENT beside the verdict, which is the +// one thing each overload has to write into its own handle. +// +// It sits in an anonymous namespace and claims no name (§11). +namespace { + +bool stamp_block_open_check( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason ) { - block.base = NULL; - block.projection = NULL; - block.bytes = 0; // a null buffer is the CALLER's defect, as an unaligned base is; a buffer // shorter than the prologue has no prologue to read and is truncated if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; } @@ -49,12 +57,43 @@ bool StampBlockOpen( StampBlock & block, void * base, int64_t bytes, TableRefuse // the base's alignment, LAST: the only clause that reads nothing out of // the block, and the caller's defect rather than the file's if ( ( (uintptr_t) base % 64 ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; } + *used_out = used; + return true; +} + +} // namespace + +// THE PRODUCER's overload, unchanged in what it does: the check above, and +// then a handle over bytes the caller may write. +bool StampBlockOpen( StampBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !stamp_block_open_check( base, bytes, &used, reason ) ) { return false; } block.base = (uint8_t *) base; block.projection = (StampBlock::Projection *) base; block.bytes = used; return true; } +// THE CONSUMER's overload (schema#455): the same check, and then a handle +// that carries const the whole way down. A read-only mapping opens through +// it with no cast, and nothing it hands back can be written through. +bool StampBlockOpen( StampBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !stamp_block_open_check( base, bytes, &used, reason ) ) { return false; } + block.base = (const uint8_t *) base; + block.projection = (const StampBlock::Projection *) base; + block.bytes = used; + return true; +} + namespace { // forward declarations, so the element column can be a constant diff --git a/testdata/golden/wide/cpp/CaptionBlock.h b/testdata/golden/wide/cpp/CaptionBlock.h index f13a140a8..41f331912 100644 --- a/testdata/golden/wide/cpp/CaptionBlock.h +++ b/testdata/golden/wide/cpp/CaptionBlock.h @@ -169,6 +169,48 @@ struct TableBlockSpan T & operator[]( int32_t i ) const { return rows[i]; } }; +// THE READ SIDE's own two views, and they are the same two with const on every +// pointer they hand out (docs/SPEC-TABLES.md §19.2). A block is memory another +// build wrote: a consumer opening bytes it received reads them and writes +// nothing, and a read-only mapping cannot be opened through a view that hands +// back a mutable row at all. The producer's pair above is untouched: Begin, +// the fill accessors and the typed base a worker indexes stay exactly what +// they were, and this pair is what the const overload of BlockOpen fills, so +// the split is the one C# already has (ref readonly, ReadOnlySpan). +template +struct TableBlockConstRows +{ + const uint8_t * base = NULL; + int32_t count = 0; + int32_t stride = 0; + + struct iterator + { + const uint8_t * p; + int32_t stride; + const T & operator*() const { return *(const T *) p; } + iterator & operator++() { p += stride; return *this; } + bool operator!=( const iterator & other ) const { return p != other.p; } + }; + + iterator begin() const { return iterator{ base, stride }; } + iterator end() const { return iterator{ base + (ptrdiff_t) count * stride, stride }; } + int32_t size() const { return count; } + operator const T *() const { return (const T *) base; } +}; + +template +struct TableBlockConstSpan +{ + const T * rows = NULL; + int32_t count = 0; + + const T * begin() const { return rows; } + const T * end() const { return rows + count; } + int32_t size() const { return count; } + const T & operator[]( int32_t i ) const { return rows[i]; } +}; + // ---- reflection over a block (docs/SPEC-TABLES.md §8, §19.2) ---- // // The descriptors are the mechanism, and they are what retires a hand-kept @@ -345,6 +387,20 @@ struct StampBlock Projection * projection = NULL; // the projection, at offset 0 int64_t bytes = 0; // the extent in use + // THE CONSUMER's handle: this same block over bytes it may not write + // (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote, + // and a consumer that received it reads it: the const overload of + // StampBlockOpen fills this from a `const void *`, so a read-only + // mapping opens with no cast, and the row accessors overloaded on it + // hand back const rows. It is a MEMBER TYPE and claims no name of its + // own (§11): the producer's handle above is unchanged in every way. + struct Const + { + const uint8_t * base = NULL; // the extent's base, 64-byte aligned + const Projection * projection = NULL; // the projection, at offset 0 + int64_t bytes = 0; // the extent in use + }; + // this table's block descriptors (docs/SPEC-TABLES.md §8, §19.2): constant // data, defined in the .cpp beside this header. static const TableBlockInfo * Type(); @@ -440,6 +496,15 @@ inline int64_t StampBlockBytes( const StampBlock & block ) // build that wrote it takes the wire (§3), which this same table still has. bool StampBlockOpen( StampBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL ); +// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A +// block arrives from another language, another process or an mmap of a +// read-only file, and a consumer that only reads should neither need a +// const_cast to open it nor receive write access to it. It is the SAME +// CHECK (one body, the same clauses in the same order, the same reason on +// the same refusal) over a base it never writes through, and it fills the +// const handle, whose row accessors hand back const rows. +bool StampBlockOpen( StampBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL ); + // ---- the block form of table Stamp: end ---- } // namespace wide