flatbuffers icon indicating copy to clipboard operation
flatbuffers copied to clipboard

[Dart] Deserializing an image requires copying the bytes - fb 23.5.26 / dart 3.2.3 / macos 13.6.1

Open tompark opened this issue 2 years ago • 5 comments

  • flatc 23.5.26
  • Flatbuffers for Dart (flat_buffers) 23.5.26
  • Dart SDK 3.2.3 / Flutter 3.16.3
  • macOS Ventura 13.6.1

Context: Hi I'm making a Flutter app and need downloadable resource bundles. Flutter's Asset Bundles are supposedly platform-specific (i.e. requires building different bundles for Android vs iOS), so I'm trying to use Flatbuffers as a cross-platform bundle format. Could use Protobufs, but zero-copy deserialization was attractive, so I'm looking at Flatbuffers and Cap'n Proto. Each asset bundles will contain not just structs of basetypes but also several images.

Problem: I can't seem to be able to access the image bytes as Uint8List. The only available accessor is a List<int> which I can't cast to a Uint8List, so it requires copying using Uint8List.fromList().

Here is all the code for the whole project:

fbuf_image.fbs - To keep it simple, let's say for now it's just a string and an image

table FbufImage {
  name: string;
  image1: [ubyte]; 
}
root_type FbufImage;

img2buf.py - This makes a flatbuffer file called bundle1.fbuf using a test.png in the current folder

import flatbuffers
import FbufImage	# generated by 'flatc --python fbuf_image.fbs'

def build_fbuf(name, image_path):
	builder = flatbuffers.Builder(0)
	with open(image_path, "rb") as file1:
		data1 = file1.read()
		image_offset1 = builder.CreateByteVector(data1)
	name_offset = builder.CreateString(name)

	# Make the ImageBundle after all other vectors/strings are built
	FbufImage.Start(builder)
	FbufImage.AddName(builder, name_offset)
	FbufImage.AddImage1(builder, image_offset1)
	ImageBundle = FbufImage.End(builder)
	builder.Finish(ImageBundle)
	return builder.Output()

def main():
	buf = build_fbuf("bundle1", "./test.png")

	with open("bundle1.fbuf", "wb") as file:
		file.write(buf)

if __name__ == '__main__':
	main()

setup.sh - this generates the Flutter project with all the files in the proper places

#! /bin/zsh

# Generate Python (FbufImage.py) and Dart code (fbuf_image_generated.dart)
SCHEMA="fbuf_image.fbs"
flatc --dart --python $SCHEMA
python3 img2buf.py

# Generate the Flutter project
PROJ=fbuf_image	# must be snake_case (https://dart.dev/tools/pub/pubspec#name)
PROJDIR=$PROJ

if [ -d $PROJDIR ] ; then
	cd $PROJDIR
	flutter clean
else
	flutter create --platform=web --org com.tompark --project-name $PROJ $PROJDIR
	cd $PROJDIR
fi

flutter pub add flat_buffers

# Copy a flatbuf asset to project, and add it to pubspec.yaml 
mkdir -p ./assets/fbuf
cp -vf "../bundle1.fbuf" "assets/fbuf"

cat << EOF > assets.txt
    - assets/fbuf/
EOF

PUBSPEC=pubspec.yaml
sed '/^  # assets:/s/# //' $PUBSPEC >$PUBSPEC.tmp		# uncomment the "assets:" section, it has 2 space indentation
sed '/^  assets:/r assets.txt' $PUBSPEC.tmp >$PUBSPEC
rm $PUBSPEC.tmp

cp ../main.dart ./lib/main.dart
cp ../fbuf_image_generated.dart ./lib

main.dart - This is the Flutter app that loads the flatbuffer image and displays it

import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:typed_data';  // ByteBuffer, Uint8List
import 'package:flutter/services.dart';  // rootBundle
import 'package:flat_buffers/flat_buffers.dart' as fbuf;
import 'fbuf_image_generated.dart';  // FbufImage

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flatbuffers Image Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const TestPage(),
    );
  }
}

class TestPage extends StatelessWidget {
  const TestPage({Key? key}) : super(key: key);

  static Future<FbufImage> load(String name) async {
    // Get original file from assets in rootBundle
    final assetBytes = await rootBundle.load("fbuf/${name}.fbuf");
    final assetBuffer = assetBytes.buffer;
    final Uint8List buffer = assetBytes.buffer.asUint8List();
    return FbufImage(buffer);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<FbufImage>(
        future: TestPage.load("bundle1"),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final bundle = snapshot.data as FbufImage ?? FbufImage([]);
            final name = bundle.name ?? "";
            if (bundle.image1 != null) {
              final bytes = bundle.image1 ?? [];
              if (bytes is Uint8List) {
                print("${name}.image1 is already a Uint8List, len=${bytes.length}");
                return Image.memory(bytes as Uint8List);
              }
              print("${name}.image1 is being copied, len=${bytes.length}");
              return Image.memory(Uint8List.fromList(bytes));
            }
          }
          return Center(child: CircularProgressIndicator());
        }
      )
    );
  }
}

I'm running the Flutter program using flutter run -d chrome The terminal output always says: bundle1.image1 is being copied, len=226933

In other words, in the generated Dart file (fbuf_image_generated.dart), the generated class looks like this:

class FbufImage {
  FbufImage._(this._bc, this._bcOffset);
  factory FbufImage(List<int> bytes) {
    final rootRef = fb.BufferContext.fromBytes(bytes);
    return reader.read(rootRef, 0);
  }

  static const fb.Reader<FbufImage> reader = _FbufImageReader();

  final fb.BufferContext _bc;
  final int _bcOffset;

  String? get name => const fb.StringReader().vTableGetNullable(_bc, _bcOffset, 4);
  List<int>? get image1 => const fb.Uint8ListReader().vTableGetNullable(_bc, _bcOffset, 6);

  @override
  String toString() {
    return 'FbufImage{name: ${name}, image1: ${image1}}';
  }
}

The root problem The catch is that the image1 property is returning a List<int>? and it cannot be cast to a Uint8List. This means that the only way to create an image from the bytes is to copy it to a Uint8List by doing this: Image.memory(Uint8List.fromList(bytes));

Is there a way to create the image without copying the bytes?

