Browse Source

Implement EventSubscriber<T> class.

This class simplifies the task of subscribing/handling javascript events from
C++ classes in the main process.
Thiago de Arruda 7 years ago
parent
commit
68f514b92f
3 changed files with 255 additions and 0 deletions
  1. 121 0
      atom/browser/api/event_subscriber.cc
  2. 132 0
      atom/browser/api/event_subscriber.h
  3. 2 0
      filenames.gypi

+ 121 - 0
atom/browser/api/event_subscriber.cc

@@ -0,0 +1,121 @@
+// Copyright (c) 2017 GitHub, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+#include <string>
+
+#include "atom/browser/api/event_subscriber.h"
+#include "atom/common/native_mate_converters/callback.h"
+
+namespace {
+
+// A FunctionTemplate lifetime is bound to the v8 context, so it can be safely
+// stored as a global here since there's only one for the main process.
+v8::Global<v8::FunctionTemplate> g_cached_template;
+
+struct JSHandlerData {
+  JSHandlerData(v8::Isolate* isolate,
+                mate::internal::EventSubscriberBase* subscriber)
+      : handle_(isolate, v8::External::New(isolate, this)),
+        subscriber_(subscriber) {
+    handle_.SetWeak(this, GC, v8::WeakCallbackType::kFinalizer);
+  }
+
+  static void GC(const v8::WeakCallbackInfo<JSHandlerData>& data) {
+    delete data.GetParameter();
+  }
+
+  v8::Global<v8::External> handle_;
+  mate::internal::EventSubscriberBase* subscriber_;
+};
+
+void InvokeCallback(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Locker locker(info.GetIsolate());
+  v8::HandleScope handle_scope(info.GetIsolate());
+  v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
+  v8::Context::Scope context_scope(context);
+  mate::Arguments args(info);
+  v8::Local<v8::Value> handler, event;
+  args.GetNext(&handler);
+  args.GetNext(&event);
+  DCHECK(handler->IsExternal());
+  DCHECK(event->IsString());
+  JSHandlerData* handler_data = static_cast<JSHandlerData*>(
+      v8::Local<v8::External>::Cast(handler)->Value());
+  handler_data->subscriber_->EventEmitted(mate::V8ToString(event), &args);
+}
+
+}  // namespace
+
+namespace mate {
+
+namespace internal {
+
+EventSubscriberBase::EventSubscriberBase(v8::Isolate* isolate,
+                                         v8::Local<v8::Object> emitter)
+    : isolate_(isolate), emitter_(isolate, emitter) {
+  if (g_cached_template.IsEmpty()) {
+    g_cached_template = v8::Global<v8::FunctionTemplate>(
+        isolate_, v8::FunctionTemplate::New(isolate_, InvokeCallback));
+  }
+}
+
+EventSubscriberBase::~EventSubscriberBase() {
+  if (!isolate_) {
+    return;
+  }
+  RemoveAllListeners();
+  emitter_.Reset();
+  DCHECK_EQ(js_handlers_.size(), 0);
+}
+
+void EventSubscriberBase::On(const std::string& event_name) {
+  DCHECK(js_handlers_.find(event_name) == js_handlers_.end());
+  v8::Locker locker(isolate_);
+  v8::Isolate::Scope isolate_scope(isolate_);
+  v8::HandleScope handle_scope(isolate_);
+  auto fn_template = g_cached_template.Get(isolate_);
+  auto event = mate::StringToV8(isolate_, event_name);
+  auto js_handler_data = new JSHandlerData(isolate_, this);
+  v8::Local<v8::Value> fn = internal::BindFunctionWith(
+      isolate_, isolate_->GetCurrentContext(), fn_template->GetFunction(),
+      js_handler_data->handle_.Get(isolate_), event);
+  js_handlers_.insert(
+      std::make_pair(event_name, v8::Global<v8::Value>(isolate_, fn)));
+  internal::ValueVector converted_args = {event, fn};
+  internal::CallMethodWithArgs(isolate_, emitter_.Get(isolate_), "on",
+                               &converted_args);
+}
+
+void EventSubscriberBase::Off(const std::string& event_name) {
+  v8::Locker locker(isolate_);
+  v8::Isolate::Scope isolate_scope(isolate_);
+  v8::HandleScope handle_scope(isolate_);
+  auto js_handler = js_handlers_.find(event_name);
+  DCHECK(js_handler != js_handlers_.end());
+  RemoveListener(js_handler);
+}
+
+void EventSubscriberBase::RemoveAllListeners() {
+  v8::Locker locker(isolate_);
+  v8::Isolate::Scope isolate_scope(isolate_);
+  v8::HandleScope handle_scope(isolate_);
+  while (!js_handlers_.empty()) {
+    RemoveListener(js_handlers_.begin());
+  }
+}
+
+std::map<std::string, v8::Global<v8::Value>>::iterator
+EventSubscriberBase::RemoveListener(
+    std::map<std::string, v8::Global<v8::Value>>::iterator it) {
+  internal::ValueVector args = {StringToV8(isolate_, it->first),
+                                it->second.Get(isolate_)};
+  internal::CallMethodWithArgs(
+      isolate_, v8::Local<v8::Object>::Cast(emitter_.Get(isolate_)),
+      "removeListener", &args);
+  it->second.Reset();
+  return js_handlers_.erase(it);
+}
+
+}  // namespace internal
+
+}  // namespace mate

