Skip to content

Commit 82b6b2f

Browse files
authored
Allow derived structs to override base fields with subtypes (#681)
* Allow derived structs to override base fields with subtypes Signed-off-by: Adam Glustein <adam.glustein@point72.com>
1 parent 0a0ae29 commit 82b6b2f

4 files changed

Lines changed: 338 additions & 53 deletions

File tree

cpp/csp/engine/Struct.cpp

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -39,47 +39,106 @@ and adjustments required for the hidden fields
3939
4040
*/
4141

42-
StructMeta::StructMeta( const std::string & name, const Fields & fields, bool isStrict,
43-
std::shared_ptr<StructMeta> base ) : m_name( name ), m_base( base ), m_fields( fields ), m_optionalFieldsBitMasks(),
42+
StructMeta::StructMeta( const std::string & name, Fields fields, bool isStrict,
43+
std::shared_ptr<StructMeta> base ) : m_name( name ), m_base( base ), m_fields(), m_optionalFieldsBitMasks(),
4444
m_size( 0 ), m_partialSize( 0 ), m_partialStart( 0 ), m_nativeStart( 0 ), m_basePadding( 0 ),
4545
m_maskLoc( 0 ), m_maskSize( 0 ), m_firstPartialField( 0 ), m_firstNativePartialField( 0 ),
4646
m_isPartialNative( true ), m_isFullyNative( true ), m_isStrict( isStrict )
4747
{
48-
if( m_fields.empty() && !m_base )
48+
if( fields.empty() && !m_base )
4949
CSP_THROW( TypeError, "Struct types must define at least 1 field" );
5050

5151
//sort by sizes, biggest first, to get proper alignment
5252
//group generic objects separately at the start so that we can safely memcpy native types
5353
//decided to place them at the start cause they are most likely size of ptr or greater
5454

55-
m_fieldnames.reserve( m_fields.size() );
56-
for( size_t i = 0; i < m_fields.size(); i++ )
57-
m_fieldnames.emplace_back( m_fields[i] -> fieldname() );
55+
std::vector<std::string> fieldnames; // need to keep the original order of the derived struct
56+
fieldnames.reserve( fields.size() );
57+
for( auto & f : fields )
58+
fieldnames.emplace_back( f -> fieldname() );
5859

59-
std::sort( m_fields.begin(), m_fields.end(), []( auto && a, auto && b )
60+
std::sort( fields.begin(), fields.end(), []( auto && a, auto && b )
6061
{
6162
return a -> isNative() < b -> isNative() ||
6263
a -> size() > b -> size();
6364
} );
6465

66+
if( m_base )
67+
{
68+
// The complete inheritance hierarchy must agree on strict/non-strict
69+
if( m_isStrict != m_base -> isStrict() )
70+
{
71+
CSP_THROW( ValueError,
72+
"Struct " << m_name << " was declared " << ( m_isStrict ? "strict" : "non-strict" ) << " but derives from "
73+
<< m_base -> name() << " which is " << ( m_base -> isStrict() ? "strict" : "non-strict" )
74+
);
75+
}
76+
77+
m_fields = m_base -> m_fields;
78+
m_fieldnames = m_base -> m_fieldnames;
79+
m_fieldMap = m_base -> m_fieldMap;
80+
m_firstPartialField = m_base -> m_fields.size();
81+
82+
// append non-override derived fieldnames in their original declaration order
83+
for( auto & fn : fieldnames )
84+
{
85+
if( m_fieldMap.find( fn.c_str() ) == m_fieldMap.end() )
86+
m_fieldnames.push_back( std::move( fn ) );
87+
}
88+
}
89+
else
90+
m_fieldnames = std::move( fieldnames );
91+
92+
std::vector<StructFieldPtr> derivedFields; // easier to store this for pre-processing and only append derived fields to m_fields at the end
93+
for( auto & derivedField : fields )
94+
{
95+
auto rv = m_fieldMap.emplace( derivedField -> fieldname().c_str(), derivedField );
96+
if( !rv.second )
97+
{
98+
auto derivedTypeEnum = derivedField -> type() -> type();
99+
auto baseTypeEnum = rv.first -> second -> type() -> type();
100+
101+
// only allowed to override fields from a base class if types are DIALECT_GENERIC or STRUCT
102+
// beyond that, its up to the dialect-specific struct impl (i.e. PyStruct) to decide whether the override types are valid
103+
if( derivedTypeEnum != baseTypeEnum || ( derivedTypeEnum != CspType::Type::DIALECT_GENERIC && derivedTypeEnum != CspType::Type::STRUCT ) )
104+
CSP_THROW( ValueError, "csp Struct " << name << " attempted to add existing field " << derivedField -> fieldname() <<
105+
" which is not allowed: base field type " << baseTypeEnum << " and derived field type " << derivedTypeEnum );
106+
107+
// replace the base field in-place with the derived override,
108+
for( size_t baseIdx = 0; baseIdx < m_firstPartialField; ++baseIdx )
109+
{
110+
if( m_fields[ baseIdx ] -> fieldname() == derivedField -> fieldname() )
111+
{
112+
derivedField -> setOffset( m_fields[ baseIdx ] -> offset() );
113+
derivedField -> setMaskOffset( m_fields[ baseIdx ] -> maskOffset(), m_fields[ baseIdx ] -> maskBit() );
114+
m_fields[ baseIdx ] = derivedField;
115+
break;
116+
}
117+
}
118+
rv.first -> second = derivedField; // field map
119+
}
120+
else
121+
derivedFields.push_back( derivedField );
122+
}
123+
65124
size_t baseSize = m_base ? m_base -> size() : 0;
66125
size_t offset = baseSize;
67126
m_basePadding = 0;
68127

69-
//align to first field's alignment
70-
if( m_fields.size() && ( offset % m_fields[0] -> alignment() != 0 ) )
71-
m_basePadding = m_fields[0] -> alignment() - offset % m_fields[0] -> alignment();
128+
//align to first field's alignment if there are derived fields remaining
129+
if( !derivedFields.empty() && ( offset % derivedFields[ 0 ] -> alignment() != 0 ) )
130+
m_basePadding = derivedFields[ 0 ] -> alignment() - offset % derivedFields[ 0 ] -> alignment();
72131

73132
offset += m_basePadding;
74-
if( !m_fields.empty() )
75-
CSP_ASSERT( ( offset % m_fields[0] -> alignment() ) == 0 );
133+
if( !derivedFields.empty() )
134+
CSP_ASSERT( ( offset % derivedFields[ 0 ] -> alignment() ) == 0 );
76135

77136
m_partialStart = offset;
78137
m_nativeStart = m_partialStart;
138+
m_firstNativePartialField = m_firstPartialField;
79139

80-
for( size_t idx = 0; idx < m_fields.size(); ++idx )
140+
for( auto & f : derivedFields )
81141
{
82-
auto & f = m_fields[ idx ];
83142
if( offset % f -> alignment() != 0 )
84143
offset += f -> alignment() - offset % f -> alignment();
85144

@@ -92,24 +151,24 @@ StructMeta::StructMeta( const std::string & name, const Fields & fields, bool is
92151
if( !f -> isNative() )
93152
{
94153
m_nativeStart = offset;
95-
m_firstNativePartialField = idx + 1;
154+
m_firstNativePartialField += 1;
96155
}
97156
}
98157

99158
m_isFullyNative = m_isPartialNative && ( m_base ? m_base -> isNative() : true );
100159

101-
//Setup masking bits for our fields
102-
//NOTE we can be more efficient by sticking masks into any potential alignment gaps, dont want to spend time on it
103-
//at this point
160+
// setup masking bits for the remaining derived (partial) fields
161+
// note that this is done after the override dedup above so that overridden fields don't consume mask bits.
162+
//NOTE we can be more efficient by sticking masks into any potential alignment gaps, dont want to spend time on it
163+
size_t optionalFieldCount = std::count_if( derivedFields.begin(), derivedFields.end(), []( const auto & f ) { return f -> isOptional(); } );
164+
size_t partialFieldCount = derivedFields.size();
104165

105-
size_t optionalFieldCount = std::count_if( m_fields.begin(), m_fields.end(), []( const auto & f ) { return f -> isOptional(); } );
106-
107-
m_maskSize = !m_fields.empty() ? 1 + ( ( m_fields.size() + optionalFieldCount - 1 ) / 8 ) : 0;
166+
m_maskSize = partialFieldCount > 0 ? 1 + ( ( partialFieldCount + optionalFieldCount - 1 ) / 8 ) : 0;
108167
m_size = offset + m_maskSize;
109168
m_partialSize = m_size - baseSize;
110169
m_maskLoc = m_size - m_maskSize;
111-
112-
uint8_t numRemainingBits = ( m_fields.size() + optionalFieldCount ) % 8;
170+
171+
uint8_t numRemainingBits = ( partialFieldCount + optionalFieldCount ) % 8;
113172
m_lastByteMask = ( 1u << numRemainingBits ) - 1;
114173

115174
size_t maskLoc = m_maskLoc;
@@ -118,9 +177,8 @@ StructMeta::StructMeta( const std::string & name, const Fields & fields, bool is
118177
// Set optional fields first so that their 2-bits never cross a byte boundary
119178
// Put both the set bits and none bits in the same vector to avoid fragmentation
120179
m_optionalFieldsBitMasks.resize( 2 * m_maskSize );
121-
for( size_t i = 0; i < m_fields.size(); ++i )
180+
for( auto & f : derivedFields )
122181
{
123-
auto & f = m_fields[ i ];
124182
if( f -> isOptional() )
125183
{
126184
f -> setMaskOffset( maskLoc, maskBit );
@@ -134,9 +192,8 @@ StructMeta::StructMeta( const std::string & name, const Fields & fields, bool is
134192
}
135193
}
136194

137-
for( size_t i = 0; i < m_fields.size(); ++i )
195+
for( auto & f : derivedFields )
138196
{
139-
auto & f = m_fields[ i ];
140197
if( !f -> isOptional() )
141198
{
142199
f -> setMaskOffset( maskLoc, maskBit );
@@ -148,31 +205,8 @@ StructMeta::StructMeta( const std::string & name, const Fields & fields, bool is
148205
}
149206
}
150207

151-
if( m_base )
152-
{
153-
// The complete inheritance hierarchy must agree on strict/non-strict
154-
if( m_isStrict != m_base -> isStrict() )
155-
{
156-
CSP_THROW( ValueError,
157-
"Struct " << m_name << " was declared " << ( m_isStrict ? "strict" : "non-strict" ) << " but derives from "
158-
<< m_base -> name() << " which is " << ( m_base -> isStrict() ? "strict" : "non-strict" )
159-
);
160-
}
161-
162-
m_fields.insert( m_fields.begin(), m_base -> m_fields.begin(), m_base -> m_fields.end() );
163-
m_fieldnames.insert( m_fieldnames.begin(), m_base -> m_fieldnames.begin(), m_base -> m_fieldnames.end() );
164-
165-
m_firstPartialField = m_base -> m_fields.size();
166-
m_firstNativePartialField += m_base -> m_fields.size();
167-
m_fieldMap = m_base -> m_fieldMap;
168-
}
169-
170-
for( size_t idx = m_firstPartialField; idx < m_fields.size(); ++idx )
171-
{
172-
auto rv = m_fieldMap.emplace( m_fields[ idx ] -> fieldname().c_str(), m_fields[ idx ] );
173-
if( !rv.second )
174-
CSP_THROW( ValueError, "csp Struct " << name << " attempted to add existing field " << m_fields[ idx ] -> fieldname() );
175-
}
208+
// append derived fields to m_fields after all processing is done
209+
m_fields.insert( m_fields.end(), derivedFields.begin(), derivedFields.end() );
176210
}
177211

178212
StructMeta::~StructMeta()

cpp/csp/engine/Struct.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ class StructMeta : public std::enable_shared_from_this<StructMeta>
625625
using FieldNames = std::vector<std::string>;
626626

627627
//Fields will be re-arranged and assigned their offsets in StructMeta for optimal performance
628-
StructMeta( const std::string & name, const Fields & fields, bool isStrict, std::shared_ptr<StructMeta> base = nullptr );
628+
StructMeta( const std::string & name, Fields fields, bool isStrict, std::shared_ptr<StructMeta> base = nullptr );
629629
virtual ~StructMeta();
630630

631631
const std::string & name() const { return m_name; }

cpp/csp/python/PyStruct.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,52 @@ static PyObject * PyStructMeta_new( PyTypeObject *subtype, PyObject *args, PyObj
182182
metabase = ( ( PyStructMeta * ) base ) -> structMeta;
183183
}
184184
}
185+
186+
// validate overriden fields in the derived class, if they exist
187+
if( metabase )
188+
{
189+
for( const auto & derivedField : fields )
190+
{
191+
const auto & baseField = metabase -> field( derivedField -> fieldname() );
192+
if( !baseField )
193+
continue;
194+
195+
// ensure the types are compatible i.e. derived field type is a subclass of base
196+
auto baseTypeEnum = baseField -> type() -> type();
197+
auto derivedTypeEnum = derivedField -> type() -> type();
198+
199+
if( baseTypeEnum != derivedTypeEnum )
200+
{
201+
CSP_THROW( TypeError, "Field '" << derivedField -> fieldname() << "' on struct " << name
202+
<< " has type incompatible with base field type: base type is " << baseTypeEnum
203+
<< ", derived type is " << derivedTypeEnum );
204+
}
205+
206+
// field overrides are only valid for Python objects and structs, not native types
207+
if( baseTypeEnum == CspType::Type::DIALECT_GENERIC )
208+
{
209+
auto * basePyField = static_cast<PyObjectStructField *>( baseField.get() );
210+
auto * derivedPyField = static_cast<PyObjectStructField *>( derivedField.get() );
211+
if( !PyType_IsSubtype( derivedPyField -> pytype(), basePyField -> pytype() ) )
212+
{
213+
CSP_THROW( TypeError, "Field '" << derivedField -> fieldname() << "' on struct " << name
214+
<< ": expected subclass of " << basePyField -> pytype() -> tp_name
215+
<< ", got " << derivedPyField -> pytype() -> tp_name << " which is not a subclass" );
216+
}
217+
}
218+
else if( baseTypeEnum == CspType::Type::STRUCT )
219+
{
220+
auto baseMeta = static_cast<const CspStructType &>( *baseField -> type() ).meta();
221+
auto derivedMeta = static_cast<const CspStructType &>( *derivedField -> type() ).meta();
222+
if( !StructMeta::isDerivedType( derivedMeta.get(), baseMeta.get() ) )
223+
{
224+
CSP_THROW( TypeError, "Field '" << derivedField -> fieldname() << "' on struct " << name
225+
<< ": expected struct derived from " << baseMeta -> name()
226+
<< ", got " << derivedMeta -> name() << " which is not a subclass" );
227+
}
228+
}
229+
}
230+
}
185231

186232
/*back reference to the struct type that will be accessible on the csp struct -> meta()
187233
DialectStructMeta needs a strong reference to the type. This creates a known strong circular dep

0 commit comments

Comments
 (0)