The whole purpose of trying to use Flatbuffers instead of Protobuf was to avoid copying the byte array. Thank you very much!

tompark avatar Dec 07 '23 08:12 tompark

I just worded this question more succinctly and clearly here: https://stackoverflow.com/questions/77623267/how-to-avoid-copying-the-bytes-in-dart-when-deserializing-an-image-from-a-flatbu

tompark avatar Dec 07 '23 22:12 tompark

Perhaps Uint8ListReader.read and Int8ListReader.read could be changed to stop returning a _FbUint8List and to instead return Uint8List.view(bc.buffer.buffer, listOffset + 4)?

jamesderlin avatar Dec 07 '23 23:12 jamesderlin

Thanks James. I was hoping I could just modify something in the generated Dart code, but you're right. It does appear that this needs to be addressed in the Flatbuffers Dart package.

I am a Dart newbie, having just started this Flutter project a few months ago. I read somewhere that Uint8List is relatively new to Dart, and List<int> was used everywhere before that, and that's still used for backward compatibility. I'm guessing the Flatbuffers Dart package was written before the existence of Uint8List, and maybe for compatibility reasons it's not so easy to just change the API.

I didn't know much about ByteData or ByteBuffer, and didn't feel confident about digging around in the Flatbuffers source code, but spent some time today seeing if I could make sense of the Flatbuffers structure.

The following code is totally heinous, but as a test I wanted to see if it's possible to create the Image without the copy, and it works although I'd never use this code in production. It's a replacement for the TestPage class in the Flutter app given above:

class TestPage extends StatelessWidget {
  const TestPage({Key? key}) : super(key: key);

  static Future<ByteData> load(String name) async {
    // Get original file from assets in rootBundle
    final byteData = await rootBundle.load("fbuf/${name}.fbuf");
    return byteData;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<ByteData>(
        future: TestPage.load("bundle1"),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final ByteData data = snapshot.data!;
            // The first uint32 is the offset to the root vtable 
            // See 'abstract class Reader<T>' in https://github.com/google/flatbuffers/blob/master/dart/lib/flat_buffers.dart#L1014 
            final root_offset = data.getUint32(0, Endian.little);
            final vt_offset = data.getInt32(root_offset, Endian.little);            // vTableSOffset, vtable starts at (root_offset - vt_offset)

            final vt_size = data.getUint16(root_offset - vt_offset, Endian.little);      // vtable size in bytes 
            final obj_size = data.getUint16(root_offset - vt_offset + 2, Endian.little);  // object size in bytes, incl the vt_offset == 4 + (2 fields * 2 byte offset each)
            final field1_offset = data.getUint16(root_offset - vt_offset + 2 + 2, Endian.little);
            final field2_offset = data.getUint16(root_offset - vt_offset + 2 + 2 + 2, Endian.little);
            print("\nroot_offset=${root_offset}, vt_offset=${vt_offset}, vt_size=${vt_size}, obj_size=${obj_size}, field1_offset=${field1_offset}, field2_offset=${field2_offset}");

            final obj_offset = root_offset;
            final name_offset = data.getInt32(obj_offset + field1_offset, Endian.little);
            final img_offset = data.getInt32(obj_offset + field2_offset, Endian.little);
            print("\nname_offset=${name_offset}, img_offset=${img_offset}");

            final int name_data_offset = obj_offset + field1_offset + name_offset;
            final name_len = data.getUint32(name_data_offset, Endian.little);
            final name_bytes = data.buffer.asUint8List(name_data_offset+4, name_len);
            final name_str = utf8.decode(name_bytes);
            print("\nname_data_offset=${name_data_offset}, name_len=${name_len}, name='${name_str}'");

            final int image_data_offset = obj_offset + field2_offset + img_offset;
            final image_len = data.getUint32(image_data_offset, Endian.little);
            print("\nimage_data_offset=${image_data_offset}, image_len=${image_len}");

            final Uint8List image_bytes = data.buffer.asUint8List(image_data_offset+4, image_len);
            if (image_bytes.length == image_len) {
              print("using image bytes hacked from ByteData, len=${image_bytes.length}");
              return Image.memory(image_bytes);
            }

            final bundle = FbufImage(data.buffer.asUint8List());
            final name = bundle.name ?? "";
            if (bundle.image1 != null) {
              final bytes = bundle.image1 ?? [];
              if (bytes is Uint8List) {
                print("${name}.image1 is already a Uint8List, len=${bytes.length}");
                return Image.memory(bytes as Uint8List);
              }
              print("${name}.image1 is being copied, len=${bytes.length}");
              return Image.memory(Uint8List.fromList(bytes));
            }
          }
          return Center(child: CircularProgressIndicator());
        }
      )
    );
  }
}

I guess this exercise wasn't helpful for any practical use, except for my own education. Since there are alternatives I could use, maybe I won't use Flatbuffers for my project after all. Should I just close this issue? If any of the project contributors see this message, feel free to close this issue. Thanks anyway!

tompark avatar Dec 08 '23 23:12 tompark

I personally think that this would be worth doing, especially if it's as simple as my suggestion from https://github.com/google/flatbuffers/issues/8183#issuecomment-1846282614.

jamesderlin avatar Dec 08 '23 23:12 jamesderlin

Oh geez, I just realized that it was you who wrote that backward compatibility thing about Uint8List vs List<int>.

https://stackoverflow.com/questions/69090275/uint8list-vs-listint-what-is-the-difference/69091484#69091484

There are a number of functions that take or return sequences of bytes as List<int>, but usually that's for historical reasons, and changing those APIs to use Uint8List now would break existing code. (There was an attempt to change the APIs, but it broke more code than expected and changes to some of the APIs had to be reverted.)

tompark avatar Dec 12 '23 19:12 tompark

Thanks @tompark for the suggestions. Indeed, the use case looks very reasonable. Maybe it would be enough to implement Uint8List on _FbUint8List?

If you'd like to experiment and contribute a PR, I'd be more than willing to review and help landing it.

vaind avatar Feb 08 '24 13:02 vaind

I am extremely interested in seeing this land and would be willing to help on the review/testing. My use case isn't quite an image, but rather packed signal data that is parsed as a Uint8List. I leaned towards protobufs in design initially because I was under the impression it was impossible to avoid the copy during deserialization.

NotTsunami avatar Apr 18 '24 18:04 NotTsunami