+ 132 - 0
atom/browser/api/event_subscriber.h

@@ -0,0 +1,132 @@
+// Copyright (c) 2017 GitHub, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ATOM_BROWSER_API_EVENT_SUBSCRIBER_H_
+#define ATOM_BROWSER_API_EVENT_SUBSCRIBER_H_
+
+#include <map>
+#include <string>
+
+#include "atom/common/api/event_emitter_caller.h"
+#include "base/synchronization/lock.h"
+#include "content/public/browser/browser_thread.h"
+#include "native_mate/native_mate/arguments.h"
+
+namespace mate {
+
+namespace internal {
+
+class EventSubscriberBase {
+ public:
+  EventSubscriberBase(v8::Isolate* isolate, v8::Local<v8::Object> emitter);
+  virtual ~EventSubscriberBase();
+  virtual void EventEmitted(const std::string& event_name,
+                            mate::Arguments* args) = 0;
+
+ protected:
+  void On(const std::string& event_name);
+  void Off(const std::string& event_name);
+  void RemoveAllListeners();
+
+ private:
+  std::map<std::string, v8::Global<v8::Value>>::iterator RemoveListener(
+      std::map<std::string, v8::Global<v8::Value>>::iterator it);
+
+  v8::Isolate* isolate_;
+  v8::Global<v8::Object> emitter_;
+  std::map<std::string, v8::Global<v8::Value>> js_handlers_;
+
+  DISALLOW_COPY_AND_ASSIGN(EventSubscriberBase);
+};
+
+}  // namespace internal
+
+template <typename HandlerType>
+class EventSubscriber : internal::EventSubscriberBase {
+ public:
+  using EventCallback = void (HandlerType::*)(mate::Arguments* args);
+  // Alias to unique_ptr with deleter.
+  using unique_ptr = std::unique_ptr<EventSubscriber<HandlerType>,
+                                     void (*)(EventSubscriber<HandlerType>*)>;
+  // EventSubscriber should only be created/deleted in the main thread since it
+  // communicates with the V8 engine. This smart pointer makes it simpler to
+  // bind the lifetime of EventSubscriber with a class whose lifetime is managed
+  // by a non-UI thread.
+  class SafePtr : public unique_ptr {
+   public:
+    SafePtr() : SafePtr(nullptr) {}
+    explicit SafePtr(EventSubscriber<HandlerType>* ptr)
+        : unique_ptr(ptr, Deleter) {}
+
+   private:
+    // Custom deleter that schedules destructor invocation to the main thread.
+    static void Deleter(EventSubscriber<HandlerType>* ptr) {
+      DCHECK(
+          !::content::BrowserThread::CurrentlyOn(::content::BrowserThread::UI));
+      DCHECK(ptr);
+      // Acquire handler lock and reset handler_ to ensure that any new events
+      // emitted will be ignored after this function returns
+      base::AutoLock auto_lock(ptr->handler_lock_);
+      ptr->handler_ = nullptr;
+      content::BrowserThread::PostTask(
+          content::BrowserThread::UI, FROM_HERE,
+          base::Bind(
+              [](EventSubscriber<HandlerType>* subscriber) {
+                delete subscriber;
+              },
+              ptr));
+    }
+  };
+
+  EventSubscriber(HandlerType* handler,
+                  v8::Isolate* isolate,
+                  v8::Local<v8::Object> emitter)
+      : EventSubscriberBase(isolate, emitter), handler_(handler) {
+    DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
+  }
+
+  void On(const std::string& event, EventCallback callback) {
+    DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
+    EventSubscriberBase::On(event);
+    callbacks_.insert(std::make_pair(event, callback));
+  }
+
+  void Off(const std::string& event) {
+    DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
+    EventSubscriberBase::Off(event);
+    DCHECK(callbacks_.find(event) != callbacks_.end());
+    callbacks_.erase(callbacks_.find(event));
+  }
+
+  void RemoveAllListeners() {
+    DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
+    EventSubscriberBase::RemoveAllListeners();
+    callbacks_.clear();
+  }
+
+ private:
+  void EventEmitted(const std::string& event_name,
+                    mate::Arguments* args) override {
+    DCHECK_CURRENTLY_ON(::content::BrowserThread::UI);
+    base::AutoLock auto_lock(handler_lock_);
+    if (!handler_) {
+      // handler_ was probably destroyed by another thread and we should not
+      // access it.
+      return;
+    }
+    auto it = callbacks_.find(event_name);
+    if (it != callbacks_.end()) {
+      auto method = it->second;
+      (handler_->*method)(args);
+    }
+  }
+
+  HandlerType* handler_;
+  base::Lock handler_lock_;
+  std::map<std::string, EventCallback> callbacks_;
+};
+
+}  // namespace mate
+
+#endif  // ATOM_BROWSER_API_EVENT_SUBSCRIBER_H_

+ 2 - 0
filenames.gypi

@@ -163,6 +163,8 @@
       'atom/browser/api/event.h',
       'atom/browser/api/event_emitter.cc',
       'atom/browser/api/event_emitter.h',
+      'atom/browser/api/event_subscriber.cc',
+      'atom/browser/api/event_subscriber.h',
       'atom/browser/api/trackable_object.cc',
       'atom/browser/api/trackable_object.h',
       'atom/browser/api/frame_subscriber.cc',