Skip to content

Commit

Permalink
Rework Linux plugin API (#61)
Browse files Browse the repository at this point in the history
Replaces the synchronous plugin response using raw data with an
asynchronous response using a much higher-level abstraction that makes
sending non-empty responses much easier.

This makes the Linux plugin API consistent with the current state of
the macOS plugin API. It follows Flutter's Java plugin API response
object pattern, since without RTTI the ObjC approach of determining
response type in a single callback isn't viable.

As with the macOS rework, this is an incremental change. A future change
will further align the macOS API with mobile, but this portion makes most
of the changes that would significantly change the structure of a plugin
(allowing for rich, async responses).

This addresses issue #45 and issue #13
  • Loading branch information
stuartmorgan committed May 31, 2018
1 parent 52adb83 commit 4c3db78
Show file tree
Hide file tree
Showing 18 changed files with 424 additions and 227 deletions.
5 changes: 5 additions & 0 deletions .clang-format
@@ -0,0 +1,5 @@
BasedOnStyle: Google
---
Language: Cpp
DerivePointerAlignment: false
PointerAlignment: Right
120 changes: 120 additions & 0 deletions linux/library/include/flutter_desktop_embedding/channels.h
@@ -0,0 +1,120 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef LINUX_INCLUDE_FLUTTER_DESKTOP_EMBEDDING_CHANNELS_H_
#define LINUX_INCLUDE_FLUTTER_DESKTOP_EMBEDDING_CHANNELS_H_

#include <json/json.h>

#include <memory>
#include <string>

#include <embedder.h>

namespace flutter_desktop_embedding {

// An object encapsulating a method call from Flutter.
// TODO: Move serialization details into a method codec class, to match mobile
// Flutter plugin APIs.
class MethodCall {
public:
// Creates a MethodCall with the given name and, optionally, arguments.
explicit MethodCall(const std::string &method_name,
const Json::Value &arguments = Json::Value());

// Returns a new MethodCall created from a JSON message received from the
// Flutter engine.
static std::unique_ptr<MethodCall> CreateFromMessage(
const Json::Value &message);
~MethodCall();

// The name of the method being called.
const std::string &method_name() const { return method_name_; }

// The arguments to the method call, or a nullValue if there are none.
const Json::Value &arguments() const { return arguments_; }

// Returns a version of the method call serialized in the format expected by
// the Flutter engine.
Json::Value AsMessage() const;

private:
std::string method_name_;
Json::Value arguments_;
};

// Encapsulates a result sent back to the Flutter engine in response to a
// MethodCall. Only one method should be called on any given instance.
class MethodResult {
public:
// Sends a success response, indicating that the call completed successfully.
// An optional value can be provided as part of the success message.
void Success(const Json::Value &result = Json::Value());

// Sends an error response, indicating that the call was understood but
// handling failed in some way. A string error code must be provided, and in
// addition an optional user-readable error_message and/or details object can
// be included.
void Error(const std::string &error_code,
const std::string &error_message = "",
const Json::Value &error_details = Json::Value());

// Sends a not-implemented response, indicating that the method either was not
// recognized, or has not been implemented.
void NotImplemented();

protected:
// Internal implementation of the interface methods above, to be implemented
// in subclasses.
virtual void SuccessInternal(const Json::Value &result) = 0;
virtual void ErrorInternal(const std::string &error_code,
const std::string &error_message,
const Json::Value &error_details) = 0;
virtual void NotImplementedInternal() = 0;
};

// Implemention of MethodResult using JSON as the protocol.
// TODO: Move this logic into method codecs.
class JsonMethodResult : public MethodResult {
public:
// Creates a result object that will send results to |engine|, tagged as
// associated with |response_handle|. The engine pointer must remain valid for
// as long as this object exists.
JsonMethodResult(FlutterEngine engine,
const FlutterPlatformMessageResponseHandle *response_handle);
~JsonMethodResult();

protected:
void SuccessInternal(const Json::Value &result) override;
void ErrorInternal(const std::string &error_code,
const std::string &error_message,
const Json::Value &error_details) override;
void NotImplementedInternal() override;

private:
// Sends the given JSON response object to the engine.
void SendResponseJson(const Json::Value &response);

// Sends the given response data (which must either be nullptr or a serialized
// JSON response) to the engine.
// Uses a pointer rather than a reference since nullptr is used to indicate
// not-implemented (which is represented to Flutter by no data).
void SendResponse(const std::string *serialized_response);

FlutterEngine engine_;
const FlutterPlatformMessageResponseHandle *response_handle_;
};

} // namespace flutter_desktop_embedding

#endif // LINUX_INCLUDE_FLUTTER_DESKTOP_EMBEDDING_CHANNELS_H_
Expand Up @@ -25,7 +25,12 @@ class ColorPickerPlugin : public Plugin {
ColorPickerPlugin();
virtual ~ColorPickerPlugin();

Json::Value HandlePlatformMessage(const Json::Value &message) override;
void HandleMethodCall(const MethodCall &method_call,
std::unique_ptr<MethodResult> result) override;

protected:
// Hides the color picker panel if it is showing.
void HidePanel();

private:
// Private implementation.
Expand Down

This file was deleted.

Expand Up @@ -23,7 +23,8 @@ class FileChooserPlugin : public Plugin {
FileChooserPlugin();
virtual ~FileChooserPlugin();

Json::Value HandlePlatformMessage(const Json::Value &message) override;
void HandleMethodCall(const MethodCall &method_call,
std::unique_ptr<MethodResult> result) override;
};

} // namespace flutter_desktop_embedding
Expand Down
16 changes: 10 additions & 6 deletions linux/library/include/flutter_desktop_embedding/plugin.h
Expand Up @@ -16,10 +16,13 @@
#include <json/json.h>

#include <functional>
#include <memory>
#include <string>

#include <embedder.h>

#include "channels.h"

namespace flutter_desktop_embedding {

// Represents a plugin that can be registered with the Flutter Embedder.
Expand All @@ -37,11 +40,12 @@ class Plugin {
explicit Plugin(std::string channel, bool input_blocking = false);
virtual ~Plugin();

// Handles a platform message sent on this platform's channel.
// Handles a method call from Flutter on this platform's channel.
//
// If some error has occurred or there is no valid response that can be
// made, must return a Json::nullValue object.
virtual Json::Value HandlePlatformMessage(const Json::Value &message) = 0;
// Implementations must call exactly one of the methods on |result|,
// exactly once. Failure to indicate a |result| is a memory leak.
virtual void HandleMethodCall(const MethodCall &method_call,
std::unique_ptr<MethodResult> result) = 0;

// Returns the channel on which this plugin listens.
virtual std::string channel() const { return channel_; }
Expand All @@ -58,8 +62,8 @@ class Plugin {
virtual void set_flutter_engine(FlutterEngine engine) { engine_ = engine; }

protected:
// Sends a message to the flutter engine on this Plugin's channel.
void SendMessageToFlutterEngine(const Json::Value &json);
// Calls a method in the Flutter engine on this Plugin's channel.
void InvokeMethod(const std::string &method, const Json::Value &arguments);

private:
std::string channel_;
Expand Down
15 changes: 7 additions & 8 deletions linux/library/include/flutter_desktop_embedding/plugin_handler.h
Expand Up @@ -42,14 +42,13 @@ class PluginHandler {
// In the event that the plugin on |channel| is input blocking, calls the
// caller-defined callbacks to block and then unblock input.
//
// See the documentation for Plugin on the type of response that can be
// returned.
//
// If there is no plugin under |channel| Json::nullValue is returned.
Json::Value HandlePlatformMessage(
const std::string& channel, const Json::Value& message,
std::function<void(void)> input_block_cb = [] {},
std::function<void(void)> input_unblock_cb = [] {});
// If no plugin is registered for the channel, NotImplemented is called on
// |result|.
void HandleMethodCall(const std::string &channel,
const MethodCall &method_call,
std::unique_ptr<MethodResult> result,
std::function<void(void)> input_block_cb = [] {},
std::function<void(void)> input_unblock_cb = [] {});

private:
std::map<std::string, std::unique_ptr<Plugin>> plugins_;
Expand Down
Expand Up @@ -79,7 +79,7 @@ class TextInputModel {
void MoveCursorToEnd();

// Returns the state in the form of a platform message.
Json::Value GetState();
Json::Value GetState() const;

int client_id() const { return client_id_; }

Expand Down
Expand Up @@ -33,7 +33,8 @@ class TextInputPlugin : public KeyboardHookHandler, public Plugin {
virtual ~TextInputPlugin();

// Plugin.
Json::Value HandlePlatformMessage(const Json::Value &message) override;
void HandleMethodCall(const MethodCall &method_call,
std::unique_ptr<MethodResult> result) override;

// KeyboardHookHandler.
void KeyboardHook(GLFWwindow *window, int key, int scancode, int action,
Expand All @@ -43,6 +44,9 @@ class TextInputPlugin : public KeyboardHookHandler, public Plugin {
void CharHook(GLFWwindow *window, unsigned int code_point) override;

private:
// Sends the current state of the given model to the Flutter engine.
void SendStateUpdate(const TextInputModel &model);

// Mapping of client IDs to text input models.
std::map<int, std::unique_ptr<TextInputModel>> input_models_;

Expand Down
122 changes: 122 additions & 0 deletions linux/library/src/channels.cc
@@ -0,0 +1,122 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <flutter_desktop_embedding/channels.h>

#include <iostream>

namespace flutter_desktop_embedding {

constexpr char kMessageMethodKey[] = "method";
constexpr char kMessageArgumentsKey[] = "args";

MethodCall::MethodCall(const std::string &method_name,
const Json::Value &arguments)
: method_name_(method_name), arguments_(arguments) {}

MethodCall::~MethodCall() {}

std::unique_ptr<MethodCall> MethodCall::CreateFromMessage(
const Json::Value &message) {
Json::Value method = message[kMessageMethodKey];
if (method.isNull()) {
return nullptr;
}
Json::Value arguments = message[kMessageArgumentsKey];
return std::make_unique<MethodCall>(method.asString(), arguments);
}

Json::Value MethodCall::AsMessage() const {
Json::Value message(Json::objectValue);
message[kMessageMethodKey] = method_name_;
message[kMessageArgumentsKey] = arguments_;
return message;
}

void MethodResult::Success(const Json::Value &result) {
SuccessInternal(result);
}

void MethodResult::Error(const std::string &error_code,
const std::string &error_message,
const Json::Value &error_details) {
ErrorInternal(error_code, error_message, error_details);
}

void MethodResult::NotImplemented() { NotImplementedInternal(); }

JsonMethodResult::JsonMethodResult(
FlutterEngine engine,
const FlutterPlatformMessageResponseHandle *response_handle)
: engine_(engine), response_handle_(response_handle) {
if (!response_handle_) {
std::cerr << "Error: Response handle must be provided for a response."
<< std::endl;
}
}

JsonMethodResult::~JsonMethodResult() {
if (response_handle_) {
// Warn, rather than send a not-implemented response, since the engine may
// no longer be valid at this point.
std::cerr
<< "Warning: Failed to respond to a message. This is a memory leak."
<< std::endl;
}
}

void JsonMethodResult::SuccessInternal(const Json::Value &result) {
Json::Value response(Json::arrayValue);
response.append(result);
SendResponseJson(response);
}

void JsonMethodResult::ErrorInternal(const std::string &error_code,
const std::string &error_message,
const Json::Value &error_details) {
Json::Value response(Json::arrayValue);
response.append(error_code);
response.append(error_message.empty() ? Json::Value() : error_message);
response.append(error_details);
SendResponseJson(response);
}

void JsonMethodResult::NotImplementedInternal() { SendResponse(nullptr); }

void JsonMethodResult::SendResponseJson(const Json::Value &response) {
Json::StreamWriterBuilder writer_builder;
std::string response_data = Json::writeString(writer_builder, response);
SendResponse(&response_data);
}

void JsonMethodResult::SendResponse(const std::string *serialized_response) {
if (!response_handle_) {
std::cerr
<< "Error: Response can be set only once. Ignoring duplicate response."
<< std::endl;
return;
}

const uint8_t *message_data =
serialized_response
? reinterpret_cast<const uint8_t *>(serialized_response->c_str())
: nullptr;
size_t message_length = serialized_response ? serialized_response->size() : 0;
FlutterEngineSendPlatformMessageResponse(engine_, response_handle_,
message_data, message_length);
// The engine frees the response handle once
// FlutterEngineSendPlatformMessageResponse is called.
response_handle_ = nullptr;
}

} // namespace flutter_desktop_embedding

0 comments on commit 4c3db78

Please sign in to comment.