Should other reader classes drop their wrapped type for the dart:typed_data equivalent (eg Uint16ListReader drops _FbUint16List for Uint16List)?

NotTsunami avatar Apr 18 '24 21:04 NotTsunami

https://github.com/google/flatbuffers/blob/7106d86685e3395efc6df5d2ee5914fe622ed6b9/dart/lib/flat_buffers.dart#L1207-L1213

Would it not be enough to just take this path instead of the lazy path? I don't see where it's possible to configure whether the lazy path is taken though. Seems like the only way is to change the generated code to pass in a false parameter to Uint8ListReader. Sorry for the spam on the thread.

NotTsunami avatar Apr 19 '24 19:04 NotTsunami

Hello. Sorry @vaind and @NotTsunami, for my lack of an earlier reply.

In Dec I did take a quick look at trying to change the Uint32ListReader.read return type as suggested by @jamesderlin, but:

  • You'd have to change not only the dart library code, but also the flatc code generator.
  • It's not backward-compatible. You'd be changing the return type of that class, so any existing app code that calls this API will break or at least cause warnings.

Why you'd have to change the flatc code generator:

For the fbuf_image.fbs example I gave above, here's what flatc --dart fbuf_image.fbs generated:

class FbufImage {
  FbufImage._(this._bc, this._bcOffset);
  factory FbufImage(List<int> bytes) {
    final rootRef = fb.BufferContext.fromBytes(bytes);
    return reader.read(rootRef, 0);
  }

  static const fb.Reader<FbufImage> reader = _FbufImageReader();

  final fb.BufferContext _bc;
  final int _bcOffset;

  String? get name => const fb.StringReader().vTableGetNullable(_bc, _bcOffset, 4);
  List<int>? get image1 => const fb.Uint8ListReader().vTableGetNullable(_bc, _bcOffset, 6);

  @override
  String toString() {
    return 'FbufImage{name: ${name}, image1: ${image1}}';
  }
}

As you can see, the get image1 computed property returns a List<int>?. If you want it to return a Uint8List? instead, you have to modify the flatc app.

Backward-compatibility vs adding new APIs

The Uint8ListReader.read function is a required implementation of the Reader<T> protocol so you could just copy-paste a duplicate of each reader with the desired type: So, instead of: class Uint8ListReader extends Reader<List<int>> Smtg like this: class Uint8ListReader2 extends Reader<Uint8List>

But you'd have to do this for every type of list that you wanted, i.e. UInt16List, UInt32List, Float64List, etc.

I myself didn't want to clutter up the official library with copy of all of these accessor classes.

If someone else wants to do this, it seems straightforward.

tompark avatar Apr 19 '24 22:04 tompark

What I did instead It would have been nice to contribute something upstream, but what I ended up doing wasn't conductive to that.

As simple as dart/lib/flat_buffers.dart may be, honestly I was very uncomfortable touching it in any way (esp. that data accessor Reader<T> protocol with its vTableGetNullable function and that private BufferContext class). The generated code seems surprisingly unfriendly and highly coupled with internals of the library code.

Since I'd already bothered to figure out the file format, I encoded the binary files using Python and just wrote my own flatbuffer decoder class in Dart.

I was thinking some time I'd write this up as a blog post but I haven't gotten around to it yet, and since others seem to need code like this now, I'll just post it here.

I'm not suggesting that this is a good idea, but if anyone wants to use my code, here it is. (Not to mention that the test code below can serve as half-decent examples of encoding more complex kinds of flatbuffers.)

flatbuffer.dart

import 'dart:typed_data';				// ByteData, Uint8List
import 'dart:convert' show utf8;

const bool dbg = true;

const int _sizeofUint16 = 2;
const int _sizeofUint32 = 4;

class Flatbuffer {
	late final ByteData data;
	late final int root;	// data offset in bytes
	late final int vTable;	// data offset in bytes
	late final int vCount;	// number of fields in vtable

	Flatbuffer(ByteData this.data, [int rootOffset = -1]) {
		final int len = data.lengthInBytes;
		if (rootOffset < 0) {
			rootOffset = (len < 4) ? 0 : data.getUint32(0, Endian.little);
		}
		root = rootOffset;

		final int vTableSOffset = (root <= 0 || root >= len-4) ? 0 : data.getInt32(root, Endian.little);
		final int vTableOffset = root - vTableSOffset;
		final int vTableSize = (vTableOffset < 0) ? 0 : data.getUint16(vTableOffset, Endian.little);
		final int vHeaderSize = (vTableOffset < 0) ? 0 : data.getUint16(vTableOffset+2, Endian.little);
		if (dbg) print("[FB] datalen=${len}, rootOffset=${rootOffset}, vtOffset=${vTableOffset}, vtSize=${vTableSize}, hdrSize=${vHeaderSize}");

		if (vTableSize <= 0) {
			vTable = 0;
			vCount = 0;
		} else {
			vTable = vTableOffset + (_sizeofUint16 * 2);	// add 4 bytes to step past vTableSize,vObjSize
			vCount = (vTableSize - (_sizeofUint16 * 2)) ~/ 2;
		}
		if (dbg) print("[FB] table entries offset=${vTable}, numFields=${vCount}");
	}

	// derefField
	// If field is scalar/struct, return the offset to the scalar value or the struct's in-line data block
	// If field is vector/string, return the offset to the pointer to the data (use 'derefPointer' to get offset of the data)
	@pragma('vm:prefer-inline')
	int derefField(int idx) => (idx < 0 || idx >= vCount) ? 0 : data.getUint16((vTable + idx*2), Endian.little);

	@pragma('vm:prefer-inline')
	int derefPointer(int offset) => offset + data.getUint32(offset, Endian.little);

	// vecFieldData
	// If field is a single blob, then the blob is prefixed with uint32 length
	// If field is an array of struct, then the array is prefixed with uint32 count (the array length or element size is not stored anywhere)
	(int,int) vecFieldData(int fieldIdx) {
		final fieldOffset = derefField(fieldIdx);
		//print("[FB.bytesField] field[${fieldIdx}].offset=${fieldOffset} ${ fieldOffset==0?'RETURNING NULL':'' }");
		if (fieldOffset == 0) return (0,0);

		final dataOffset = derefPointer(root + fieldOffset);
		//print("[FB.bytesField] data offset=${dataOffset}");
		if (dataOffset > data.lengthInBytes) return (0,0);

		final length = data.getUint32(dataOffset, Endian.little);
		if (dbg) print("FB.bytes: field[${fieldIdx}].delta=${fieldOffset}, data[${root+fieldOffset}] --> data[${dataOffset}], len=${length}");
		if (dataOffset + length > data.lengthInBytes) return (0,0);
		return (dataOffset + _sizeofUint32, length);
	}

