• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dart-lang / ffigen / 6366517051

28 Sep 2023 08:12PM UTC coverage: 92.593% (+0.08%) from 92.509%
6366517051

push

github

web-flow
Handle ObjC nullable annotations (#624)

* Fix #334

* Fix analysis

* fix swift test

* Fix tests

* Fix nullability bug

* Fix formatting

* Fix linker error

* Fix static methods bug

* fix objective_c_example_test

87 of 87 new or added lines in 7 files covered. (100.0%)

3700 of 3996 relevant lines covered (92.59%)

28.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.31
/lib/src/code_generator/objc_interface.dart
1
// Copyright (c) 2022, 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
import 'package:ffigen/src/code_generator.dart';
6
import 'package:logging/logging.dart';
7

8
import 'binding_string.dart';
9
import 'utils.dart';
10
import 'writer.dart';
11

12
// Class methods defined on NSObject that we don't want to copy to child objects
13
// by default.
14
const _excludedNSObjectClassMethods = {
15
  'allocWithZone:',
16
  'class',
17
  'conformsToProtocol:',
18
  'copyWithZone:',
19
  'debugDescription',
20
  'description',
21
  'hash',
22
  'initialize',
23
  'instanceMethodForSelector:',
24
  'instanceMethodSignatureForSelector:',
25
  'instancesRespondToSelector:',
26
  'isSubclassOfClass:',
27
  'load',
28
  'mutableCopyWithZone:',
29
  'poseAsClass:',
30
  'resolveClassMethod:',
31
  'resolveInstanceMethod:',
32
  'setVersion:',
33
  'superclass',
34
  'version',
35
};
36

37
final _logger = Logger('ffigen.code_generator.objc_interface');
6✔
38

39
class ObjCInterface extends BindingType {
40
  ObjCInterface? superType;
41
  final methods = <String, ObjCMethod>{};
42
  bool filled = false;
43

44
  final String lookupName;
45
  final ObjCBuiltInFunctions builtInFunctions;
46
  late final ObjCInternalGlobal _classObject;
47
  late final ObjCInternalGlobal _isKindOfClass;
48
  late final ObjCMsgSendFunc _isKindOfClassMsgSend;
49

50
  ObjCInterface({
2✔
51
    String? usr,
52
    required String originalName,
53
    String? name,
54
    String? lookupName,
55
    String? dartDoc,
56
    required this.builtInFunctions,
57
  })  : lookupName = lookupName ?? originalName,
58
        super(
2✔
59
          usr: usr,
60
          originalName: originalName,
61
          name: name ?? originalName,
62
          dartDoc: dartDoc,
63
        ) {
64
    builtInFunctions.registerInterface(this);
4✔
65
  }
66

67
  bool get isNSString => originalName == "NSString";
6✔
68
  bool get isNSData => originalName == "NSData";
6✔
69

70
  @override
2✔
71
  BindingString toBindingString(Writer w) {
72
    String paramsToString(List<ObjCMethodParam> params,
2✔
73
        {required bool isStatic}) {
74
      final List<String> stringParams = [];
2✔
75

76
      if (isStatic) {
77
        stringParams.add('${w.className} _lib');
6✔
78
      }
79
      stringParams.addAll(
2✔
80
          params.map((p) => '${_getConvertedType(p.type, w, name)} ${p.name}'));
14✔
81
      return '(${stringParams.join(", ")})';
4✔
82
    }
83

84
    final s = StringBuffer();
2✔
85
    if (dartDoc != null) {
2✔
86
      s.write(makeDartDoc(dartDoc!));
×
87
    }
88

89
    final uniqueNamer = UniqueNamer({name, '_id', '_lib'});
8✔
90
    final natLib = w.className;
2✔
91

92
    builtInFunctions.ensureUtilsExist(w, s);
4✔
93
    final objType = PointerType(objCObjectType).getCType(w);
6✔
94

95
    // Class declaration.
96
    s.write('''
2✔
97
class $name extends ${superType?.name ?? '_ObjCWrapper'} {
6✔
98
  $name._($objType id, $natLib lib,
2✔
99
      {bool retain = false, bool release = false}) :
100
          super._(id, lib, retain: retain, release: release);
101

102
  /// Returns a [$name] that points to the same underlying object as [other].
2✔
103
  static $name castFrom<T extends _ObjCWrapper>(T other) {
2✔
104
    return $name._(other._id, other._lib, retain: true, release: true);
2✔
105
  }
106

107
  /// Returns a [$name] that wraps the given raw object pointer.
2✔
108
  static $name castFromPointer($natLib lib, $objType other,
2✔
109
      {bool retain = false, bool release = false}) {
110
    return $name._(other, lib, retain: retain, release: release);
2✔
111
  }
112

113
  /// Returns whether [obj] is an instance of [$name].
2✔
114
  static bool isInstance(_ObjCWrapper obj) {
115
    return obj._lib.${_isKindOfClassMsgSend.name}(
4✔
116
        obj._id, obj._lib.${_isKindOfClass.name},
4✔
117
        obj._lib.${_classObject.name});
4✔
118
  }
119

120
''');
2✔
121

122
    if (isNSString) {
2✔
123
      builtInFunctions.generateNSStringUtils(w, s);
4✔
124
    }
125

126
    // Methods.
127
    for (final m in methods.values) {
6✔
128
      final methodName = m._getDartMethodName(uniqueNamer);
2✔
129
      final isStatic = m.isClass;
2✔
130
      final isStret = m.msgSend!.isStret;
4✔
131

132
      var returnType = m.returnType;
2✔
133
      var params = m.params;
2✔
134
      if (isStret) {
135
        params = [ObjCMethodParam(PointerType(returnType), 'stret'), ...params];
8✔
136
        returnType = voidType;
2✔
137
      }
138

139
      // The method declaration.
140
      if (m.dartDoc != null) {
2✔
141
        s.write(makeDartDoc(m.dartDoc!));
×
142
      }
143

144
      s.write('  ');
2✔
145
      if (isStatic) {
146
        s.write('static ');
2✔
147
        s.write(_getConvertedType(returnType, w, name));
6✔
148

149
        switch (m.kind) {
2✔
150
          case ObjCMethodKind.method:
2✔
151
            // static returnType methodName(NativeLibrary _lib, ...)
152
            s.write(' $methodName');
4✔
153
            break;
154
          case ObjCMethodKind.propertyGetter:
2✔
155
            // static returnType getMethodName(NativeLibrary _lib)
156
            s.write(' get');
2✔
157
            s.write(methodName[0].toUpperCase() + methodName.substring(1));
10✔
158
            break;
159
          case ObjCMethodKind.propertySetter:
2✔
160
            // static void setMethodName(NativeLibrary _lib, ...)
161
            s.write(' set');
2✔
162
            s.write(methodName[0].toUpperCase() + methodName.substring(1));
10✔
163
            break;
164
        }
165
        s.write(paramsToString(params, isStatic: true));
4✔
166
      } else {
167
        if (superType?.methods[m.originalName]?.sameAs(m) ?? false) {
10✔
168
          s.write('@override\n  ');
2✔
169
        }
170
        switch (m.kind) {
2✔
171
          case ObjCMethodKind.method:
2✔
172
            // returnType methodName(...)
173
            s.write(_getConvertedType(returnType, w, name));
6✔
174
            s.write(' $methodName');
4✔
175
            s.write(paramsToString(params, isStatic: false));
4✔
176
            break;
177
          case ObjCMethodKind.propertyGetter:
2✔
178
            s.write(_getConvertedType(returnType, w, name));
6✔
179
            if (isStret) {
180
              // void getMethodName(Pointer<returnType> stret, NativeLibrary _lib)
181
              s.write(' get');
2✔
182
              s.write(methodName[0].toUpperCase() + methodName.substring(1));
10✔
183
              s.write(paramsToString(params, isStatic: false));
4✔
184
            } else {
185
              // returnType get methodName
186
              s.write(' get $methodName');
4✔
187
            }
188
            break;
189
          case ObjCMethodKind.propertySetter:
2✔
190
            // set methodName(...)
191
            s.write(' set $methodName');
4✔
192
            s.write(paramsToString(params, isStatic: false));
4✔
193
            break;
194
        }
195
      }
196

197
      s.write(' {\n');
2✔
198

199
      // Implementation.
200
      final convertReturn = m.kind != ObjCMethodKind.propertySetter &&
4✔
201
          _needsConverting(returnType);
2✔
202

203
      if (returnType != voidType) {
4✔
204
        s.write('    ${convertReturn ? 'final _ret = ' : 'return '}');
4✔
205
      }
206
      s.write('_lib.${m.msgSend!.name}(');
8✔
207
      if (isStret) {
208
        s.write('stret, ');
2✔
209
      }
210
      s.write(isStatic ? '_lib.${_classObject.name}' : '_id');
8✔
211
      s.write(', _lib.${m.selObject!.name}');
8✔
212
      for (final p in m.params) {
4✔
213
        s.write(', ${_doArgConversion(p)}');
6✔
214
      }
215
      s.write(');\n');
2✔
216
      if (convertReturn) {
217
        final result = _doReturnConversion(
2✔
218
            returnType, '_ret', name, '_lib', m.isOwnedReturn);
4✔
219
        s.write('    return $result;');
4✔
220
      }
221

222
      s.write('  }\n\n');
2✔
223
    }
224

225
    s.write('}\n\n');
2✔
226

227
    if (isNSString) {
2✔
228
      builtInFunctions.generateStringUtils(w, s);
4✔
229
    }
230

231
    return BindingString(
2✔
232
        type: BindingStringType.objcInterface, string: s.toString());
2✔
233
  }
234

235
  @override
2✔
236
  void addDependencies(Set<Binding> dependencies) {
237
    if (dependencies.contains(this)) return;
2✔
238
    dependencies.add(this);
2✔
239
    builtInFunctions.addDependencies(dependencies);
4✔
240

241
    _classObject = ObjCInternalGlobal(
4✔
242
        '_class_$originalName',
4✔
243
        (Writer w) => '${builtInFunctions.getClass.name}("$lookupName")',
12✔
244
        builtInFunctions.getClass)
4✔
245
      ..addDependencies(dependencies);
2✔
246
    _isKindOfClass = builtInFunctions.getSelObject('isKindOfClass:');
6✔
247
    _isKindOfClassMsgSend = builtInFunctions.getMsgSendFunc(
6✔
248
        BooleanType(), [ObjCMethodParam(PointerType(objCObjectType), 'clazz')]);
10✔
249

250
    if (isNSString) {
2✔
251
      _addNSStringMethods();
2✔
252
    }
253

254
    if (isNSData) {
2✔
255
      _addNSDataMethods();
2✔
256
    }
257

258
    if (superType != null) {
2✔
259
      superType!.addDependencies(dependencies);
4✔
260
      _copyMethodsFromSuperType();
2✔
261
      _fixNullabilityOfOverriddenMethods();
2✔
262
    }
263

264
    for (final m in methods.values) {
6✔
265
      m.addDependencies(dependencies, builtInFunctions);
4✔
266
    }
267
  }
268

269
  void _copyMethodsFromSuperType() {
2✔
270
    // We need to copy certain methods from the super type:
271
    //  - Class methods, because Dart classes don't inherit static methods.
272
    //  - Methods that return instancetype, because the subclass's copy of the
273
    //    method needs to return the subclass, not the super class.
274
    //    Note: instancetype is only allowed as a return type, not an arg type.
275
    for (final m in superType!.methods.values) {
8✔
276
      if (m.isClass &&
2✔
277
          !_excludedNSObjectClassMethods.contains(m.originalName)) {
4✔
278
        addMethod(m);
2✔
279
      } else if (_isInstanceType(m.returnType)) {
4✔
280
        addMethod(m);
2✔
281
      }
282
    }
283
  }
284

285
  void _fixNullabilityOfOverriddenMethods() {
2✔
286
    // ObjC ignores nullability when deciding if an override for an inherited
287
    // method is valid. But in Dart it's invalid to override a method and change
288
    // it's return type from non-null to nullable, or its arg type from nullable
289
    // to non-null. So in these cases we have to make the non-null type
290
    // nullable, to avoid Dart compile errors.
291
    var superType_ = superType;
2✔
292
    while (superType_ != null) {
293
      for (final method in methods.values) {
6✔
294
        final superMethod = superType_.methods[method.originalName];
6✔
295
        if (superMethod != null && !superMethod.isClass && !method.isClass) {
4✔
296
          if (superMethod.returnType.typealiasType is! ObjCNullable &&
6✔
297
              method.returnType.typealiasType is ObjCNullable) {
6✔
298
            superMethod.returnType = ObjCNullable(superMethod.returnType);
6✔
299
          }
300
          final numArgs = method.params.length < superMethod.params.length
10✔
301
              ? method.params.length
×
302
              : superMethod.params.length;
4✔
303
          for (int i = 0; i < numArgs; ++i) {
4✔
304
            final param = method.params[i];
4✔
305
            final superParam = superMethod.params[i];
4✔
306
            if (superParam.type.typealiasType is ObjCNullable &&
6✔
307
                param.type.typealiasType is! ObjCNullable) {
6✔
308
              param.type = ObjCNullable(param.type);
×
309
            }
310
          }
311
        }
312
      }
313
      superType_ = superType_.superType;
2✔
314
    }
315
  }
316

317
  static bool _isInstanceType(Type type) =>
2✔
318
      type is ObjCInstanceType ||
2✔
319
      (type is ObjCNullable && type.child is ObjCInstanceType);
6✔
320

321
  void addMethod(ObjCMethod method) {
2✔
322
    final oldMethod = methods[method.originalName];
6✔
323
    if (oldMethod != null) {
324
      // Typically we ignore duplicate methods. However, property setters and
325
      // getters are duplicated in the AST. One copy is marked with
326
      // ObjCMethodKind.propertyGetter/Setter. The other copy is missing
327
      // important information, and is a plain old instanceMethod. So if the
328
      // existing method is an instanceMethod, and the new one is a property,
329
      // override it.
330
      if (method.isProperty && !oldMethod.isProperty) {
4✔
331
        // Fallthrough.
332
      } else if (!method.isProperty && oldMethod.isProperty) {
4✔
333
        // Don't override, but also skip the same method check below.
334
        return;
335
      } else {
336
        // Check duplicate is the same method.
337
        if (!method.sameAs(oldMethod)) {
2✔
338
          _logger.severe('Duplicate methods with different signatures: '
6✔
339
              '$originalName.${method.originalName}');
4✔
340
        }
341
        return;
342
      }
343
    }
344
    methods[method.originalName] = method;
6✔
345
  }
346

347
  void _addNSStringMethods() {
2✔
348
    addMethod(ObjCMethod(
4✔
349
      originalName: 'stringWithCharacters:length:',
350
      kind: ObjCMethodKind.method,
351
      isClass: true,
352
      returnType: this,
353
      params_: [
2✔
354
        ObjCMethodParam(PointerType(wCharType), 'characters'),
6✔
355
        ObjCMethodParam(unsignedIntType, 'length'),
4✔
356
      ],
357
    ));
358
    addMethod(ObjCMethod(
4✔
359
      originalName: 'dataUsingEncoding:',
360
      kind: ObjCMethodKind.method,
361
      isClass: false,
362
      returnType: builtInFunctions.nsData,
4✔
363
      params_: [
2✔
364
        ObjCMethodParam(unsignedIntType, 'encoding'),
4✔
365
      ],
366
    ));
367
    addMethod(ObjCMethod(
4✔
368
      originalName: 'length',
369
      kind: ObjCMethodKind.propertyGetter,
370
      isClass: false,
371
      returnType: unsignedIntType,
2✔
372
      params_: [],
2✔
373
    ));
374
  }
375

376
  void _addNSDataMethods() {
2✔
377
    addMethod(ObjCMethod(
4✔
378
      originalName: 'bytes',
379
      kind: ObjCMethodKind.propertyGetter,
380
      isClass: false,
381
      returnType: PointerType(voidType),
4✔
382
      params_: [],
2✔
383
    ));
384
  }
385

386
  @override
2✔
387
  String getCType(Writer w) => PointerType(objCObjectType).getCType(w);
6✔
388

389
  @override
2✔
390
  String getDartType(Writer w) => name;
2✔
391

392
  // Utils for converting between the internal types passed to native code, and
393
  // the external types visible to the user. For example, ObjCInterfaces are
394
  // passed to native as Pointer<ObjCObject>, but the user sees the Dart wrapper
395
  // class. These methods need to be kept in sync.
396
  bool _needsConverting(Type type) =>
2✔
397
      type is ObjCInterface ||
2✔
398
      type is ObjCBlock ||
2✔
399
      type is ObjCObjectPointer ||
2✔
400
      type is ObjCInstanceType ||
2✔
401
      type is ObjCNullable;
2✔
402

403
  String _getConvertedType(Type type, Writer w, String enclosingClass) {
2✔
404
    if (type is ObjCInstanceType) return enclosingClass;
2✔
405
    if (type is ObjCNullable && type.child is ObjCInstanceType) {
6✔
406
      return '$enclosingClass?';
2✔
407
    }
408
    return type.getDartType(w);
2✔
409
  }
410

411
  String _doArgConversion(ObjCMethodParam arg) {
2✔
412
    if (arg.type is ObjCNullable) {
4✔
413
      return '${arg.name}?._id ?? ffi.nullptr';
4✔
414
    } else if (arg.type is ObjCInterface ||
4✔
415
        arg.type is ObjCObjectPointer ||
4✔
416
        arg.type is ObjCInstanceType ||
4✔
417
        arg.type is ObjCBlock) {
4✔
418
      return '${arg.name}._id';
4✔
419
    }
420
    return arg.name;
2✔
421
  }
422

423
  String _doReturnConversion(Type type, String value, String enclosingClass,
2✔
424
      String library, bool isOwnedReturn) {
425
    var prefix = '';
426
    if (type is ObjCNullable) {
2✔
427
      prefix = '$value.address == 0 ? null : ';
2✔
428
      type = type.child;
2✔
429
    }
430
    final ownerFlags = 'retain: ${!isOwnedReturn}, release: true';
2✔
431
    if (type is ObjCInterface) {
2✔
432
      return '$prefix${type.name}._($value, $library, $ownerFlags)';
4✔
433
    }
434
    if (type is ObjCBlock) {
2✔
435
      return '$prefix${type.name}._($value, $library)';
4✔
436
    }
437
    if (type is ObjCObjectPointer) {
2✔
438
      return '${prefix}NSObject._($value, $library, $ownerFlags)';
2✔
439
    }
440
    if (type is ObjCInstanceType) {
2✔
441
      return '$prefix$enclosingClass._($value, $library, $ownerFlags)';
2✔
442
    }
443
    return prefix + value;
×
444
  }
445
}
446

447
enum ObjCMethodKind {
448
  method,
449
  propertyGetter,
450
  propertySetter,
451
}
452

453
class ObjCProperty {
454
  final String originalName;
455
  String? dartName;
456

457
  ObjCProperty(this.originalName);
2✔
458
}
459

460
class ObjCMethod {
461
  final String? dartDoc;
462
  final String originalName;
463
  final ObjCProperty? property;
464
  Type returnType;
465
  final List<ObjCMethodParam> params;
466
  final ObjCMethodKind kind;
467
  final bool isClass;
468
  bool returnsRetained = false;
469
  ObjCInternalGlobal? selObject;
470
  ObjCMsgSendFunc? msgSend;
471

472
  ObjCMethod({
2✔
473
    required this.originalName,
474
    this.property,
475
    this.dartDoc,
476
    required this.kind,
477
    required this.isClass,
478
    required this.returnType,
479
    List<ObjCMethodParam>? params_,
480
  }) : params = params_ ?? [];
2✔
481

482
  bool get isProperty =>
2✔
483
      kind == ObjCMethodKind.propertyGetter ||
4✔
484
      kind == ObjCMethodKind.propertySetter;
4✔
485

486
  void addDependencies(
2✔
487
      Set<Binding> dependencies, ObjCBuiltInFunctions builtInFunctions) {
488
    returnType.addDependencies(dependencies);
4✔
489
    for (final p in params) {
4✔
490
      p.type.addDependencies(dependencies);
4✔
491
    }
492
    selObject ??= builtInFunctions.getSelObject(originalName)
6✔
493
      ..addDependencies(dependencies);
2✔
494
    msgSend ??= builtInFunctions.getMsgSendFunc(returnType, params)
8✔
495
      ..addDependencies(dependencies);
2✔
496
  }
497

498
  String _getDartMethodName(UniqueNamer uniqueNamer) {
2✔
499
    if (property != null) {
2✔
500
      // A getter and a setter are allowed to have the same name, so we can't
501
      // just run the name through uniqueNamer. Instead they need to share
502
      // the dartName, which is run through uniqueNamer.
503
      if (property!.dartName == null) {
4✔
504
        property!.dartName = uniqueNamer.makeUnique(property!.originalName);
10✔
505
      }
506
      return property!.dartName!;
4✔
507
    }
508
    // Objective C methods can look like:
509
    // foo
510
    // foo:
511
    // foo:someArgName:
512
    // So replace all ':' with '_'.
513
    return uniqueNamer.makeUnique(originalName.replaceAll(":", "_"));
6✔
514
  }
515

516
  bool sameAs(ObjCMethod other) {
2✔
517
    if (originalName != other.originalName) return false;
6✔
518
    if (kind != other.kind) return false;
6✔
519
    if (isClass != other.isClass) return false;
6✔
520
    // msgSend is deduped by signature, so this check covers the signature.
521
    return msgSend == other.msgSend;
6✔
522
  }
523

524
  static final _copyRegExp = RegExp('[cC]opy');
6✔
525
  bool get isOwnedReturn =>
2✔
526
      returnsRetained ||
2✔
527
      originalName.startsWith('new') ||
4✔
528
      originalName.startsWith('alloc') ||
4✔
529
      originalName.contains(_copyRegExp);
6✔
530

531
  @override
×
532
  String toString() => '$returnType $originalName(${params.join(', ')})';
×
533
}
534

535
class ObjCMethodParam {
536
  Type type;
537
  final String name;
538
  ObjCMethodParam(this.type, this.name);
2✔
539

540
  @override
×
541
  String toString() => '$type $name';
×
542
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc