diff --git a/packages/pigeon/.gitignore b/packages/pigeon/.gitignore new file mode 100644 index 000000000000..add3fc06234a --- /dev/null +++ b/packages/pigeon/.gitignore @@ -0,0 +1,2 @@ +build/ +e2e_tests/test_objc/ios/Flutter/ diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md new file mode 100644 index 000000000000..e0f8d17e6367 --- /dev/null +++ b/packages/pigeon/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0-experimental.0 + +* Initial release. diff --git a/packages/pigeon/LICENSE b/packages/pigeon/LICENSE new file mode 100644 index 000000000000..bc67b8f95568 --- /dev/null +++ b/packages/pigeon/LICENSE @@ -0,0 +1,27 @@ +Copyright 2019 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md new file mode 100644 index 000000000000..916cbd9ac186 --- /dev/null +++ b/packages/pigeon/README.md @@ -0,0 +1,126 @@ +# Pigeon + + + +Pigeon is a code generator tool to make communication between Flutter and the +host platform type-safe and easier. + +## Supported Platforms + +Currently Pigeon only supports generating Objective-C code for usage on iOS and calling host functions from Flutter. + +## Runtime Requirements + +Pigeon generates all the code that is needed to communicate between Flutter and the host platform, there is no extra runtime requirement. A plugin author doesn't need to worry about conflicting versions of Pigeon. + +## Usage + +### Steps + +1) Add Pigeon as a dev_dependency. +1) Make a ".dart" file outside of your "lib" directory for defining the communication interface. +1) Run pigeon on your ".dart" file to generate the required Dart and Objective-C code. +1) Add the generated code to your `ios/Runner.xcworkspace` XCode project for compilation. +1) Implement the generated iOS protocol for handling the calls on iOS, set it up + as the handler for the messages. +1) Call the generated Dart methods. + +### Rules for defining your communication interface + +1) The file should contain no methods or function definitions. +1) Datatypes are defined as classes with fields of the supported datatypes (see + the supported Datatypes section). +1) Api's should be defined as an `abstract class` with either `HostApi()` or + `FlutterApi()` as metadata. The former being for procedures that are defined + on the host platform and the latter for procedures that are defined in Dart. +1) Method declarations on the Api classes should have one argument and a return + value whose types are defined in the file. + +### Example + +#### message.dart + +```dart +import 'package:pigeon/pigeon_lib.dart'; + +class SearchRequest { + String query; +} + +class SearchReply { + String result; +} + +@HostApi() +abstract class Api { + SearchReply search(SearchRequest request); +} +``` + +#### invocation + +```sh +pub run pigeon \ + --input pigeons/message.dart \ + --dart_out lib/pigeon.dart \ + --objc_header_out ios/Runner/pigeon.h \ + --objc_source_out ios/Runner/pigeon.m +``` + +#### AppDelegate.m + +```objc +#import "AppDelegate.h" +#import +#import "pigeon.h" + +@interface MyApi : NSObject +@end + +@implementation MyApi +-(SearchReply*)search:(SearchRequest*)request { + SearchReply *reply = [[SearchReply alloc] init]; + reply.result = + [NSString stringWithFormat:@"Hi %@!", request.query]; + return reply; +} +@end + +- (BOOL)application:(UIApplication *)application +didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + MyApi *api = [[MyApi alloc] init]; + ApiSetup(getFlutterEngine().binaryMessenger, api); + return YES; +} +``` + +#### test.dart + +```dart +import 'pigeon.dart'; + +void main() { + testWidgets("test pigeon", (WidgetTester tester) async { + SearchRequest request = SearchRequest()..query = "Aaron"; + Api api = Api(); + SearchReply reply = await api.search(request); + expect(reply.result, equals("Hi Aaron!")); + }); +} + +``` + +## Supported Datatypes + +Pigeon uses the `StandardMessageCodec` so it supports any data-type platform +channels supports +[[documentation](https://flutter.dev/docs/development/platform-integration/platform-channels#codec)]. Nested data-types are supported, too. + +Note: Generics for List and Map aren't supported yet. + +## Feedback + +File an issue in [flutter/flutter](https://github.com/flutter/flutter) with the +word 'pigeon' clearly in the title and cc **@gaaclarke**. diff --git a/packages/pigeon/bin/pigeon.dart b/packages/pigeon/bin/pigeon.dart new file mode 100644 index 000000000000..e203bf26ae6b --- /dev/null +++ b/packages/pigeon/bin/pigeon.dart @@ -0,0 +1,32 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'package:pigeon/pigeon_lib.dart'; + +Future main(List args) async { + final PigeonOptions opts = Pigeon.parseArgs(args); + assert(opts.input != null); + final String importLine = + (opts.input != null) ? 'import \'${opts.input}\';\n' : ''; + final String code = """$importLine +import 'dart:io'; +import 'package:pigeon/pigeon_lib.dart'; + +void main(List args) async { + exit(await Pigeon.run(args)); +} +"""; + // TODO(aaclarke): Start using a system temp file. + const String tempFilename = '_pigeon_temp_.dart'; + final File tempFile = await File(tempFilename).writeAsString(code); + final Process process = + await Process.start('dart', [tempFilename] + args); + process.stdout.transform(utf8.decoder).listen((String data) => print(data)); + process.stderr.transform(utf8.decoder).listen((String data) => print(data)); + final int exitCode = await process.exitCode; + tempFile.deleteSync(); + exit(exitCode); +} diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart new file mode 100644 index 000000000000..6c0e42d70ce9 --- /dev/null +++ b/packages/pigeon/lib/ast.dart @@ -0,0 +1,81 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Enum that represents where an [Api] is located, on the host or Flutter. +enum ApiLocation { + /// The API is for calling functions defined on the host. + host, + + /// The API is for calling functions defined in Flutter. + flutter, +} + +/// Superclass for all AST nodes. +class Node {} + +/// Represents a method on an [Api]. +class Method extends Node { + /// Parametric constructor for [Method]. + Method({this.name, this.returnType, this.argType}); + + /// The name of the method. + String name; + + /// The data-type of the return value. + String returnType; + + /// The data-type of the argument. + String argType; +} + +/// Represents a collection of [Method]s that are hosted ona given [location]. +class Api extends Node { + /// Parametric constructor for [Api]. + Api({this.name, this.location, this.methods}); + + /// The name of the API. + String name; + + /// Where the API's implementation is located, host or Flutter. + ApiLocation location; + + /// List of methods inside the API. + List methods; +} + +/// Represents a field on a [Class]. +class Field extends Node { + /// Parametric constructor for [Field]. + Field({this.name, this.dataType}); + + /// The name of the field. + String name; + + /// The data-type of the field (ex 'String' or 'int'). + String dataType; +} + +/// Represents a class with [Field]s. +class Class extends Node { + /// Parametric constructor for [Class]. + Class({this.name, this.fields}); + + /// The name of the class. + String name; + + /// All the fields contained in the class. + List fields; +} + +/// Top-level node for the AST. +class Root extends Node { + /// Parametric constructor for [Root]. + Root({this.classes, this.apis}); + + /// All the classes contained in the AST. + List classes; + + /// All the API's contained in the AST. + List apis; +} diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart new file mode 100644 index 000000000000..c249a883ab9c --- /dev/null +++ b/packages/pigeon/lib/dart_generator.dart @@ -0,0 +1,79 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'ast.dart'; +import 'generator_tools.dart'; + +/// Generates Dart source code for the given AST represented by [root], +/// outputting the code to [sink]. +void generateDart(Root root, StringSink sink) { + final List customClassNames = + root.classes.map((Class x) => x.name).toList(); + final Indent indent = Indent(sink); + indent.writeln('// Autogenerated from Dartle, do not edit directly.'); + indent.writeln('import \'package:flutter/services.dart\';'); + indent.writeln(''); + + for (Class klass in root.classes) { + sink.write('class ${klass.name} '); + indent.scoped('{', '}', () { + for (Field field in klass.fields) { + indent.writeln('${field.dataType} ${field.name};'); + } + indent.write('Map _toMap() '); + indent.scoped('{', '}', () { + indent.writeln('Map dartleMap = Map();'); + for (Field field in klass.fields) { + indent.write('dartleMap["${field.name}"] = '); + if (customClassNames.contains(field.dataType)) { + indent.addln('${field.name}._toMap();'); + } else { + indent.addln('${field.name};'); + } + } + indent.writeln('return dartleMap;'); + }); + indent.write('static ${klass.name} _fromMap(Map dartleMap) '); + indent.scoped('{', '}', () { + indent.writeln('var result = ${klass.name}();'); + for (Field field in klass.fields) { + indent.write('result.${field.name} = '); + if (customClassNames.contains(field.dataType)) { + indent.addln( + '${field.dataType}._fromMap(dartleMap["${field.name}"]);'); + } else { + indent.addln('dartleMap["${field.name}"];'); + } + } + indent.writeln('return result;'); + }); + }); + indent.writeln(''); + } + for (Api api in root.apis) { + if (api.location == ApiLocation.host) { + indent.write('class ${api.name} '); + indent.scoped('{', '}', () { + for (Method func in api.methods) { + indent.write( + 'Future<${func.returnType}> ${func.name}(${func.argType} arg) async '); + indent.scoped('{', '}', () { + indent.writeln('Map requestMap = arg._toMap();'); + final String channelName = makeChannelName(api, func); + indent.writeln('BasicMessageChannel channel ='); + indent.inc(); + indent.inc(); + indent.writeln( + 'BasicMessageChannel(\'$channelName\', StandardMessageCodec());'); + indent.dec(); + indent.dec(); + indent.writeln('Map replyMap = await channel.send(requestMap);'); + indent.writeln('return ${func.returnType}._fromMap(replyMap);'); + }); + } + }); + indent.writeln(''); + } + } +} diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart new file mode 100644 index 000000000000..db662aa2f69a --- /dev/null +++ b/packages/pigeon/lib/generator_tools.dart @@ -0,0 +1,84 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'ast.dart'; + +/// Read all the content from [stdin] to a String. +String readStdin() { + final List bytes = []; + int byte = stdin.readByteSync(); + while (byte >= 0) { + bytes.add(byte); + byte = stdin.readByteSync(); + } + return utf8.decode(bytes); +} + +/// A helper class for managing indentation, wrapping a [StringSink]. +class Indent { + /// Constructor which takes a [StringSink] [Ident] will wrap. + Indent(this._sink); + + int _count = 0; + final StringSink _sink; + + /// String used for newlines (ex "\n"). + final String newline = '\n'; + + /// Increase the indentation level. + void inc() { + _count += 1; + } + + /// Decrement the indentation level. + void dec() { + _count -= 1; + } + + /// Returns the String represneting the current indentation. + String str() { + String result = ''; + for (int i = 0; i < _count; i++) { + result += ' '; + } + return result; + } + + /// Scoped increase of the ident level. For the execution of [func] the + /// indentation will be incremented. + void scoped(String begin, String end, Function func) { + _sink.write(begin + newline); + inc(); + func(); + dec(); + _sink.write(str() + end + newline); + } + + /// Add [str] with indentation and a newline. + void writeln(String str) { + _sink.write(this.str() + str + newline); + } + + /// Add [str] with indentation. + void write(String str) { + _sink.write(this.str() + str); + } + + /// Add [str] with a newline. + void addln(String str) { + _sink.write(str + newline); + } + + /// Just adds [str]. + void add(String str) { + _sink.write(str); + } +} + +/// Create the generated channel name for a [func] on a [api]. +String makeChannelName(Api api, Method func) { + return 'dev.flutter.dartle.${api.name}.${func.name}'; +} diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart new file mode 100644 index 000000000000..5b59d412e0c7 --- /dev/null +++ b/packages/pigeon/lib/objc_generator.dart @@ -0,0 +1,217 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'ast.dart'; +import 'generator_tools.dart'; + +/// Options that control how Objective-C code will be generated. +class ObjcOptions { + /// Parametric constructor for ObjcOptions. + ObjcOptions({this.header, this.prefix}); + + /// The path to the header that will get placed in the source filed (example: + /// "foo.h"). + String header; + + /// Prefix that will be appended before all generated classes and protocols. + String prefix; +} + +String _className(String prefix, String className) { + if (prefix != null) { + return '$prefix$className'; + } else { + return className; + } +} + +const Map _objcTypeForDartTypeMap = { + 'bool': 'NSNumber *', + 'int': 'NSNumber *', + 'String': 'NSString *', + 'double': 'NSNumber *', + 'Uint8List': 'FlutterStandardTypedData *', + 'Int32List': 'FlutterStandardTypedData *', + 'Int64List': 'FlutterStandardTypedData *', + 'Float64List': 'FlutterStandardTypedData *', +}; + +const Map _propertyTypeForDartTypeMap = { + 'String': 'copy', + 'bool': 'strong', + 'int': 'strong', + 'double': 'strong', + 'Uint8List': 'strong', + 'Int32List': 'strong', + 'Int64List': 'strong', + 'Float64List': 'strong', +}; + +String _objcTypeForDartType(String type) { + return _objcTypeForDartTypeMap[type]; +} + +String _propertyTypeForDartType(String type) { + final String result = _propertyTypeForDartTypeMap[type]; + if (result == null) { + return 'assign'; + } else { + return result; + } +} + +/// Generates the ".h" file for the AST represented by [root] to [sink] with the +/// provided [options]. +void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) { + final Indent indent = Indent(sink); + indent.writeln('// Autogenerated from Dartle.'); + indent.writeln('#import '); + indent.writeln('@protocol FlutterBinaryMessenger;'); + indent.writeln('@class FlutterStandardTypedData;'); + indent.writeln(''); + + for (Class klass in root.classes) { + indent.writeln('@class ${_className(options.prefix, klass.name)};'); + } + + indent.writeln(''); + + for (Class klass in root.classes) { + indent.writeln( + '@interface ${_className(options.prefix, klass.name)} : NSObject '); + for (Field field in klass.fields) { + String propertyType = _propertyTypeForDartType(field.dataType); + String objcType = _objcTypeForDartType(field.dataType); + if (objcType == null && + root.classes.map((Class x) => x.name).contains(field.dataType)) { + propertyType = 'strong'; + objcType = '${_className(options.prefix, field.dataType)} *'; + } + indent.writeln( + '@property(nonatomic, $propertyType) $objcType ${field.name};'); + } + indent.writeln('@end'); + indent.writeln(''); + } + + for (Api api in root.apis) { + if (api.location == ApiLocation.host) { + final String apiName = _className(options.prefix, api.name); + indent.writeln('@protocol $apiName'); + for (Method func in api.methods) { + final String returnType = _className(options.prefix, func.returnType); + final String argType = _className(options.prefix, func.argType); + indent.writeln('-($returnType *)${func.name}:($argType*)input;'); + } + indent.writeln('@end'); + indent.writeln(''); + indent.writeln( + 'extern void ${apiName}Setup(id binaryMessenger, id<$apiName> api);'); + indent.writeln(''); + } + } +} + +String _dictGetter( + List classnames, String dict, Field field, String prefix) { + if (classnames.contains(field.dataType)) { + String className = field.dataType; + if (prefix != null) { + className = '$prefix$className'; + } + return '[$className fromMap:$dict[@"${field.name}"]]'; + } else { + return '$dict[@"${field.name}"]'; + } +} + +String _dictValue(List classnames, Field field) { + if (classnames.contains(field.dataType)) { + return '[self.${field.name} toMap]'; + } else { + return 'self.${field.name}'; + } +} + +/// Generates the ".m" file for the AST represented by [root] to [sink] with the +/// provided [options]. +void generateObjcSource(ObjcOptions options, Root root, StringSink sink) { + final Indent indent = Indent(sink); + final List classnames = + root.classes.map((Class x) => x.name).toList(); + + indent.writeln('// Autogenerated from Dartle.'); + indent.writeln('#import "${options.header}"'); + indent.writeln('#import '); + indent.writeln(''); + + for (Class klass in root.classes) { + final String className = _className(options.prefix, klass.name); + indent.writeln('@interface $className ()'); + indent.writeln('+($className*)fromMap:(NSDictionary*)dict;'); + indent.writeln('-(NSDictionary*)toMap;'); + indent.writeln('@end'); + } + + indent.writeln(''); + + for (Class klass in root.classes) { + final String className = _className(options.prefix, klass.name); + indent.writeln('@implementation $className'); + indent.write('+($className*)fromMap:(NSDictionary*)dict '); + indent.scoped('{', '}', () { + indent.writeln('$className* result = [[$className alloc] init];'); + for (Field field in klass.fields) { + indent.writeln( + 'result.${field.name} = ${_dictGetter(classnames, 'dict', field, options.prefix)};'); + } + indent.writeln('return result;'); + }); + indent.write('-(NSDictionary*)toMap '); + indent.scoped('{', '}', () { + indent.write('return [NSDictionary dictionaryWithObjectsAndKeys:'); + for (Field field in klass.fields) { + indent.add(_dictValue(classnames, field) + ', @"${field.name}", '); + } + indent.addln('nil];'); + }); + indent.writeln('@end'); + indent.writeln(''); + } + + for (Api api in root.apis) { + if (api.location == ApiLocation.host) { + final String apiName = _className(options.prefix, api.name); + indent.write( + 'void ${apiName}Setup(id binaryMessenger, id<$apiName> api) '); + indent.scoped('{', '}', () { + for (Method func in api.methods) { + indent.write(''); + indent.scoped('{', '}', () { + indent.writeln('FlutterBasicMessageChannel *channel ='); + indent.inc(); + indent.writeln('[FlutterBasicMessageChannel'); + indent.inc(); + indent.writeln( + 'messageChannelWithName:@"${makeChannelName(api, func)}"'); + indent.writeln('binaryMessenger:binaryMessenger];'); + indent.dec(); + indent.dec(); + + indent.write( + '[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) '); + indent.scoped('{', '}];', () { + final String argType = _className(options.prefix, func.argType); + final String returnType = + _className(options.prefix, func.returnType); + indent.writeln('$argType *input = [$argType fromMap:message];'); + indent.writeln('$returnType *output = [api ${func.name}:input];'); + indent.writeln('callback([output toMap]);'); + }); + }); + } + }); + } + } +} diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart new file mode 100644 index 000000000000..2b58649526f8 --- /dev/null +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -0,0 +1,327 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:args/args.dart'; +import 'package:path/path.dart'; + +import 'ast.dart'; +import 'dart_generator.dart'; +import 'objc_generator.dart'; + +const List _validTypes = [ + 'String', + 'int', + 'double', + 'Uint8List', + 'Int32List', + 'Int64List', + 'Float64List', + 'List', + 'Map', +]; + +/// Metadata to mark an API which will be implemented on the host platform. +class HostApi { + /// Parametric constructor for [HostApi]. + const HostApi(); +} + +/// Metadata to mark an API which will be implemented in Flutter. +class FlutterApi { + /// Parametric constructor for [FlutterApi]. + const FlutterApi(); +} + +/// Represents an error as a result of parsing and generating code. +class Error { + /// Parametric constructor for Error. + Error({this.message, this.filename, this.lineNumber}); + + /// A description of the error. + String message; + + /// What file caused the [Error]. + String filename; + + /// What line the error happened on. + int lineNumber; +} + +bool _isApi(ClassMirror classMirror) { + return classMirror.isAbstract && _isHostApi(classMirror); +} + +bool _isHostApi(ClassMirror apiMirror) { + for (InstanceMirror instance in apiMirror.metadata) { + if (instance.reflectee is HostApi) { + return true; + } + } + return false; +} + +/// Options used when running the code generator. +class PigeonOptions { + /// Path to the file which will be processed. + String input; + + /// Path to the dart file that will be generated. + String dartOut; + + /// Path to the ".h" Objective-C file will be generated. + String objcHeaderOut; + + /// Path to the ".m" Objective-C file will be generated. + String objcSourceOut; + + /// Options that control how Objective-C will be generated. + ObjcOptions objcOptions = ObjcOptions(); +} + +/// A collection of an AST represented as a [Root] and [Error]'s. +class ParseResults { + /// Parametric constructor for [ParseResults]. + ParseResults({this.root, this.errors}); + + /// The resulting AST. + final Root root; + + /// Errors generated while parsing input. + final List errors; +} + +/// Tool for generating code to facilitate platform channels usage. +class Pigeon { + /// Create and setup a [Pigeon] instance. + static Pigeon setup() { + return Pigeon(); + } + + Class _parseClassMirror(ClassMirror klassMirror) { + final List fields = []; + for (DeclarationMirror declaration in klassMirror.declarations.values) { + if (declaration is VariableMirror) { + fields.add(Field() + ..name = MirrorSystem.getName(declaration.simpleName) + ..dataType = MirrorSystem.getName(declaration.type.simpleName)); + } + } + final Class klass = Class() + ..name = MirrorSystem.getName(klassMirror.simpleName) + ..fields = fields; + return klass; + } + + /// Use reflection to parse the [types] provided. + ParseResults parse(List types) { + final Root root = Root(); + final Set classes = {}; + final List apis = []; + + for (Type type in types) { + final ClassMirror classMirror = reflectClass(type); + if (_isApi(classMirror)) { + apis.add(classMirror); + } else { + classes.add(classMirror); + } + } + + for (ClassMirror apiMirror in apis) { + for (DeclarationMirror declaration in apiMirror.declarations.values) { + if (declaration is MethodMirror && !declaration.isConstructor) { + classes.add(declaration.returnType); + classes.add(declaration.parameters[0].type); + } + } + } + + root.classes = classes.map(_parseClassMirror).toList(); + + root.apis = []; + for (ClassMirror apiMirror in apis) { + if (_isHostApi(apiMirror)) { + final List methods = []; + for (DeclarationMirror declaration in apiMirror.declarations.values) { + if (declaration is MethodMirror && !declaration.isConstructor) { + methods.add(Method() + ..name = MirrorSystem.getName(declaration.simpleName) + ..argType = MirrorSystem.getName( + declaration.parameters[0].type.simpleName) + ..returnType = + MirrorSystem.getName(declaration.returnType.simpleName)); + } + } + root.apis.add(Api() + ..name = MirrorSystem.getName(apiMirror.simpleName) + ..location = ApiLocation.host + ..methods = methods); + } + } + + final List validateErrors = _validateAst(root); + return ParseResults(root: root, errors: validateErrors); + } + + /// String that describes how the tool is used. + static String get usage { + return ''' + +Pigeon is a tool for generating type-safe communication code between Flutter +and the host platform. + +usage: pigeon --input --dart_out [option]* + +options: +''' + + _argParser.usage; + } + + static final ArgParser _argParser = ArgParser() + ..addOption('input', help: 'REQUIRED: Path to pigeon file.') + ..addOption('dart_out', + help: 'REQUIRED: Path to generated dart source file (.dart).') + ..addOption('objc_source_out', + help: 'Path to generated Objective-C source file (.m).') + ..addOption('objc_header_out', + help: 'Path to generated Objective-C header file (.h).') + ..addOption('objc_prefix', + help: 'Prefix for generated Objective-C classes and protocols.'); + + /// Convert command-line arugments to [PigeonOptions]. + static PigeonOptions parseArgs(List args) { + final ArgResults results = _argParser.parse(args); + + final PigeonOptions opts = PigeonOptions(); + opts.input = results['input']; + opts.dartOut = results['dart_out']; + opts.objcHeaderOut = results['objc_header_out']; + opts.objcSourceOut = results['objc_source_out']; + opts.objcOptions.prefix = results['objc_prefix']; + return opts; + } + + static Future _runGenerator( + String output, void Function(IOSink sink) func) async { + IOSink sink; + File file; + if (output == 'stdout') { + sink = stdout; + } else { + file = File(output); + sink = file.openWrite(); + } + func(sink); + await sink.flush(); + } + + List _validateAst(Root root) { + final List result = []; + final List customClasses = + root.classes.map((Class x) => x.name).toList(); + for (Class klass in root.classes) { + for (Field field in klass.fields) { + if (!(_validTypes.contains(field.dataType) || + customClasses.contains(field.dataType))) { + result.add(Error( + message: + 'Unsupported datatype:"${field.dataType}" in class "${klass.name}".')); + } + } + } + return result; + } + + /// Crawls through the reflection system looking for a setupPigeon method and + /// executing it. + static void _executeSetupPigeon(PigeonOptions options) { + for (LibraryMirror library in currentMirrorSystem().libraries.values) { + for (DeclarationMirror declaration in library.declarations.values) { + if (declaration is MethodMirror && + MirrorSystem.getName(declaration.simpleName) == 'setupPigeon') { + if (declaration.parameters.length == 1 && + declaration.parameters[0].type == reflectClass(PigeonOptions)) { + library.invoke(declaration.simpleName, [options]); + } else { + print('warning: invalid \'setupPigeon\' method defined.'); + } + } + } + } + } + + /// The 'main' entrypoint used by the command-line tool. [args] are the + /// command-line arguments. + static Future run(List args) async { + final Pigeon pigeon = Pigeon.setup(); + final PigeonOptions options = Pigeon.parseArgs(args); + + _executeSetupPigeon(options); + + if (options.input == null || options.dartOut == null) { + print(usage); + return 0; + } + + final List errors = []; + final List apis = []; + options.objcOptions.header = basename(options.objcHeaderOut); + + for (LibraryMirror library in currentMirrorSystem().libraries.values) { + for (DeclarationMirror declaration in library.declarations.values) { + if (declaration is ClassMirror && _isApi(declaration)) { + apis.add(declaration.reflectedType); + } + } + } + + if (apis.isNotEmpty) { + final ParseResults parseResults = pigeon.parse(apis); + for (Error err in parseResults.errors) { + errors.add(Error(message: err.message, filename: options.input)); + } + if (options.dartOut != null) { + await _runGenerator(options.dartOut, + (StringSink sink) => generateDart(parseResults.root, sink)); + } + if (options.objcHeaderOut != null) { + await _runGenerator( + options.objcHeaderOut, + (StringSink sink) => generateObjcHeader( + options.objcOptions, parseResults.root, sink)); + } + if (options.objcSourceOut != null) { + await _runGenerator( + options.objcSourceOut, + (StringSink sink) => generateObjcSource( + options.objcOptions, parseResults.root, sink)); + } + } else { + errors.add(Error(message: 'No pigeon classes found, nothing generated.')); + } + + printErrors(errors); + + return errors.isNotEmpty ? 1 : 0; + } + + /// Print a list of errors to stderr. + static void printErrors(List errors) { + for (Error err in errors) { + if (err.filename != null) { + if (err.lineNumber != null) { + stderr.writeln( + 'Error: ${err.filename}:${err.lineNumber}: ${err.message}'); + } else { + stderr.writeln('Error: ${err.filename}: ${err.message}'); + } + } else { + stderr.writeln('Error: ${err.message}'); + } + } + } +} diff --git a/packages/pigeon/pigeons/message.dart b/packages/pigeon/pigeons/message.dart new file mode 100644 index 000000000000..c99fc8f9684e --- /dev/null +++ b/packages/pigeon/pigeons/message.dart @@ -0,0 +1,31 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon_lib.dart'; + +class SearchRequest { + String query; +} + +class SearchReply { + String result; +} + +@HostApi() +abstract class Api { + SearchReply search(SearchRequest request); +} + +class Nested { + SearchRequest request; +} + +@HostApi() +abstract class NestedApi { + SearchReply search(Nested nested); +} + +void setupPigeon(PigeonOptions options) { + options.objcOptions.prefix = 'AC'; +} diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml new file mode 100644 index 000000000000..ae831aaeaf0b --- /dev/null +++ b/packages/pigeon/pubspec.yaml @@ -0,0 +1,11 @@ +name: pigeon +version: 0.1.0-experimental.0 +description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. +homepage: https://github.com/flutter/packages/tree/master/packages/pigeon +dependencies: + path: ^1.6.4 + args: ^1.5.2 +dev_dependencies: + test: ^1.11.1 +environment: + sdk: '>=2.2.0 <3.0.0' \ No newline at end of file diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh new file mode 100755 index 000000000000..cc0f99bf9214 --- /dev/null +++ b/packages/pigeon/run_tests.sh @@ -0,0 +1,40 @@ +# exit when any command fails +set -e + +pub run test test/ + +# cd build +# make +# ./dartle --input ../dartles/simple.dartle --dart_out dartle.dart --objc_header_out dartle.h --objc_source_out dartle.m +# cd .. +# cp build/dartle.h tests/test_objc/ios/Runner/ +# cp build/dartle.m tests/test_objc/ios/Runner/ +# cp build/dartle.dart tests/test_objc/lib/ + +# e2e tests are disabled while I work to fix iOS e2e. +# DARTLE_H="e2e_tests/test_objc/ios/Runner/dartle.h" +# DARTLE_M="e2e_tests/test_objc/ios/Runner/dartle.m" +# DARTLE_DART="e2e_tests/test_objc/lib/dartle.dart" +# pub run pigeon \ +# --input pigeons/message.dart \ +# --dart_out $DARTLE_DART \ +# --objc_header_out $DARTLE_H \ +# --objc_source_out $DARTLE_M +# dartfmt -w $DARTLE_DART +# cd e2e_tests/test_objc + +############################################# +# Uncomment to just launch the app. +############################################# +# open -a Simulator +# flutter run +# exit + +# flutter build ios -t test/e2e_test.dart --simulator +# cd ios +# xcodebuild \ +# -workspace Runner.xcworkspace \ +# -scheme RunnerTests \ +# -sdk iphonesimulator \ +# -destination 'platform=iOS Simulator,name=iPhone 8' \ +# test | xcpretty diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart new file mode 100644 index 000000000000..332e40165bc8 --- /dev/null +++ b/packages/pigeon/test/dart_generator_test.dart @@ -0,0 +1,64 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/test.dart'; +import 'package:pigeon/dart_generator.dart'; +import 'package:pigeon/ast.dart'; + +void main() { + test('gen one class', () { + final Class klass = Class() + ..name = 'Foobar' + ..fields = [ + Field() + ..name = 'field1' + ..dataType = 'dataType1' + ]; + final Root root = Root() + ..apis = [] + ..classes = [klass]; + final StringBuffer sink = StringBuffer(); + generateDart(root, sink); + final String code = sink.toString(); + expect(code, contains('class Foobar')); + expect(code, contains(' dataType1 field1;')); + }); + + test('gen one host api', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method(name: 'doSomething', argType: 'Input', returnType: 'Output') + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]) + ]); + final StringBuffer sink = StringBuffer(); + generateDart(root, sink); + final String code = sink.toString(); + expect(code, contains('class Api')); + expect(code, matches('Output.*doSomething.*Input')); + }); + + test('nested class', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Nested', + fields: [Field(name: 'nested', dataType: 'Input')]) + ]); + final StringBuffer sink = StringBuffer(); + generateDart(root, sink); + final String code = sink.toString(); + expect(code, contains('dartleMap["nested"] = nested._toMap()')); + expect( + code, contains('result.nested = Input._fromMap(dartleMap["nested"]);')); + }); +} diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart new file mode 100644 index 000000000000..cac11f822283 --- /dev/null +++ b/packages/pigeon/test/objc_generator_test.dart @@ -0,0 +1,225 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/test.dart'; +import 'package:pigeon/objc_generator.dart'; +import 'package:pigeon/ast.dart'; + +void main() { + test('gen one class header', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Foobar', + fields: [Field(name: 'field1', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(), root, sink); + final String code = sink.toString(); + expect(code, contains('@interface Foobar')); + expect(code, matches('@property.*NSString.*field1')); + }); + + test('gen one class source', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Foobar', + fields: [Field(name: 'field1', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect(code, contains('#import \"foo.h\"')); + expect(code, contains('@implementation Foobar')); + }); + + test('gen one api header', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method(name: 'doSomething', argType: 'Input', returnType: 'Output') + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]) + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(), root, sink); + final String code = sink.toString(); + expect(code, contains('@interface Input')); + expect(code, contains('@interface Output')); + expect(code, contains('@protocol Api')); + expect(code, matches('Output.*doSomething.*Input')); + expect(code, contains('ApiSetup(')); + }); + + test('gen one api source', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method(name: 'doSomething', argType: 'Input', returnType: 'Output') + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]) + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect(code, contains('#import "foo.h"')); + expect(code, contains('@implementation Input')); + expect(code, contains('@implementation Output')); + expect(code, contains('ApiSetup(')); + }); + + test('all the simple datatypes header', () { + final Root root = Root(apis: [], classes: [ + Class(name: 'Foobar', fields: [ + Field(name: 'aBool', dataType: 'bool'), + Field(name: 'aInt', dataType: 'int'), + Field(name: 'aDouble', dataType: 'double'), + Field(name: 'aString', dataType: 'String'), + Field(name: 'aUint8List', dataType: 'Uint8List'), + Field(name: 'aInt32List', dataType: 'Int32List'), + Field(name: 'aInt64List', dataType: 'Int64List'), + Field(name: 'aFloat64List', dataType: 'Float64List'), + ]), + ]); + + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect(code, contains('@interface Foobar')); + expect(code, contains('@class FlutterStandardTypedData;')); + expect(code, matches('@property.*strong.*NSNumber.*aBool')); + expect(code, matches('@property.*strong.*NSNumber.*aInt')); + expect(code, matches('@property.*strong.*NSNumber.*aDouble')); + expect(code, matches('@property.*copy.*NSString.*aString')); + expect(code, + matches('@property.*strong.*FlutterStandardTypedData.*aUint8List')); + expect(code, + matches('@property.*strong.*FlutterStandardTypedData.*aInt32List')); + expect(code, + matches('@property.*strong.*FlutterStandardTypedData.*Int64List')); + expect(code, + matches('@property.*strong.*FlutterStandardTypedData.*Float64List')); + }); + + test('bool source', () { + final Root root = Root(apis: [], classes: [ + Class(name: 'Foobar', fields: [ + Field(name: 'aBool', dataType: 'bool'), + ]), + ]); + + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect(code, contains('@implementation Foobar')); + expect(code, contains('result.aBool = dict[@\"aBool\"];')); + }); + + test('nested class header', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Nested', + fields: [Field(name: 'nested', dataType: 'Input')]) + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect(code, contains('@property(nonatomic, strong) Input * nested;')); + }); + + test('nested class source', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Nested', + fields: [Field(name: 'nested', dataType: 'Input')]) + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink); + final String code = sink.toString(); + expect( + code, contains('result.nested = [Input fromMap:dict[@\"nested\"]];')); + expect(code, contains('[self.nested toMap], @\"nested\"')); + }); + + test('prefix class header', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Foobar', + fields: [Field(name: 'field1', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('@interface ABCFoobar')); + }); + + test('prefix class source', () { + final Root root = Root(apis: [], classes: [ + Class( + name: 'Foobar', + fields: [Field(name: 'field1', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('@implementation ABCFoobar')); + }); + + test('prefix nested class header', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method(name: 'doSomething', argType: 'Input', returnType: 'Nested') + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Nested', + fields: [Field(name: 'nested', dataType: 'Input')]) + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, matches('property.*ABCInput')); + expect(code, matches('ABCNested.*doSomething.*ABCInput')); + expect(code, contains('@protocol ABCApi')); + }); + + test('prefix nested class source', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method(name: 'doSomething', argType: 'Input', returnType: 'Nested') + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Nested', + fields: [Field(name: 'nested', dataType: 'Input')]) + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect(code, contains('ABCInput fromMap')); + expect(code, matches('ABCInput.*=.*ABCInput fromMap')); + expect(code, contains('void ABCApiSetup(')); + }); +} diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart new file mode 100644 index 000000000000..1642e9e807b4 --- /dev/null +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -0,0 +1,122 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/test.dart'; +import 'package:pigeon/pigeon_lib.dart'; +import 'package:pigeon/ast.dart'; + +class Input1 { + String input; +} + +class Output1 { + String output; +} + +@HostApi() +abstract class Api1 { + Output1 doit(Input1 input); +} + +class InvalidDatatype { + dynamic something; +} + +@HostApi() +abstract class ApiTwoMethods { + Output1 method1(Input1 input); + Output1 method2(Input1 input); +} + +class Nested { + Input1 input; +} + +void main() { + test('parse args - input', () { + final PigeonOptions opts = + Pigeon.parseArgs(['--input', 'foo.dart']); + expect(opts.input, equals('foo.dart')); + }); + + test('parse args - dart_out', () { + final PigeonOptions opts = + Pigeon.parseArgs(['--dart_out', 'foo.dart']); + expect(opts.dartOut, equals('foo.dart')); + }); + + test('parse args - objc_header_out', () { + final PigeonOptions opts = + Pigeon.parseArgs(['--objc_header_out', 'foo.h']); + expect(opts.objcHeaderOut, equals('foo.h')); + }); + + test('parse args - objc_source_out', () { + final PigeonOptions opts = + Pigeon.parseArgs(['--objc_source_out', 'foo.m']); + expect(opts.objcSourceOut, equals('foo.m')); + }); + + test('simple parse api', () { + final Pigeon dartle = Pigeon.setup(); + final ParseResults parseResult = dartle.parse([Api1]); + expect(parseResult.errors.length, equals(0)); + final Root root = parseResult.root; + expect(root.classes.length, equals(2)); + expect(root.apis.length, equals(1)); + expect(root.apis[0].name, equals('Api1')); + expect(root.apis[0].methods.length, equals(1)); + expect(root.apis[0].methods[0].name, equals('doit')); + expect(root.apis[0].methods[0].argType, equals('Input1')); + expect(root.apis[0].methods[0].returnType, equals('Output1')); + + Class input; + Class output; + for (Class klass in root.classes) { + if (klass.name == 'Input1') { + input = klass; + } else if (klass.name == 'Output1') { + output = klass; + } + } + expect(input, isNotNull); + expect(output, isNotNull); + + expect(input.fields.length, equals(1)); + expect(input.fields[0].name, equals('input')); + expect(input.fields[0].dataType, equals('String')); + + expect(output.fields.length, equals(1)); + expect(output.fields[0].name, equals('output')); + expect(output.fields[0].dataType, equals('String')); + }); + + test('invalid datatype', () { + final Pigeon dartle = Pigeon.setup(); + final ParseResults results = dartle.parse([InvalidDatatype]); + expect(results.errors.length, 1); + expect(results.errors[0].message, contains('InvalidDatatype')); + expect(results.errors[0].message, contains('dynamic')); + }); + + test('two methods', () { + final Pigeon dartle = Pigeon.setup(); + final ParseResults results = dartle.parse([ApiTwoMethods]); + expect(results.errors.length, 0); + expect(results.root.apis.length, 1); + expect(results.root.apis[0].methods.length, equals(2)); + expect(results.root.apis[0].methods[0].name, equals('method1')); + expect(results.root.apis[0].methods[1].name, equals('method2')); + }); + + test('nested', () { + final Pigeon dartle = Pigeon.setup(); + final ParseResults results = dartle.parse([Nested, Input1]); + expect(results.errors.length, equals(0)); + expect(results.root.classes.length, equals(2)); + expect(results.root.classes[0].name, equals('Nested')); + expect(results.root.classes[0].fields.length, equals(1)); + expect(results.root.classes[0].fields[0].dataType, equals('Input1')); + }); +}