	Uint8List? bytesField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asUint8List(offset, length);
	}

	String? stringField(int fieldIdx) {
		final bytes = bytesField(fieldIdx);
		return (bytes == null) ? null : utf8.decode(bytes);
	}

	Float32List? float32ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asFloat32List(offset, length);
	}
	Float64List? float64ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asFloat64List(offset, length);
	}
	Int16List? int16ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asInt16List(offset, length);
	}
	Int32List? int32ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asInt32List(offset, length);
	}
	Int64List? int64ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asInt64List(offset, length);
	}
	Int8List? int8ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asInt8List(offset, length);
	}
	Uint16List? uint16ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asUint16List(offset, length);
	}
	Uint32List? uint32ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asUint32List(offset, length);
	}
	Uint64List? uint64ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asUint64List(offset, length);
	}
	Uint8List? uint8ListField(int fieldIdx) {
		final (offset, length) = vecFieldData(fieldIdx);
		return (length == 0) ? null : data.buffer.asUint8List(offset, length);
	}

	// SCALARS -- the value is inline so DO NOT USE 'derefPointer'

	int? structOffset(int fieldIdx) {
		final valueOffset = derefField(fieldIdx);
		if (dbg) print("FB.struct: field[${fieldIdx}].delta=${valueOffset} --> ${ valueOffset==0? 'NULL': 'data[${root+valueOffset}]' }");
		return (valueOffset == 0) ? null : root + valueOffset;
	}

	int? uint16Field(int fieldIdx) {
		final valueOffset = derefField(fieldIdx);
		if (dbg) print("FB.uint16: field[${fieldIdx}].delta=${valueOffset} --> ${ valueOffset==0? 'NULL': 'data[${root+valueOffset}]' }");
		return (valueOffset == 0) ? null : data.getUint16(root + valueOffset, Endian.little);
	}

	double? float32Field(int fieldIdx) {
		final valueOffset = derefField(fieldIdx);
		if (dbg) print("FB.float: field[${fieldIdx}].delta=${valueOffset} --> ${ valueOffset==0? 'NULL': 'data[${root+valueOffset}]' }");
		return (valueOffset == 0) ? null : data.getFloat32(root + valueOffset, Endian.little);
	}
}

That doesn't work with the flatc generated code, but the data accessors in the above decoder are trivial to use.

Here are examples for deserializing tables containing a Uint8List, a table containing an inlined struct, and a table containing an array of struct with nested structs:

test_types.dart

import 'dart:typed_data';				// ByteData, Uint8List
import 'flatbuffer.dart';

// table Test01 {
//   name: string;
//   image1: [ubyte]; 
// }
class Test01 extends Flatbuffer {
	Test01(ByteData bd) : super(bd);

	// Fields
	String? get name => stringField(0);
	Uint8List? get image1 => bytesField(1);

	@override
	String toString() => 'Test01{name: ${name}, image1.len: ${image1?.lengthInBytes}}';
}

// table Test02 {
//   x: uint16;
//   y: uint16;
//   dx: float;
//   dy: float;
//   name: string;
//   image1: [ubyte]; 
// }
class Test02 extends Flatbuffer {
	Test02(ByteData bd) : super(bd);

	// Fields
	int? get x => uint16Field(0);
	int? get y => uint16Field(1);
	double? get dx => float32Field(2);
	double? get dy => float32Field(3);
	String? get name => stringField(4);
	Uint8List? get image1 => bytesField(5);

	@override
	String toString() => 'Test02{x=${x}, y=${y}, dx=${dx}, dy=${dy}, name: ${name}, image1.len: ${image1?.lengthInBytes}}';
}


// struct Rect {
//   left: float;
//   top: float;
//   width: float;
//   height: float;
// }
class Rect {
	static const size = 16;
	final ByteData bd;
	final int bdOffset;
	const Rect(this.bd, this.bdOffset);

	double get left => bd.getFloat32(bdOffset + 0, Endian.little);
	double get top => bd.getFloat32(bdOffset + 4, Endian.little);
	double get width => bd.getFloat32(bdOffset + 8, Endian.little);
	double get height => bd.getFloat32(bdOffset + 12, Endian.little);
}
// table Test03 {
//   id: uint16;
//   rect: Rect;
//   f4: [float];
//   name: string;
//   image1: [ubyte]; 
// }
class Test03 extends Flatbuffer {
	Test03(ByteData bd) : super(bd);

	// Fields
	int? get id => uint16Field(0);
	Rect? get rect {
		final offset = structOffset(1);
		return (offset == null) ? null : Rect(data, offset);
	}
	Float32List? get f4 => float32ListField(2);
	String? get name => stringField(3);
	Uint8List? get image1 => bytesField(4);

	// @override
	// String toString() => 'Test02{id: ${id}, rect:[${left},${top},${width},${height}], f4:[${f4[0]},${f4[1]},${f4[2]},${f4[3]}], name: ${name}, image1.len: ${image1?.lengthInBytes}}';
}

// struct Item {
// 	final uint16 type;
// 	final Rect btnRect;
// 	final Rect imgRect;
// }
class Item {
	static const size = 36;
	final ByteData bd;
	final int bdOffset;
	const Item(this.bd, this.bdOffset);

	int? get type => bd.getUint16(bdOffset + 0, Endian.little);
	//padding 2 bytes
	Rect get btnRect => Rect(bd, bdOffset + 4);
	Rect get imgRect => Rect(bd, bdOffset + 4 + Rect.size);

	// Rect? get btnRect {
	// 	final offset = structOffset(1);
	// 	return (offset == null) ? null : Rect(data, offset);
	// }
	// Rect? get imgRect {
	// 	final offset = structOffset(2);
	// 	return (offset == null) ? null : Rect(data, offset);
	// }
}

