diff --git a/rust/reflection/src/lib.rs b/rust/reflection/src/lib.rs index db1f3993544..87579bf0ffc 100644 --- a/rust/reflection/src/lib.rs +++ b/rust/reflection/src/lib.rs @@ -154,6 +154,10 @@ pub unsafe fn get_field_struct<'a>( /// Gets a Vector table field given its exact type. Returns empty vector if the field is not set. Returns error if the type doesn't match. /// +/// This function works for vectors of scalar/primitive types (integers, floats, bools). +/// For vectors of tables, use [`get_field_vector_of_tables`]. For vectors of strings, use +/// [`get_field_vector_of_strings`]. For vectors of structs, use [`get_field_vector_of_structs`]. +/// /// # Safety /// /// The value of the corresponding slot must have type Vector @@ -173,6 +177,166 @@ pub unsafe fn get_field_vector<'a, T: Follow<'a, Inner = T>>( Ok(table.get::>>(field.offset(), Some(Vector::::default()))) } +/// Gets a vector-of-tables field. Returns [None] if the field is not set. +/// Returns error if the field is not a vector of tables (rejects vectors of structs). +/// +/// Unlike [`get_field_vector`], this function works with vectors of tables where each +/// element is stored as an offset (`ForwardsUOffset`) in the buffer. +/// +/// Requires the [`Schema`] to distinguish tables from structs (both use `BaseType::Obj`). +/// +/// # Safety +/// +/// The value of the corresponding slot must be a vector of tables, and the schema must +/// match the buffer. +pub unsafe fn get_field_vector_of_tables<'a>( + table: &Table<'a>, + field: &Field, + schema: &Schema, +) -> FlatbufferResult>>>> { + if field.type_().base_type() != BaseType::Vector || field.type_().element() != BaseType::Obj { + return Err(FlatbufferError::FieldTypeMismatch( + String::from("Vector of Table"), + field.type_().base_type().variant_name().unwrap_or_default().to_string(), + )); + } + + let type_index = field.type_().index(); + if type_index < 0 || type_index as usize >= schema.objects().len() { + return Err(FlatbufferError::InvalidSchema); + } + let object = schema.objects().get(type_index as usize); + if object.is_struct() { + return Err(FlatbufferError::FieldTypeMismatch( + String::from("Vector of Table"), + String::from("Vector of Struct"), + )); + } + + Ok(table.get::>>>>(field.offset(), None)) +} + +/// Gets a vector-of-strings field. Returns [None] if the field is not set. +/// Returns error if the field is not a vector or the element type is not `String`. +/// +/// Unlike [`get_field_vector`], this function works with vectors of strings where each +/// element is stored as an offset (`ForwardsUOffset<&str>`) in the buffer. +/// +/// # Safety +/// +/// The value of the corresponding slot must be a vector of strings. +pub unsafe fn get_field_vector_of_strings<'a>( + table: &Table<'a>, + field: &Field, +) -> FlatbufferResult>>> { + if field.type_().base_type() != BaseType::Vector + || field.type_().element() != BaseType::String + { + return Err(FlatbufferError::FieldTypeMismatch( + String::from("Vector of String"), + field.type_().base_type().variant_name().unwrap_or_default().to_string(), + )); + } + + Ok(table.get::>>>(field.offset(), None)) +} + +/// A vector of structs accessed through the reflection API. +/// +/// Since struct elements are stored inline at their schema-defined byte size (which +/// may differ from the Rust `Struct` type's size), this type provides indexed access +/// using the correct element stride from the schema. +#[derive(Debug)] +pub struct StructVector<'a> { + buf: &'a [u8], + loc: usize, + len: usize, + element_size: usize, +} + +impl<'a> StructVector<'a> { + /// Returns the number of elements in this vector. + pub fn len(&self) -> usize { + self.len + } + + /// Returns `true` if the vector has no elements. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Returns the struct at the given index. + /// + /// # Panics + /// + /// Panics if `idx >= self.len()`. + pub fn get(&self, idx: usize) -> Struct<'a> { + assert!(idx < self.len); + let element_loc = self.loc + SIZE_UOFFSET + idx * self.element_size; + // SAFETY: the caller of `get_field_vector_of_structs` guaranteed the buffer + // contains a valid vector of structs matching the schema. + unsafe { Struct::new(self.buf, element_loc) } + } +} + +/// Gets a vector-of-structs field. Returns [None] if the field is not set. +/// Returns error if the field is not a vector of structs. +/// +/// Requires the [Schema] to look up the struct's byte size for correct element indexing. +/// +/// # Safety +/// +/// The value of the corresponding slot must be a vector of structs, and the schema must +/// match the buffer. +pub unsafe fn get_field_vector_of_structs<'a>( + table: &Table<'a>, + field: &Field, + schema: &Schema, +) -> FlatbufferResult>> { + if field.type_().base_type() != BaseType::Vector || field.type_().element() != BaseType::Obj { + return Err(FlatbufferError::FieldTypeMismatch( + String::from("Vector of Struct"), + field.type_().base_type().variant_name().unwrap_or_default().to_string(), + )); + } + + let type_index = field.type_().index(); + if type_index < 0 || type_index as usize >= schema.objects().len() { + return Err(FlatbufferError::InvalidSchema); + } + let object = schema.objects().get(type_index as usize); + if !object.is_struct() { + return Err(FlatbufferError::FieldTypeMismatch( + String::from("Vector of Struct"), + String::from("Vector of Table"), + )); + } + + let element_size = object.bytesize() as usize; + if element_size == 0 { + return Err(FlatbufferError::InvalidSchema); + } + + let field_offset = table.vtable().get(field.offset()) as usize; + if field_offset == 0 { + return Ok(None); + } + + let vector_loc = { + let field_loc = table.loc() + field_offset; + let offset = read_scalar::(&table.buf()[field_loc..]) as usize; + field_loc + offset + }; + let len = read_scalar::(&table.buf()[vector_loc..]) as usize; + + Ok(Some(StructVector { + buf: table.buf(), + loc: vector_loc, + len, + element_size, + })) +} + /// Gets a Table table field given its exact type. Returns [None] if the field is not set. Returns error if the type doesn't match. /// /// # Safety diff --git a/rust/reflection/src/safe_buffer.rs b/rust/reflection/src/safe_buffer.rs index dc0f8f9860f..551ce5c8104 100644 --- a/rust/reflection/src/safe_buffer.rs +++ b/rust/reflection/src/safe_buffer.rs @@ -21,8 +21,9 @@ use crate::{ get_any_field_float, get_any_field_float_in_struct, get_any_field_integer, get_any_field_integer_in_struct, get_any_field_string, get_any_field_string_in_struct, get_any_root, get_field_float, get_field_integer, get_field_string, get_field_struct, - get_field_struct_in_struct, get_field_table, get_field_vector, FlatbufferError, - FlatbufferResult, ForwardsUOffset, + get_field_struct_in_struct, get_field_table, get_field_vector, + get_field_vector_of_strings, get_field_vector_of_structs, get_field_vector_of_tables, + FlatbufferError, FlatbufferResult, ForwardsUOffset, StructVector, }; use flatbuffers::{Follow, Table, Vector, VerifierOptions}; use num_traits::float::Float; @@ -167,6 +168,68 @@ impl<'a> SafeTable<'a> { } } + /// Gets a vector-of-tables field by name. Returns [None] if the field is not set. Returns error if + /// the table doesn't match the buffer or + /// the [field_name] doesn't match the table or + /// the field is not a vector of tables. + pub fn get_field_vector_of_tables( + &self, + field_name: &str, + ) -> FlatbufferResult>>>> { + if let Some(field) = self.safe_buf.find_field_by_name(self.loc, field_name)? { + // SAFETY: the buffer was verified during construction. + unsafe { + get_field_vector_of_tables( + &Table::new(&self.safe_buf.buf, self.loc), + &field, + self.safe_buf.schema, + ) + } + } else { + Err(FlatbufferError::FieldNotFound) + } + } + + /// Gets a vector-of-strings field by name. Returns [None] if the field is not set. Returns error if + /// the table doesn't match the buffer or + /// the [field_name] doesn't match the table or + /// the field is not a vector of strings. + pub fn get_field_vector_of_strings( + &self, + field_name: &str, + ) -> FlatbufferResult>>> { + if let Some(field) = self.safe_buf.find_field_by_name(self.loc, field_name)? { + // SAFETY: the buffer was verified during construction. + unsafe { + get_field_vector_of_strings(&Table::new(&self.safe_buf.buf, self.loc), &field) + } + } else { + Err(FlatbufferError::FieldNotFound) + } + } + + /// Gets a vector-of-structs field by name. Returns [None] if the field is not set. Returns error if + /// the table doesn't match the buffer or + /// the [field_name] doesn't match the table or + /// the field is not a vector of structs. + pub fn get_field_vector_of_structs( + &self, + field_name: &str, + ) -> FlatbufferResult>> { + if let Some(field) = self.safe_buf.find_field_by_name(self.loc, field_name)? { + // SAFETY: the buffer was verified during construction. + unsafe { + get_field_vector_of_structs( + &Table::new(&self.safe_buf.buf, self.loc), + &field, + self.safe_buf.schema, + ) + } + } else { + Err(FlatbufferError::FieldNotFound) + } + } + /// Gets a [SafeTable] table field given its exact type. Returns [None] if the field is not set. Returns error if /// the table doesn't match the buffer or /// the [field_name] doesn't match the table or diff --git a/tests/rust_reflection_test/src/lib.rs b/tests/rust_reflection_test/src/lib.rs index 6e92ecb2bb8..3240a3ce259 100644 --- a/tests/rust_reflection_test/src/lib.rs +++ b/tests/rust_reflection_test/src/lib.rs @@ -3,8 +3,10 @@ use flatbuffers_reflection::{ get_any_field_float, get_any_field_float_in_struct, get_any_field_integer, get_any_field_integer_in_struct, get_any_field_string, get_any_field_string_in_struct, get_any_root, get_field_float, get_field_integer, get_field_string, get_field_struct, - get_field_struct_in_struct, get_field_table, get_field_vector, set_any_field_float, - set_any_field_integer, set_any_field_string, set_field, set_string, FlatbufferError, + get_field_struct_in_struct, get_field_table, get_field_vector, + get_field_vector_of_strings, get_field_vector_of_structs, get_field_vector_of_tables, + set_any_field_float, set_any_field_integer, set_any_field_string, set_field, set_string, + FlatbufferError, }; use flatbuffers_reflection::{SafeBuffer, Struct}; @@ -349,6 +351,180 @@ fn test_buffer_vector_diff_type_fails() { ); } +#[test] +fn test_buffer_vector_of_tables_succeeds() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema_buffer = load_file_as_buffer("../monster_test.bfbs"); + let schema = root_as_schema(schema_buffer.as_slice()).unwrap(); + let field = get_schema_field(&schema_buffer, "testarrayoftables"); + + let value = unsafe { get_field_vector_of_tables(&root_table, &field, &schema) }; + + assert!(value.is_ok()); + let optional_vector = value.unwrap(); + assert!(optional_vector.is_some()); + let vector = optional_vector.unwrap(); + assert_eq!(vector.len(), 2); + + // Access the sub-tables and verify their contents via the reflection API. + let monster_obj = schema + .objects() + .lookup_by_key("MyGame.Example.Monster", |o, k| o.key_compare_with_value(k)) + .unwrap(); + let hp_field = monster_obj + .fields() + .lookup_by_key("hp", |f, k| f.key_compare_with_value(k)) + .unwrap(); + + let table0 = vector.get(0); + let hp0 = unsafe { get_field_integer::(&table0, &hp_field) }.unwrap(); + assert_eq!(hp0, Some(100)); + + let table1 = vector.get(1); + let hp1 = unsafe { get_field_integer::(&table1, &hp_field) }.unwrap(); + assert_eq!(hp1, Some(200)); +} + +#[test] +fn test_buffer_vector_of_tables_wrong_field_fails() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema_buffer = load_file_as_buffer("../monster_test.bfbs"); + let schema = root_as_schema(schema_buffer.as_slice()).unwrap(); + let field = get_schema_field(&schema_buffer, "inventory"); // ubyte vector, not table vector + + let value = unsafe { get_field_vector_of_tables(&root_table, &field, &schema) }; + + assert!(value.is_err()); +} + +#[test] +fn test_buffer_vector_of_tables_rejects_struct_vector() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema_buffer = load_file_as_buffer("../monster_test.bfbs"); + let schema = root_as_schema(schema_buffer.as_slice()).unwrap(); + let field = get_schema_field(&schema_buffer, "test4"); // [Test] is a vector of structs + + let value = unsafe { get_field_vector_of_tables(&root_table, &field, &schema) }; + + assert!(value.is_err()); + assert_eq!( + value.unwrap_err(), + FlatbufferError::FieldTypeMismatch( + String::from("Vector of Table"), + String::from("Vector of Struct"), + ) + ); +} + +#[test] +fn test_buffer_vector_of_structs_rejects_table_vector() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema_buffer = load_file_as_buffer("../monster_test.bfbs"); + let schema = root_as_schema(schema_buffer.as_slice()).unwrap(); + let field = get_schema_field(&schema_buffer, "testarrayoftables"); // [Monster] is a vector of tables + + let value = unsafe { get_field_vector_of_structs(&root_table, &field, &schema) }; + + assert!(value.is_err()); + assert_eq!( + value.unwrap_err(), + FlatbufferError::FieldTypeMismatch( + String::from("Vector of Struct"), + String::from("Vector of Table"), + ) + ); +} + +#[test] +fn test_buffer_vector_of_strings_succeeds() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema = load_file_as_buffer("../monster_test.bfbs"); + let field = get_schema_field(&schema, "testarrayofstring"); + + let value = unsafe { get_field_vector_of_strings(&root_table, &field) }; + + assert!(value.is_ok()); + let optional_vector = value.unwrap(); + assert!(optional_vector.is_some()); + let vector = optional_vector.unwrap(); + assert_eq!(vector.len(), 2); + assert_eq!(vector.get(0), "test1"); + assert_eq!(vector.get(1), "test2"); +} + +#[test] +fn test_buffer_vector_of_strings_wrong_field_fails() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema = load_file_as_buffer("../monster_test.bfbs"); + let field = get_schema_field(&schema, "inventory"); // ubyte vector, not string vector + + let value = unsafe { get_field_vector_of_strings(&root_table, &field) }; + + assert!(value.is_err()); +} + +#[test] +fn test_buffer_vector_of_structs_succeeds() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema_buffer = load_file_as_buffer("../monster_test.bfbs"); + let schema = root_as_schema(schema_buffer.as_slice()).unwrap(); + let field = get_schema_field(&schema_buffer, "test4"); + + let value = unsafe { get_field_vector_of_structs(&root_table, &field, &schema) }; + + assert!(value.is_ok()); + let optional_vector = value.unwrap(); + assert!(optional_vector.is_some()); + let vector = optional_vector.unwrap(); + assert_eq!(vector.len(), 2); + + // Test struct has fields: a:short (offset 0), b:byte (offset 2) + let test_obj = schema + .objects() + .lookup_by_key("MyGame.Example.Test", |o, k| o.key_compare_with_value(k)) + .unwrap(); + let a_field = test_obj + .fields() + .lookup_by_key("a", |f, k| f.key_compare_with_value(k)) + .unwrap(); + let b_field = test_obj + .fields() + .lookup_by_key("b", |f, k| f.key_compare_with_value(k)) + .unwrap(); + + let st0 = vector.get(0); + let a0 = unsafe { get_any_field_integer_in_struct(&st0, &a_field) }.unwrap(); + let b0 = unsafe { get_any_field_integer_in_struct(&st0, &b_field) }.unwrap(); + assert_eq!(a0, 10); + assert_eq!(b0, 20); + + let st1 = vector.get(1); + let a1 = unsafe { get_any_field_integer_in_struct(&st1, &a_field) }.unwrap(); + let b1 = unsafe { get_any_field_integer_in_struct(&st1, &b_field) }.unwrap(); + assert_eq!(a1, 30); + assert_eq!(b1, 40); +} + +#[test] +fn test_buffer_vector_of_structs_wrong_field_fails() { + let buffer = create_test_buffer(); + let root_table = unsafe { get_any_root(&buffer) }; + let schema_buffer = load_file_as_buffer("../monster_test.bfbs"); + let schema = root_as_schema(schema_buffer.as_slice()).unwrap(); + let field = get_schema_field(&schema_buffer, "inventory"); // ubyte vector, not struct vector + + let value = unsafe { get_field_vector_of_structs(&root_table, &field, &schema) }; + + assert!(value.is_err()); +} + #[test] fn test_buffer_table_same_type_succeeds() { let buffer = create_test_buffer(); @@ -2009,6 +2185,26 @@ fn create_test_buffer() -> Vec { &my_game::example::Test::new(5i16, 6i8), ); + let sub_mon1_name = builder.create_string("Sub1"); + let sub_mon1 = my_game::example::Monster::create( + &mut builder, + &my_game::example::MonsterArgs { + name: Some(sub_mon1_name), + hp: 100, + ..Default::default() + }, + ); + let sub_mon2_name = builder.create_string("Sub2"); + let sub_mon2 = my_game::example::Monster::create( + &mut builder, + &my_game::example::MonsterArgs { + name: Some(sub_mon2_name), + hp: 200, + ..Default::default() + }, + ); + let testarrayoftables = builder.create_vector(&[sub_mon1, sub_mon2]); + let args = my_game::example::MonsterArgs { hp: 32767, testf: 3.14, @@ -2033,6 +2229,7 @@ fn create_test_buffer() -> Vec { my_game::example::Test::new(30, 40), ])), testarrayofstring: Some(builder.create_vector(&[s0, s1])), + testarrayoftables: Some(testarrayoftables), ..Default::default() }; my_game::example::Monster::create(&mut builder, &args)