Skip to content

Commit

Permalink
[Pigeon] Added java target for host functions. (flutter#96)
Browse files Browse the repository at this point in the history
* [Pigeon] Added java target for host functions.
  • Loading branch information
gaaclarke authored Feb 20, 2020
1 parent b1e6038 commit 389c4fc
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 2 deletions.
4 changes: 4 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.1.0-experimental.3

* Added Flutter->Host calls for Android Java.

## 0.1.0-experimental.2

* Added Host->Flutter calls for Objective-C
Expand Down
147 changes: 147 additions & 0 deletions packages/pigeon/lib/java_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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 Java code will be generated.
class JavaOptions {
/// The name of the class that will house all the generated classes.
String className;

/// The package where the generated class will live.
String package;
}

void _writeHostApi(Indent indent, Api api) {
assert(api.location == ApiLocation.host);
indent.writeln(
'/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/');
indent.write('public interface ${api.name} ');
indent.scoped('{', '}', () {
for (Method method in api.methods) {
indent.writeln(
'${method.returnType} ${method.name}(${method.argType} arg);');
}
indent.addln('');
indent.writeln(
'/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger` */');
indent.write(
'public static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
indent.scoped('{', '}', () {
for (Method method in api.methods) {
final String channelName = makeChannelName(api, method);
indent.write('');
indent.scoped('{', '}', () {
indent.writeln('BasicMessageChannel<Object> channel =');
indent.inc();
indent.inc();
indent.writeln(
'new BasicMessageChannel<Object>(binaryMessenger, "$channelName", new StandardMessageCodec());');
indent.dec();
indent.dec();
indent.write(
'channel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() ');
indent.scoped('{', '});', () {
indent.write(
'public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) ');
indent.scoped('{', '}', () {
final String argType = method.argType;
final String returnType = method.returnType;
indent.writeln(
'$argType input = $argType.fromMap((HashMap)message);');
indent.writeln('$returnType output = api.${method.name}(input);');
indent.writeln('reply.reply(output.toMap());');
});
});
});
}
});
});
}

// void _writeFlutterApi(Indent indent, Api api) {
// assert(api.location == ApiLocation.flutter);
// }

String _makeGetter(Field field) {
final String uppercased =
field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
return 'get$uppercased';
}

String _makeSetter(Field field) {
final String uppercased =
field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
return 'set$uppercased';
}

/// Generates the ".java" file for the AST represented by [root] to [sink] with the
/// provided [options].
void generateJava(JavaOptions options, Root root, StringSink sink) {
final Indent indent = Indent(sink);
indent.writeln('// Autogenerated from Pigeon, do not edit directly.');
indent.writeln('// See also: https://pub.dev/packages/pigeon');
indent.addln('');
if (options.package != null) {
indent.writeln('package ${options.package};');
}
indent.addln('');

indent.writeln('import java.util.HashMap;');
indent.addln('');
indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;');
indent.writeln('import io.flutter.plugin.common.BinaryMessenger;');
indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;');

indent.addln('');
assert(options.className != null);
indent.writeln('/** Generated class from Pigeon. */');
indent.write('public class ${options.className} ');
indent.scoped('{', '}', () {
for (Class klass in root.classes) {
indent.addln('');
indent.writeln(
'/** Generated class from Pigeon that represents data sent in messages. */');
indent.write('public static class ${klass.name} ');
indent.scoped('{', '}', () {
for (Field field in klass.fields) {
indent.writeln('private ${field.dataType} ${field.name};');
indent.writeln(
'public ${field.dataType} ${_makeGetter(field)}() { return ${field.name}; }');
indent.writeln(
'public void ${_makeSetter(field)}(${field.dataType} setterArg) { this.${field.name} = setterArg; }');
indent.addln('');
}
indent.write('HashMap toMap() ');
indent.scoped('{', '}', () {
indent.writeln(
'HashMap<String, Object> toMapResult = new HashMap<String, Object>();');
for (Field field in klass.fields) {
indent.writeln('toMapResult.put("${field.name}", ${field.name});');
}
indent.writeln('return toMapResult;');
});
indent.write('static ${klass.name} fromMap(HashMap map) ');
indent.scoped('{', '}', () {
indent.writeln('${klass.name} fromMapResult = new ${klass.name}();');
for (Field field in klass.fields) {
indent.writeln(
'fromMapResult.${field.name} = (${field.dataType})map.get("${field.name}");');
}
indent.writeln('return fromMapResult;');
});
});
}

for (Api api in root.apis) {
indent.addln('');
if (api.location == ApiLocation.host) {
_writeHostApi(indent, api);
} else if (api.location == ApiLocation.flutter) {
// _writeFlutterApi(indent, api);
}
}
});
}
22 changes: 21 additions & 1 deletion packages/pigeon/lib/pigeon_lib.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:mirrors';

import 'package:args/args.dart';
import 'package:path/path.dart';
import 'package:pigeon/java_generator.dart';

import 'ast.dart';
import 'dart_generator.dart';
Expand Down Expand Up @@ -90,6 +91,12 @@ class PigeonOptions {

/// Options that control how Objective-C will be generated.
ObjcOptions objcOptions = ObjcOptions();

/// Path to the java file that will be generated.
String javaOut;

/// Options that control how Java will be generated.
JavaOptions javaOptions = JavaOptions();
}

/// A collection of an AST represented as a [Root] and [Error]'s.
Expand Down Expand Up @@ -196,6 +203,7 @@ options:
help: 'REQUIRED: Path to generated dart source file (.dart).')
..addOption('objc_source_out',
help: 'Path to generated Objective-C source file (.m).')
..addOption('java_out', help: 'Path to generated Java file (.java).')
..addOption('objc_header_out',
help: 'Path to generated Objective-C header file (.h).')
..addOption('objc_prefix',
Expand All @@ -211,6 +219,7 @@ options:
opts.objcHeaderOut = results['objc_header_out'];
opts.objcSourceOut = results['objc_source_out'];
opts.objcOptions.prefix = results['objc_prefix'];
opts.javaOut = results['java_out'];
return opts;
}

Expand Down Expand Up @@ -278,7 +287,12 @@ options:

final List<Error> errors = <Error>[];
final List<Type> apis = <Type>[];
options.objcOptions.header = basename(options.objcHeaderOut);
if (options.objcHeaderOut != null) {
options.objcOptions.header = basename(options.objcHeaderOut);
}
if (options.javaOut != null) {
options.javaOptions.className = basenameWithoutExtension(options.javaOut);
}

for (LibraryMirror library in currentMirrorSystem().libraries.values) {
for (DeclarationMirror declaration in library.declarations.values) {
Expand Down Expand Up @@ -309,6 +323,12 @@ options:
(StringSink sink) => generateObjcSource(
options.objcOptions, parseResults.root, sink));
}
if (options.javaOut != null) {
await _runGenerator(
options.javaOut,
(StringSink sink) =>
generateJava(options.javaOptions, parseResults.root, sink));
}
} else {
errors.add(Error(message: 'No pigeon classes found, nothing generated.'));
}
Expand Down
1 change: 1 addition & 0 deletions packages/pigeon/pigeons/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ abstract class NestedApi {

void setupPigeon(PigeonOptions options) {
options.objcOptions.prefix = 'AC';
options.javaOptions.package = 'dev.flutter.aaclarke.pigeon';
}

@FlutterApi()
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: pigeon
version: 0.1.0-experimental.2
version: 0.1.0-experimental.3
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:
Expand Down
19 changes: 19 additions & 0 deletions packages/pigeon/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,26 @@ test_pigeon_ios() {
rm -rf $temp_dir
}

test_pigeon_android() {
temp_dir=$(mktemp -d -t pigeon)

pub run pigeon \
--input $1 \
--dart_out $temp_dir/pigeon.dart \
--java_out $temp_dir/Pigeon.java \

if ! javac $temp_dir/Pigeon.java \
-Xlint:unchecked \
-classpath "$flutter_bin/cache/artifacts/engine/android-x64/flutter.jar"; then
echo "javac $temp_dir/Pigeon.java failed"
exit 1
fi

rm -rf $temp_dir
}

pub run test test/
test_pigeon_android ./pigeons/message.dart
test_pigeon_ios ./pigeons/message.dart
test_pigeon_ios ./pigeons/host2flutter.dart

Expand Down
73 changes: 73 additions & 0 deletions packages/pigeon/test/java_generator_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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/java_generator.dart';
import 'package:pigeon/ast.dart';

void main() {
test('gen one class', () {
final Class klass = Class()
..name = 'Foobar'
..fields = <Field>[
Field()
..name = 'field1'
..dataType = 'int'
];
final Root root = Root()
..apis = <Api>[]
..classes = <Class>[klass];
final StringBuffer sink = StringBuffer();
final JavaOptions javaOptions = JavaOptions();
javaOptions.className = 'Messages';
generateJava(javaOptions, root, sink);
final String code = sink.toString();
expect(code, contains('public class Messages'));
expect(code, contains('public static class Foobar'));
expect(code, contains('private int field1;'));
});

test('package', () {
final Class klass = Class()
..name = 'Foobar'
..fields = <Field>[
Field()
..name = 'field1'
..dataType = 'int'
];
final Root root = Root()
..apis = <Api>[]
..classes = <Class>[klass];
final StringBuffer sink = StringBuffer();
final JavaOptions javaOptions = JavaOptions();
javaOptions.className = 'Messages';
javaOptions.package = 'com.google.foobar';
generateJava(javaOptions, root, sink);
final String code = sink.toString();
expect(code, contains('package com.google.foobar;'));
expect(code, contains('HashMap toMap()'));
});

test('gen one host api', () {
final Root root = Root(apis: <Api>[
Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
])
], classes: <Class>[
Class(
name: 'Input',
fields: <Field>[Field(name: 'input', dataType: 'String')]),
Class(
name: 'Output',
fields: <Field>[Field(name: 'output', dataType: 'String')])
]);
final StringBuffer sink = StringBuffer();
final JavaOptions javaOptions = JavaOptions();
javaOptions.className = 'Messages';
generateJava(javaOptions, root, sink);
final String code = sink.toString();
expect(code, contains('public interface Api'));
expect(code, matches('Output.*doSomething.*Input'));
});
}
6 changes: 6 additions & 0 deletions packages/pigeon/test/pigeon_lib_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ void main() {
expect(opts.dartOut, equals('foo.dart'));
});

test('parse args - input', () {
final PigeonOptions opts =
Pigeon.parseArgs(<String>['--java_out', 'foo.java']);
expect(opts.javaOut, equals('foo.java'));
});

test('parse args - objc_header_out', () {
final PigeonOptions opts =
Pigeon.parseArgs(<String>['--objc_header_out', 'foo.h']);
Expand Down

0 comments on commit 389c4fc

Please sign in to comment.