// table Test04 {
//   id: uint16;
//   numItems: uint16;
//   items: [Item];
// }
class Test04 extends Flatbuffer {
	Test04(ByteData bd) : super(bd);

	// Fields
	int? get id => uint16Field(0);
	int? get numItems => uint16Field(1);
	List<Item> get items {
		final (offset, count) = vecFieldData(2);
		// This should be the itemCount
		const size = Item.size; // The element size is hard-coded in the generated dart code

		// final int size = length ~/ count;
		// final rem = length % count;
		// if (rem != 0) print('WARNING: vector length is not a multiple of item count: len=${length}, count=${count}, elemSize=${size}, remainder=${rem}');
		if (dbg) print('[List<Item>] offset=${offset}, numItems=${numItems} == itemCount=${count}, itemSize=${size} (bytes)');
		List<Item> items = [];
		for (var idx=0; idx<count; idx++) {
			items.add(Item(data, (idx*size)+offset));
		}
		return items;
	} 

	// @override
	// String toString() => 'Test02{id: ${id}, rect:[${left},${top},${width},${height}], f4:[${f4[0]},${f4[1]},${f4[2]},${f4[3]}], name: ${name}, image1.len: ${image1?.lengthInBytes}}';
}

This is an example main.dart that uses the above tables:

main.dart

import 'package:flutter/material.dart';

import 'dart:io';
import 'dart:typed_data';								// ByteBuffer, Uint8List
import 'dart:convert' show utf8;
import 'package:flutter/services.dart';			// rootBundle

import 'flatbuffer.dart' as fbuf;
import 'test_types.dart';


void main() {
	WidgetsFlutterBinding.ensureInitialized();
	runApp(const MainApp());
}

class MainApp extends StatelessWidget {
	const MainApp({super.key});

	@override
	Widget build(BuildContext context) {
		return MaterialApp(
			title: 'Flatbuffers Demo',
			theme: ThemeData(
				colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
				useMaterial3: true,
			),
			home: const TestPage(),
		);
	}
}


class TestPage extends StatelessWidget {
	const TestPage({Key? key}) : super(key: key);

	static Future<ByteData> load(String name) async {
		final fname = "fbuf/fbuf_${name}.bin";
		// Get original file from assets in rootBundle
		final byteData = await rootBundle.load(fname);
		print("[MAIN] loaded file: '$fname', len: ${byteData.lengthInBytes}");

		//final assetBuffer = assetBytes.buffer;								// ByteBuffer
		//final Uint8List buffer = assetBytes.buffer.asUint8List();
		return byteData;
	}

	static String? getNameField(Object obj) {
		return (obj is Test01) ? obj.name
			: (obj is Test02) ? obj.name
			: (obj is Test03) ? obj.name
			: null;
	}
	static Uint8List? getImageField(Object obj) {
		return (obj is Test01) ? obj.image1
			: (obj is Test02) ? obj.image1
			: (obj is Test03) ? obj.image1
			: null;
	}

	@override
	Widget build(BuildContext context) {
		const String testName = "test04";
		return Scaffold(
			body: FutureBuilder<ByteData>(
				future: TestPage.load(testName),
				builder: (context, snapshot) {
					if (snapshot.hasData) {
						final ByteData data = snapshot.data!;

						final bundle = switch(testName) {
							"test01" => Test01(data),
							"test02" => Test02(data),
							"test03" => Test03(data),
							"test04" => Test04(data),
							_ => Test01(data),
						};
						if (bundle is Test02) {
							final int? x = bundle.x;
							final int? y = bundle.y;
							final double? dx = bundle.dx;
							final double? dy = bundle.dy;
							print("[MAIN] Scalars: x=${x}, y=${y}, dx=${dx?.toStringAsFixed(4)}, dy=${dy?.toStringAsFixed(4)}\n");
						}
						else if (bundle is Test03) {
							final int? id = bundle.id;
							final Rect? rect = bundle.rect;
							final double? left = rect?.left;
							final double? top = rect?.top;
							final double? width = rect?.width;
							final double? height = rect?.height;
							print("[MAIN] Scalars: id=${id}, rect=[${left?.toStringAsFixed(4)},${top?.toStringAsFixed(4)},${width?.toStringAsFixed(4)},${height?.toStringAsFixed(4)}] \n");
							final Float32List f4 = bundle.f4 ?? Float32List.fromList([-1,-1,-1,-1]);
							print("[MAIN] Vec: f4=[${f4[0].toStringAsFixed(4)},${f4[1].toStringAsFixed(4)},${f4[2].toStringAsFixed(4)},${f4[3].toStringAsFixed(4)}] \n");
						}
						else if (bundle is Test04) {
							final int? id = bundle.id;
							final int? numItems = bundle.numItems;
							print("[MAIN] Scalars: id=${id}, count=${numItems}\n");

							final List<Item> items = bundle.items;
							final int count = items.length;
							for (int idx=0; idx<count; idx++) {
								final int type = items[idx]?.type ?? 0;
								final Rect? btnRect = items[idx]?.btnRect;
								final Rect? imgRect = items[idx]?.imgRect;
								print(" [${idx}]: type=${type}");
								print(" [${idx}]: btnRect=[${btnRect?.left?.toStringAsFixed(4)},${btnRect?.top?.toStringAsFixed(4)},${btnRect?.width?.toStringAsFixed(4)},${btnRect?.height?.toStringAsFixed(4)}]");
								print(" [${idx}]: imgRect=[${imgRect?.left?.toStringAsFixed(4)},${imgRect?.top?.toStringAsFixed(4)},${imgRect?.width?.toStringAsFixed(4)},${imgRect?.height?.toStringAsFixed(4)}]");
							}
						}
						else {
						}
						final name = getNameField(bundle) ?? ""; //bundle.name ?? "";
						final Uint8List bytes = getImageField(bundle) ?? Uint8List(0); //bundle.image1 ?? Uint8List(0);
						if (name != null) print("[MAIN] Vec: name='${name}'");
						if (bytes.length > 0) {
							if (bytes is Uint8List) {
								print("[MAIN] image1 is already a Uint8List, len=${bytes.length}");
								return Image.memory(bytes as Uint8List);
							}
							print("[MAIN] image1 is being copied, len=${bytes.length}");
							return Image.memory(Uint8List.fromList(bytes));
							//return Image.memory((bytes as Uint8List).bc.buffer.buffer.asUint8List(6, bytes.length));
						}
					}
					return Center(child: CircularProgressIndicator());
				}
			)
		);
	}
}

