Skip to content

Commit d4b7cda

Browse files
Added enums -> as-int config option to use integer values (#1344)
1 parent 281124f commit d4b7cda

File tree

9 files changed

+212
-6
lines changed

9 files changed

+212
-6
lines changed

pkgs/ffigen/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
of abstract classes with integer constants. Native enum members with the same
1717
integer values are handled properly on the Dart side, and native functions
1818
that use enums in their signatures now accept the generated enums on the Dart
19-
side, instead of integer values.
19+
side, instead of integer values. To opt out of this, use the `enums->as-int`
20+
option as specified in the README.
2021
- __Breaking change__: Enum integer types are implementation-defined and not
2122
part of the ABI. Therefore FFIgen does a best-effort approach trying to mimic
2223
the most common compilers for the various OS and architecture combinations.
@@ -90,7 +91,7 @@ must be passed to change this behaviour.
9091
generate a typedef for the `Function`.
9192
- Use Dart wrapper types in args and returns of ObjCBlocks.
9293
- Use Dart wrapper types in args and returns of static functions.
93-
- Renamed `asset` to `assetId` for `ffi-native`.
94+
- Renamed `asset` to `assetId` for `ffi-native`.
9495

9596
## 9.0.1
9697

pkgs/ffigen/README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ or
111111
```yaml
112112
output:
113113
bindings: 'generated_bindings.dart'
114-
...
114+
...
115115
```
116116
</td>
117117
</tr>
@@ -263,6 +263,10 @@ enums:
263263
# $1 keeps only the 1st
264264
# group i.e only '(.*)'.
265265
'CXType(.*)': '$1'
266+
as-int:
267+
# These enums will be generated as Dart integers instead of Dart enums
268+
include:
269+
- MyIntegerEnum
266270
globals:
267271
exclude:
268272
- aGlobal
@@ -816,6 +820,31 @@ unnamed-enums:
816820
'CXType_(.*)': '$1'
817821
```
818822

823+
### How can I handle unexpected enum values?
824+
825+
Native enums are, by default, generated into Dart enums with `int get value` and `fromValue(int)`.
826+
This works well in the case that your enums values are known in advance and not going to change,
827+
and in return, you get the full benefits of Dart enums like exhaustiveness checking.
828+
829+
However, if a native library adds another possible enum value after you generate your bindings,
830+
and this new value is passed to your Dart code, this will result in an `ArgumentError` at runtime.
831+
To fix this, you can regenerate the bindings on the new header file, but if you wish to avoid this
832+
issue entirely, you can tell ffigen to generate plain Dart integers for your enum instead. To do
833+
this, simply list your enum's name in the `as-int` section of your ffigen config:
834+
```yaml
835+
enums:
836+
as-int:
837+
include:
838+
- MyIntegerEnum
839+
- '*IntegerEnum'
840+
exclude:
841+
- FakeIntegerEnum
842+
```
843+
844+
Functions that accept or return these enums will now accept or return integers instead, and it will
845+
be up to your code to map integer values to behavior and handle invalid values. But your code will
846+
be future-proof against new additions to the enums.
847+
819848
### Why are some struct/union declarations generated even after excluded them in config?
820849

821850
This happens when an excluded struct/union is a dependency to some included declaration.

pkgs/ffigen/ffigen.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@
250250
},
251251
"member-rename": {
252252
"$ref": "#/$defs/memberRename"
253+
},
254+
"as-int": {
255+
"$ref": "#/$defs/includeExclude"
253256
}
254257
}
255258
},
@@ -265,6 +268,9 @@
265268
},
266269
"rename": {
267270
"$ref": "#/$defs/rename"
271+
},
272+
"as-int": {
273+
"$ref": "#/$defs/includeExclude"
268274
}
269275
}
270276
},

pkgs/ffigen/lib/src/code_generator/enum_class.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class EnumClass extends BindingType {
5252

5353
ObjCBuiltInFunctions? objCBuiltInFunctions;
5454

55+
/// Whether this enum should be generated as a collection of integers.
56+
bool generateAsInt;
57+
5558
EnumClass({
5659
super.usr,
5760
super.originalName,
@@ -60,6 +63,7 @@ class EnumClass extends BindingType {
6063
Type? nativeType,
6164
List<EnumConstant>? enumConstants,
6265
this.objCBuiltInFunctions,
66+
this.generateAsInt = false,
6367
}) : nativeType = nativeType ?? intType,
6468
enumConstants = enumConstants ?? [],
6569
namer = UniqueNamer({name});
@@ -84,7 +88,7 @@ class EnumClass extends BindingType {
8488

8589
/// Returns a string to declare the enum member and any documentation it may
8690
/// have had.
87-
String formatValue(EnumConstant ec) {
91+
String formatValue(EnumConstant ec, {bool asInt = false}) {
8892
final buffer = StringBuffer();
8993
final enumValueName = namer.makeUnique(ec.name);
9094
enumNames[ec] = enumValueName;
@@ -93,7 +97,11 @@ class EnumClass extends BindingType {
9397
buffer.writeAll(ec.dartDoc!.split('\n'), '\n$depth/// ');
9498
buffer.write('\n');
9599
}
96-
buffer.write('$depth$enumValueName(${ec.value})');
100+
if (asInt) {
101+
buffer.write('${depth}static const $enumValueName = ${ec.value};');
102+
} else {
103+
buffer.write('$depth$enumValueName(${ec.value})');
104+
}
97105
return buffer.toString();
98106
}
99107

@@ -124,6 +132,10 @@ class EnumClass extends BindingType {
124132
}
125133
}
126134

135+
void writeIntegerConstants(StringBuffer s) {
136+
s.writeAll(enumConstants.map((c) => formatValue(c, asInt: true)), '\n');
137+
}
138+
127139
/// Writes the enum declarations for all unique members.
128140
///
129141
/// Eg, C: `apple = 1`, Dart: `apple(1)`
@@ -232,6 +244,10 @@ class EnumClass extends BindingType {
232244
writeDartDoc(s);
233245
if (enumConstants.isEmpty) {
234246
writeEmptyEnum(s);
247+
} else if (generateAsInt) {
248+
s.write('abstract class $name {\n');
249+
writeIntegerConstants(s);
250+
s.write('}\n\n');
235251
} else {
236252
s.write('enum $name {\n');
237253
writeUniqueMembers(s);
@@ -278,7 +294,7 @@ class EnumClass extends BindingType {
278294
bool get sameFfiDartAndCType => nativeType.sameFfiDartAndCType;
279295

280296
@override
281-
bool get sameDartAndFfiDartType => false;
297+
bool get sameDartAndFfiDartType => generateAsInt;
282298

283299
@override
284300
String? getDefaultValue(Writer w) => '0';

pkgs/ffigen/lib/src/config_provider/config.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ class Config {
204204
Includer get leafFunctions => _leafFunctions;
205205
late Includer _leafFunctions;
206206

207+
Includer get enumsAsInt => _enumsAsInt;
208+
late Includer _enumsAsInt;
209+
210+
Includer get unnamedEnumsAsInt => _unnamedEnumsAsInt;
211+
late Includer _unnamedEnumsAsInt;
212+
207213
FfiNativeConfig get ffiNativeConfig => _ffiNativeConfig;
208214
late FfiNativeConfig _ffiNativeConfig;
209215

@@ -482,10 +488,13 @@ class Config {
482488
..._includeExcludeProperties(),
483489
..._renameProperties(),
484490
..._memberRenameProperties(),
491+
..._enumIntProperties(),
485492
],
486493
result: (node) {
487494
_enumClassDecl = declarationConfigExtractor(
488495
node.value as Map<dynamic, dynamic>);
496+
_enumsAsInt =
497+
(node.value as Map)[strings.enumAsInt] as Includer;
489498
},
490499
)),
491500
HeterogeneousMapEntry(
@@ -494,10 +503,13 @@ class Config {
494503
entries: [
495504
..._includeExcludeProperties(),
496505
..._renameProperties(),
506+
..._enumIntProperties(),
497507
],
498508
result: (node) {
499509
_unnamedEnumConstants = declarationConfigExtractor(
500510
node.value as Map<dynamic, dynamic>);
511+
_unnamedEnumsAsInt =
512+
(node.value as Map)[strings.enumAsInt] as Includer;
501513
},
502514
)),
503515
HeterogeneousMapEntry(
@@ -946,6 +958,14 @@ class Config {
946958
];
947959
}
948960

961+
List<HeterogeneousMapEntry> _enumIntProperties() => [
962+
HeterogeneousMapEntry(
963+
key: strings.enumAsInt,
964+
defaultValue: (node) => Includer.excludeByDefault(),
965+
valueConfigSpec: _includeExcludeObject(),
966+
),
967+
];
968+
949969
HeterogeneousMapConfigSpec<List<String>, Includer> _includeExcludeObject() {
950970
return HeterogeneousMapConfigSpec(
951971
schemaDefName: 'includeExclude',

pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ final _logger = Logger('ffigen.header_parser.enumdecl_parser');
5555
originalName: enumName,
5656
name: config.enumClassDecl.renameUsingConfig(enumName),
5757
nativeType: nativeType,
58+
generateAsInt: config.enumsAsInt.shouldInclude(enumName),
5859
objCBuiltInFunctions: objCBuiltInFunctions,
5960
);
6061
cursor.visitChildren((clang_types.CXCursor child) {

pkgs/ffigen/lib/src/strings.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ const exposeFunctionTypedefs = 'expose-typedefs';
9090
const leafFunctions = 'leaf';
9191
const varArgFunctions = 'variadic-arguments';
9292

93+
// Nested under `enums`
94+
const enumAsInt = 'as-int';
95+
9396
// Nested under varArg entries
9497
const postfix = 'postfix';
9598
const types = 'types';

pkgs/ffigen/test/code_generator_tests/code_generator_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,64 @@ void main() {
390390
_matchLib(library, 'enumclass_duplicates');
391391
});
392392

393+
test('enum_class as integers', () {
394+
final enum1 = EnumClass(
395+
name: 'MyEnum',
396+
enumConstants: [
397+
const EnumConstant(
398+
name: 'value1',
399+
value: 0,
400+
),
401+
const EnumConstant(
402+
name: 'value2',
403+
value: 1,
404+
),
405+
const EnumConstant(
406+
name: 'value3',
407+
value: 2,
408+
),
409+
],
410+
);
411+
final enum2 = EnumClass(
412+
name: 'MyIntegerEnum',
413+
generateAsInt: true,
414+
enumConstants: [
415+
const EnumConstant(
416+
name: 'int1',
417+
value: 1,
418+
),
419+
const EnumConstant(
420+
name: 'int2',
421+
value: 2,
422+
),
423+
const EnumConstant(
424+
name: 'int3',
425+
value: 10,
426+
),
427+
],
428+
);
429+
final library = Library(
430+
name: 'Bindings',
431+
header: '$licenseHeader\n// ignore_for_file: unused_import\n',
432+
silenceEnumWarning: true,
433+
bindings: [
434+
enum1,
435+
enum2,
436+
Func(
437+
name: 'acceptsEnum',
438+
returnType: enum1,
439+
parameters: [Parameter(name: 'value', type: enum1)],
440+
),
441+
Func(
442+
name: 'acceptsInt',
443+
returnType: enum2,
444+
parameters: [Parameter(name: 'value', type: enum2)],
445+
),
446+
],
447+
);
448+
_matchLib(library, 'enumclass_integers');
449+
});
450+
393451
test('Internal conflict resolution', () {
394452
final library = Library(
395453
name: 'init_dylib',
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// ignore_for_file: unused_import
6+
7+
// AUTO GENERATED FILE, DO NOT EDIT.
8+
//
9+
// Generated by `package:ffigen`.
10+
// ignore_for_file: type=lint
11+
import 'dart:ffi' as ffi;
12+
13+
class Bindings {
14+
/// Holds the symbol lookup function.
15+
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
16+
_lookup;
17+
18+
/// The symbols are looked up in [dynamicLibrary].
19+
Bindings(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup;
20+
21+
/// The symbols are looked up with [lookup].
22+
Bindings.fromLookup(
23+
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
24+
lookup)
25+
: _lookup = lookup;
26+
27+
MyEnum acceptsEnum(
28+
MyEnum value,
29+
) {
30+
return MyEnum.fromValue(_acceptsEnum(
31+
value.value,
32+
));
33+
}
34+
35+
late final _acceptsEnumPtr =
36+
_lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int)>>('acceptsEnum');
37+
late final _acceptsEnum = _acceptsEnumPtr.asFunction<int Function(int)>();
38+
39+
int acceptsInt(
40+
int value,
41+
) {
42+
return _acceptsInt(
43+
value,
44+
);
45+
}
46+
47+
late final _acceptsIntPtr =
48+
_lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int)>>('acceptsInt');
49+
late final _acceptsInt = _acceptsIntPtr.asFunction<int Function(int)>();
50+
}
51+
52+
enum MyEnum {
53+
value1(0),
54+
value2(1),
55+
value3(2);
56+
57+
final int value;
58+
const MyEnum(this.value);
59+
60+
static MyEnum fromValue(int value) => switch (value) {
61+
0 => value1,
62+
1 => value2,
63+
2 => value3,
64+
_ => throw ArgumentError("Unknown value for MyEnum: $value"),
65+
};
66+
}
67+
68+
abstract class MyIntegerEnum {
69+
static const int1 = 1;
70+
static const int2 = 2;
71+
static const int3 = 10;
72+
}

0 commit comments

Comments
 (0)