And for completeness, here are the .fbs files and the Python scripts to create the flatbuffer binaries.

test01.fbs

table Test01 {
  name: string;
  image1: [ubyte]; 
}
root_type Test01;

fbuf01.py

import flatbuffers

import Test01	# generated by 'flatc --python test01.fbs'

def build_fbuf(name, image_path):

	builder = flatbuffers.Builder(0)

	with open(image_path, "rb") as file1:
		data1 = file1.read()
		image_offset1 = builder.CreateByteVector(data1)

	name_offset = builder.CreateString(name)

	# Make the ImageBundle after all other vectors/strings are built
	Test01.Start(builder)
	Test01.AddName(builder, name_offset)
	Test01.AddImage1(builder, image_offset1)
	ImageBundle = Test01.End(builder)
	builder.Finish(ImageBundle)
	return builder.Output()


def main():
	buf = build_fbuf("bundle1", "./test.png")

	with open("fbuf_test01.bin", "wb") as file:
		file.write(buf)

if __name__ == '__main__':
	main()

test02.fbs

table Test02 {
  x: uint16;
  y: uint16;
  dx: float;
  dy: float;
  name: string;
  image1: [ubyte]; 
}
root_type Test02;

fbuf02.py

import flatbuffers

import Test02	# generated by 'flatc --python test02.fbs'

def build_fbuf(name, image_path):

	builder = flatbuffers.Builder(0)

	with open(image_path, "rb") as file1:
		data1 = file1.read()
		image_offset1 = builder.CreateByteVector(data1)

	name_offset = builder.CreateString(name)

	# Make the ImageBundle after all other vectors/strings are built
	# Add the fields in reverse order (bottom up)
	#	image1, name, dy, dx, y, x
	Test02.Start(builder)
	Test02.AddImage1(builder, image_offset1)
	Test02.AddName(builder, name_offset)
	Test02.AddDy(builder, 0.2)
	Test02.AddDx(builder, 0.1)
	Test02.AddY(builder, 688)
	Test02.AddX(builder, 42)
	ImageBundle = Test02.End(builder)
	builder.Finish(ImageBundle)
	return builder.Output()


def main():
	buf = build_fbuf("bundle2", "./test.png")

	with open("fbuf_test02.bin", "wb") as file:
		file.write(buf)

if __name__ == '__main__':
	main()

test03.fbs

struct Rect {
  left: float;
  top: float;
  width: float;
  height: float;
}
table Test03 {
  id: uint16;
  rect: Rect;
  f4: [float];
  name: string;
  image1: [ubyte]; 
}
root_type Test03;

fbuf03.py

import flatbuffers

import Test03	# generated by 'flatc --python test03.fbs'
# import Rect

def build_fbuf(name, image_path):

	builder = flatbuffers.Builder(0)

	# table Test03 {
	# 	id: uint16;
	# 	rect: Rect;
	# 	f4: [float];
	# 	name: string;
	# 	image1: [ubyte]; 
	# }
	id_value = 42
	left_value = 11.1
	top_value = 22.2
	width_value = 33.3
	height_value = 44.4
	f4_values = [1.01, 2.02, 3.03, 4.04]
	# name = 'bundle3'
	# image_path = './test.png'

	# Build in bottom-up order: image1, name, f4, rect, id	

	# Build 'image1'
	with open(image_path, "rb") as file1:
		data1 = file1.read()
		image_offset1 = builder.CreateByteVector(data1)

	# Build 'name'
	name_offset = builder.CreateString(name)

	# Build 'f4' (array of float)
	f4_offsets = []
	builder.StartVector(4, 4, 4)	# elemSize:4, numElems:4, alignment:4
	for val in reversed(f4_values):
		offset = builder.PrependFloat32(val)
		f4_offsets.append(offset)
	f4_offset = builder.EndVector()

	# Build rect?
	# NO: a Flatbuffer struct is stored in-line
	# If it's built ahead of time here, then later when calling 'Test03.AddRect()', Flatbuffers throws an exception:
	#	"flatbuffers.builder.StructIsNotInlineError: flatbuffers: Tried to write a Struct at an Offset that is different from the current Offset of the Builder."
	# builder.Prep(4, 16)		# size: 4, dataBytes: 16 
	# builder.PrependFloat32(height_value)
	# builder.PrependFloat32(width_value)
	# builder.PrependFloat32(top_value)
	# builder.PrependFloat32(left_value)
	# rect_offset = builder.Offset()

	# Make the ImageBundle after all other vectors/strings are built 
	# Add the fields in reverse order (bottom up)
	#	image1, name, dy, dx, y, x
	Test03.Start(builder)
	Test03.AddImage1(builder, image_offset1)
	Test03.AddName(builder, name_offset)
	Test03.AddF4(builder, f4_offset)

	# Rect
	# A Flatbuffer struct is stored in-line, so build it here instead of ahead of time 
	# rect_offset = Rect.CreateRect(builder, left, top, width, height)	# see the generated 'Rect.py'
	builder.Prep(4, 16)		# size: 4, dataBytes: 16 
	builder.PrependFloat32(height_value)
	builder.PrependFloat32(width_value)
	builder.PrependFloat32(top_value)
	builder.PrependFloat32(left_value)
	rect_offset = builder.Offset()
	Test03.AddRect(builder, rect_offset)

	Test03.AddId(builder, id_value)
	Bundle = Test03.End(builder)
	builder.Finish(Bundle)
	return builder.Output()


def main():
	buf = build_fbuf("bundle3", "./test.png")

	with open("fbuf_test03.bin", "wb") as file:
		file.write(buf)

if __name__ == '__main__':
	main()

test04.fbs

struct Rect {
  left: float;
  top: float;
  width: float;
  height: float;
}

struct Item {
  type: uint16;
  btn_rect: Rect;
  img_rect: Rect;
}

table Test04 {
  id: uint16;
  num_items: uint16;
  items: [Item];
}

root_type Test04;

fbuf04.py

import flatbuffers
import json
from types import SimpleNamespace

import Test04	# generated by 'flatc --python test04.fbs'
# import Rect

def build_item(builder, item):

	# struct Item {
	# 	final uint16 type;
	# 	final Rect btn_rect;
	# 	final Rect img_rect;
	# }
	type = item.type
	btnRect = item.btn_rect
	imgRect = item.img_rect

	# see the generated 'Item.py'
	# item_offset = Item.CreateItem(builder, type, btnRect_left, btnRect_top, btnRect_width, btnRect_height, imgRect_left, imgRect_top, imgRect_width, imgRect_height):
	builder.Prep(4, 36)
	builder.Prep(4, 16)
	builder.PrependFloat32(imgRect.height)
	builder.PrependFloat32(imgRect.width)
	builder.PrependFloat32(imgRect.top)
	builder.PrependFloat32(imgRect.left)
	builder.Prep(4, 16)
	builder.PrependFloat32(btnRect.height)
	builder.PrependFloat32(btnRect.width)
	builder.PrependFloat32(btnRect.top)
	builder.PrependFloat32(btnRect.left)
	builder.Pad(2)
	builder.PrependUint16(type)
	return builder.Offset()

def build_fbuf(name, obj):

	# table Test04 {
	#   id: uint16;
	#   num_items: uint16;
	#   items: [Item];
	# }
	id_value = obj.id
	count_value = obj.num_items
	items = obj.items

	# Build in bottom-up order: items, numItems, id	
	builder = flatbuffers.Builder(0)

	# Build 'items' (array of Item)
	# vec_offsets = []
	builder.StartVector(36, len(items), 4)	# elemSize:36 (4+16+16), numElems:x, alignment:4
	for item in reversed(items):
		offset = build_item(builder, item)
		# vec_offsets.append(offset)
		print(f"   item offset: {offset}")
	items_offset = builder.EndVector()
	print(f"vector of items offset: {items_offset}")

	# Make the bundle after all other vectors/strings are built 
	# Add the fields in reverse order (bottom up)
	#	items, num_items, 
	Test04.Start(builder)
	Test04.AddItems(builder, items_offset)
	Test04.AddNumItems(builder, len(items))
	Test04.AddId(builder, id_value)
	Bundle = Test04.End(builder)
	builder.Finish(Bundle)
	return builder.Output()


def main():

	obj = json.loads(
		'''{
			"id": 42, "num_items": 4,
			"items": [
				{"type":1, "btn_rect":{"left":0.21, "top":0.26, "width":0.17, "height":0.17}, "img_rect":{"left":0.0, "top":0.05, "width":0.66, "height":0.64}},
				{"type":2, "btn_rect":{"left":0.65, "top":0.18, "width":0.17, "height":0.17}, "img_rect":{"left":0.19, "top":0.125, "width":1.24, "height":0.92}},
				{"type":3, "btn_rect":{"left":0.43, "top":0.41, "width":0.17, "height":0.17}, "img_rect":{"left":0.2, "top":0.22, "width":0.8, "height":0.8}},
				{"type":4, "btn_rect":{"left":0.60, "top":0.72, "width":0.17, "height":0.17}, "img_rect":{"left":0.505, "top":0.615, "width":0.82, "height":0.965}}
			]
		}''',
		object_hook=lambda d: SimpleNamespace(**d)
		# See https://stackoverflow.com/questions/6578986/how-to-convert-json-data-into-a-python-object
	)
	buf = build_fbuf("bundle4", obj)

	with open("fbuf_test04.bin", "wb") as file:
		file.write(buf)

if __name__ == '__main__':
	main()

tompark avatar Apr 19 '24 22:04 tompark

  • It's not backward-compatible. You'd be changing the return type of that class, so any existing app code that calls this API will break or at least cause warnings.

No. Uint8ListReader.read and Int8ListReader.read both return List<int>. If they returned Uint8List.view(...) as I suggested, the return type would not need to change; Uint8List is a subtype of List<int>. Of course, List<int> is a wider type than necessary and isn't ideal, but it still allow callers to cast to the narrower Uint8List type without creating a copy.

jamesderlin avatar Apr 19 '24 22:04 jamesderlin

No. Uint8ListReader.read and Int8ListReader.read both return List<int>. If they returned Uint8List.view(...) as I suggested, the return type would not need to change; Uint8List is a subtype of List<int>. Of course, List<int> is a wider type than necessary and isn't ideal, but it still allow callers to cast to the narrower Uint8List type without creating a copy.

OK, I'm wrong and obviously I don't know about enough about Uint8List. I said earlier that I do not feel comfortable with submitting a pull request, but since that seems to be the way I'm supposed to resolve this issue other than closing it, then I'll just close the issue. If someone else wants to submit the pull request then they can open a new issue or reopen this one.

tompark avatar Apr 19 '24 23:04 tompark

To reiterate, what I'm proposing is to change Uint8ListReader.read and Int8ListReader.read to return a Uint8List/Int8List along all code paths instead of just along some of them. Currently the lazy paths returns a _FbUint8List, so that prevents callers from casting the result to a Uint8List/Int8List. All of this happens dart/lib/flat_buffers.dart, so I don't think it requires any changes for code generation. In the generated code, a line such as:

List<int>? get image1 => const fb.Uint8ListReader().vTableGetNullable(_bc, _bcOffset, 6);

calls into Uint8ListReader().vTableGetNullable which I think returns whatever Uint8ListReader.read returns.

To be precise, I think the following should work?

diff --git a/dart/lib/flat_buffers.dart b/dart/lib/flat_buffers.dart
index 6f307872..113364e1 100644
--- a/dart/lib/flat_buffers.dart
+++ b/dart/lib/flat_buffers.dart
@@ -729,7 +729,7 @@ class Builder {
 
   @pragma('vm:prefer-inline')
   void _writeUTFString(String value) {
-    final bytes = utf8.encode(value) as Uint8List;
+    final bytes = utf8.encode(value);
     final length = bytes.length;
     _prepare(4, 1, additionalBytes: length + 1);
     _setUint32AtTail(_tail, length);
@@ -1202,7 +1202,7 @@ class Uint8ListReader extends Reader<List<int>> {
   @pragma('vm:prefer-inline')
   List<int> read(BufferContext bc, int offset) {
     final listOffset = bc.derefObject(offset);
-    if (lazy) return _FbUint8List(bc, listOffset);
+    if (lazy) return Uint8List.view(bc.buffer.buffer, listOffset + 4);
 
     final length = bc._getUint32(listOffset);
     final result = Uint8List(length);
@@ -1247,7 +1247,7 @@ class Int8ListReader extends Reader<List<int>> {
   @pragma('vm:prefer-inline')
   List<int> read(BufferContext bc, int offset) {
     final listOffset = bc.derefObject(offset);
-    if (lazy) return _FbUint8List(bc, listOffset);
+    if (lazy) return Int8List.view(bc.buffer.buffer, listOffset + 4);
 
     final length = bc._getUint32(listOffset);
     final result = Int8List(length);
@@ -1337,24 +1337,6 @@ class _FbUint16List extends _FbList<int> {
   int operator [](int i) => bc._getUint16(offset + 4 + 2 * i);
 }
 
-/// List backed by 8-bit unsigned integers.
-class _FbUint8List extends _FbList<int> {
-  _FbUint8List(BufferContext bc, int offset) : super(bc, offset);
-
-  @override
-  @pragma('vm:prefer-inline')
-  int operator [](int i) => bc._getUint8(offset + 4 + i);
-}
-
-/// List backed by 8-bit signed integers.
-class _FbInt8List extends _FbList<int> {
-  _FbInt8List(BufferContext bc, int offset) : super(bc, offset);
-
-  @override
-  @pragma('vm:prefer-inline')
-  int operator [](int i) => bc._getInt8(offset + 4 + i);
-}
-
 /// List backed by 8-bit unsigned integers.
 class _FbBoolList extends _FbList<bool> {
   _FbBoolList(BufferContext bc, int offset) : super(bc, offset);

(I dislike the magic 4 offsets and would prefer replacing them with symbolic constants if I understood what it represented. At any rate, the code currently already uses them, so it's not any worse. Additionally, the old Int8ListReader.read used _FbUint8List instead of _FbInt8List, and _FbInt8List was unused, which was probably a bug.)

I would be fine creating a pull request, but I am unfamiliar with flatbuffers and have no idea how to test the change. Otherwise, although I hate having to keep asking him for favors, perhaps @dnfield could help or provide additional insight; it appears that he originally wrote this file.

jamesderlin avatar Apr 21 '24 17:04 jamesderlin

To reiterate, what I'm proposing is to change Uint8ListReader.read and Int8ListReader.read to return a Uint8List/Int8List along all code paths instead of just along some of them. Currently the lazy paths returns a _FbUint8List, so that prevents callers from casting the result to a Uint8List/Int8List. All of this happens dart/lib/flat_buffers.dart, so I don't think it requires any changes for code generation. In the generated code, a line such as:

List<int>? get image1 => const fb.Uint8ListReader().vTableGetNullable(_bc, _bcOffset, 6);

calls into Uint8ListReader().vTableGetNullable which I think returns whatever Uint8ListReader.read returns.

To be precise, I think the following should work?

diff --git a/dart/lib/flat_buffers.dart b/dart/lib/flat_buffers.dart
index 6f307872..113364e1 100644
--- a/dart/lib/flat_buffers.dart
+++ b/dart/lib/flat_buffers.dart
@@ -729,7 +729,7 @@ class Builder {
 
   @pragma('vm:prefer-inline')
   void _writeUTFString(String value) {
-    final bytes = utf8.encode(value) as Uint8List;
+    final bytes = utf8.encode(value);
     final length = bytes.length;
     _prepare(4, 1, additionalBytes: length + 1);
     _setUint32AtTail(_tail, length);
@@ -1202,7 +1202,7 @@ class Uint8ListReader extends Reader<List<int>> {
   @pragma('vm:prefer-inline')
   List<int> read(BufferContext bc, int offset) {
     final listOffset = bc.derefObject(offset);
-    if (lazy) return _FbUint8List(bc, listOffset);
+    if (lazy) return Uint8List.view(bc.buffer.buffer, listOffset + 4);
 
     final length = bc._getUint32(listOffset);
     final result = Uint8List(length);
@@ -1247,7 +1247,7 @@ class Int8ListReader extends Reader<List<int>> {
   @pragma('vm:prefer-inline')
   List<int> read(BufferContext bc, int offset) {
     final listOffset = bc.derefObject(offset);
-    if (lazy) return _FbUint8List(bc, listOffset);
+    if (lazy) return Int8List.view(bc.buffer.buffer, listOffset + 4);
 
     final length = bc._getUint32(listOffset);
     final result = Int8List(length);
@@ -1337,24 +1337,6 @@ class _FbUint16List extends _FbList<int> {
   int operator [](int i) => bc._getUint16(offset + 4 + 2 * i);
 }
 
-/// List backed by 8-bit unsigned integers.
-class _FbUint8List extends _FbList<int> {
-  _FbUint8List(BufferContext bc, int offset) : super(bc, offset);
-
-  @override
-  @pragma('vm:prefer-inline')
-  int operator [](int i) => bc._getUint8(offset + 4 + i);
-}
-
-/// List backed by 8-bit signed integers.
-class _FbInt8List extends _FbList<int> {
-  _FbInt8List(BufferContext bc, int offset) : super(bc, offset);
-
-  @override
-  @pragma('vm:prefer-inline')
-  int operator [](int i) => bc._getInt8(offset + 4 + i);
-}
-
 /// List backed by 8-bit unsigned integers.
 class _FbBoolList extends _FbList<bool> {
   _FbBoolList(BufferContext bc, int offset) : super(bc, offset);

(I dislike the magic 4 offsets and would prefer replacing them with symbolic constants if I understood what it represented. At any rate, the code currently already uses them, so it's not any worse. Additionally, the old Int8ListReader.read used _FbUint8List instead of _FbInt8List, and _FbInt8List was unused, which was probably a bug.)

I would be fine creating a pull request, but I am unfamiliar with flatbuffers and have no idea how to test the change. Otherwise, although I hate having to keep asking him for favors, perhaps @dnfield could help or provide additional insight; it appears that he originally wrote this file.

That patch isn't quite right, as the ints are written to the Uint8List as Int32 instead of Int8, but I opened up a PR that writes them correctly. Still unsure of any side effects, and it's functionally untested. Also, instead of removing _FbInt8List I just made Int8ListReader use the currently unused type. Hopefully it can get some traction and land!

NotTsunami avatar Apr 22 '24 16:04 NotTsunami