blob: e715b9724053f551b3832ba22f6cb335a51afe83 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SRC_UI_SCENIC_LIB_UTILS_OBJECT_LINKER_H_
#define SRC_UI_SCENIC_LIB_UTILS_OBJECT_LINKER_H_
#include <lib/async/cpp/wait.h>
#include <lib/fit/function.h>
#include <lib/zx/handle.h>
#include <lib/zx/object_traits.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <cstdint>
#include <memory>
#include <mutex>
#include <type_traits>
#include <unordered_map>
#include "lib/async/dispatcher.h"
#include "src/lib/fxl/macros.h"
#include "src/lib/fxl/memory/weak_ptr.h"
#include "src/ui/scenic/lib/scenic/util/error_reporter.h"
namespace utils {
using scenic_impl::ErrorReporter;
// Contains common linking functionality that operates on type-erased objects.
// Use ObjectLinker to link objects of concrete types together.
class ObjectLinkerBase {
public:
virtual ~ObjectLinkerBase() = default;
size_t ExportCount() {
auto access = GetScopedAccess();
return exports_.size();
}
size_t UnresolvedExportCount();
size_t ImportCount() {
auto access = GetScopedAccess();
return imports_.size();
}
size_t UnresolvedImportCount();
protected:
class Link {
public:
virtual ~Link() = default;
protected:
// When invalidating a link, the caller may choose to not invalidate the peer, which instead
// returns the peer to an initialized-but-unresolved state. This is primarily used when the
// caller releases the token of an existing link.
virtual void Invalidate(bool on_destruction, bool invalidate_peer) = 0;
// Must be virtual so ObjectLinker::Link can pull the typed PeerObject from `peer_link`.
virtual void LinkResolved(ObjectLinkerBase::Link* peer_link) = 0;
// Invalidating a link deletes the token it was created with, making the link permanently
// invalid and therefore allowing for the deletion of the |link_invalidated_| callback.
// Unresolving a link means its peer's token was released and may be used again, so the callback
// is called but not deleted.
void LinkInvalidatedLocked(bool on_destruction);
void LinkUnresolvedLocked();
std::function<void(bool on_destruction)> link_invalidated_;
friend class ObjectLinkerBase;
};
// Information for one end of a Link registered with the linker.
struct Endpoint {
Endpoint(zx_koid_t peer_endpoint_id, zx::handle token, async_dispatcher_t* dispatcher,
std::unique_ptr<async::Wait> peer_death_waiter)
: peer_endpoint_id(peer_endpoint_id),
link(nullptr),
token(std::move(token)),
dispatcher(dispatcher),
peer_death_waiter(std::move(peer_death_waiter)) {
FX_DCHECK(dispatcher);
}
Endpoint(Endpoint&&) = default;
~Endpoint() {
if (!peer_death_waiter) {
return;
}
// Cancel the |peer_death_waiter| explicitly, rather than implicitly in the
// |async::Wait| dtor, to enable logging of failures.
zx_status_t res = peer_death_waiter->Cancel();
if (res != ZX_OK) {
FX_LOGS(INFO) << "Failed to cancel |peer_death_waiter_|: " << zx_status_get_string(res);
}
}
bool IsUnresolved() const { return peer_death_waiter != nullptr; }
zx_koid_t peer_endpoint_id;
Link* link;
zx::handle token; // The token may be released by the link owner.
async_dispatcher_t* dispatcher;
std::unique_ptr<async::Wait> peer_death_waiter; // Only non-null if the link is unresolved.
};
ObjectLinkerBase() : weak_ptr_factory_(this) {}
// Creates a new Endpoint for linking and reports any errors in creation
// using |error_reporter|.
//
// Returns a zx_koid_t that uniquely identifies the registered Endpoint, or
// ZX_KOID_INVALID if creation failed.
zx_koid_t CreateEndpoint(zx::handle token, ErrorReporter* error_reporter, bool is_import);
// Destroys the Endpoint pointed to by |endpoint_id| and removes all traces
// of it from the linker. If the Endpoint is linked to a peer, the peer
// will be notified of the Endpoint's destruction.
void DestroyEndpoint(zx_koid_t endpoint_id, bool is_import, bool destroy_peer);
// Puts the Endpoint pointed to by |endpoint_id| into an initialized state
// by supplying it with an object and connection callbacks. The Endpoint
// will not be linked until its peer is also initialized.
void InitializeEndpoint(ObjectLinkerBase::Link* link, zx_koid_t endpoint_id, bool is_import);
// Attempts linking of the endpoints associated with |endpoint_id| and |peer_endpoint_id|.
//
// The operation will only succeed if both endpoints have been initialized first.
void AttemptLinking(zx_koid_t endpoint_id, zx_koid_t peer_endpoint_id, bool is_import);
// Sets up an async::Wait on |Endpoint| that will fire a callback if the Endpoint peer's token is
// destroyed before a link has been established.
// All wait tasks run on the Endpoint's dispatcher, i.e. the default dispatcher at the Endpoint's
// construction time.
std::unique_ptr<async::Wait> WaitForPeerDeath(async_dispatcher_t* dispatcher,
zx_handle_t endpoint_handle, zx_koid_t endpoint_id,
bool is_import);
// Releases the zx::handle for the Endpoint associated with |endpoint_id|, allowing the caller
// to establish a new link with it.
//
// This operation works regardless of whether or not the link has resolved. If the link was
// resolved, the peer Endpoint receives a |link_invalidated| callback and is put back in the
// initialized-but-unresolved state.
zx::handle ReleaseToken(zx_koid_t endpoint_id, bool is_import);
// Class that encapsulates acquiring |mutex_| to provide serialized access to ObjectLinker and
// Links created from it.
// This protects internal data structures of ObjectLinker, |exports_| and |imports_|, and Links
// that are referenced under these data structures. i.e. one thread may run AttemptLinking()
// while the other invalidates the Link endpoint, which would cause unexpected behavior and
// dropped link_resolved callback. Therefore, each ObjectLinker method accessing these data
// structures and each Link method modifying internals should hold ScopedAccess to make sure
// calls are serialized.
class ScopedAccess {
public:
ScopedAccess() = default;
explicit ScopedAccess(ObjectLinkerBase* linker) : linker_(linker) { linker_->mutex_.lock(); }
~ScopedAccess() {
if (linker_) {
linker_->mutex_.unlock();
}
}
private:
ObjectLinkerBase* linker_ = nullptr;
};
// Each method should acquire this before execution.
ScopedAccess GetScopedAccess() { return ScopedAccess(this); }
std::unordered_map<zx_koid_t, Endpoint> exports_;
std::unordered_map<zx_koid_t, Endpoint> imports_;
private:
std::recursive_mutex mutex_;
fxl::WeakPtrFactory<ObjectLinkerBase> weak_ptr_factory_;
FXL_DISALLOW_COPY_AND_ASSIGN(ObjectLinkerBase);
};
// Allows direct linking of peer objects, regardless of which session(s) they
// exist in. Once the objects are linked, they have direct references to each
// other.
//
// This linking is accomplished via lookup between pairable kernel objects.
// zx::eventpair objects are a natural fit for this purpose and are commonly
// used.
//
// To create a Link, provide a handle to one half of a pairable kernel object
// (zx::object_traits:supports_duplication is true) to the |CreateExport| or
// |CreateImport| methods. It can be connected with its peer by providing a
// concrete object to link along with callbacks for both successful and
// unsuccessful resolution. Import and Export should be copy constructable as
// they might be relinked.
//
// When the other half of the kernel object is registered with the
// ObjectLinker, and Initialize() is called on the corresponding Link, the
// provided resolution callbacks in both Links will be fired. The callback
// associated with the Export will always fire first.
//
// If either Link endpoint is destroyed, this will cause the provided
// disconnection callback on its peer endpoint to be fired. If the peer
// endpoint has not been provided any callbacks yet via Initialize(), the
// disconnection callback will be fired later when Initialize() is first called
// on it.
//
// Attempts to register either half of the kernel object multiple times, even
// through cloned handles, will result in an error.
// TODO(https://fxbug.dev/42098134): Allow multiple Imports.
//
// Thread-safety: this class is thread-safe. Links may be created and used on multiple threads.
// Links must be destroyed before their dispatcher is destroyed (i.e. the default dispatcher when
// the Link is created).
template <typename Export, typename Import>
class ObjectLinker : public ObjectLinkerBase,
public std::enable_shared_from_this<ObjectLinker<Export, Import>> {
public:
// Represents one endpoint of a Link between two objects in different
// |Session|s.
//
// Links can be moved, but not copied. Valid Links can only be constructed by
// the CreateExport and CreateImport methods.
template <bool is_import>
class Link : public ObjectLinkerBase::Link {
public:
using Obj = typename std::conditional<is_import, Import, Export>::type;
using PeerObj = typename std::conditional<is_import, Export, Import>::type;
Link() = default;
virtual ~Link() {
auto access = GetLinkerScopedAccess();
Invalidate(/*on_destruction=*/true, /*invalidate_peer=*/true);
}
// TODO(https://fxbug.dev/42160907): Make this immovable type.
Link(Link&& other) noexcept {
FX_CHECK(!linker_ || !other.linker_ || linker_ == other.linker_);
auto access = GetLinkerScopedAccess();
auto other_access = other.GetLinkerScopedAccess();
*this = std::move(other);
}
Link& operator=(nullptr_t) {
auto access = GetLinkerScopedAccess();
Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
}
Link& operator=(Link&& other);
bool valid() {
auto access = GetLinkerScopedAccess();
return endpoint_id_ != ZX_KOID_INVALID;
}
bool initialized() {
auto access = GetLinkerScopedAccess();
return valid() && link_resolved_;
}
zx_koid_t endpoint_id() {
auto access = GetLinkerScopedAccess();
return endpoint_id_;
}
// Initialize the Link with an |object| and callbacks for |link_resolved|
// and |link_invalidated| events, making it ready for connection to its peer.
// The |link_invalidated| event is guaranteed to be called regardless of
// whether or not the |link_resolved| callback is, including if this Link
// is destroyed, in which case |on_destruction| will be true.
void Initialize(std::function<void(PeerObj peer_object)> link_resolved = nullptr,
std::function<void(bool on_destruction)> link_invalidated = nullptr);
// Releases the zx::handle for this link, allowing the caller to establish a new link with it.
//
// This operation works regardless of whether or not the link has resolved. If the link was
// resolved, the peer receives a |link_invalidated| callback and is put back in the
// initialized-but-unresolved state.
std::optional<zx::handle> ReleaseToken();
private:
// Kept private so only an ObjectLinker can construct a valid Link.
Link(Obj object, zx_koid_t endpoint_id, std::shared_ptr<ObjectLinker> linker)
: object_(std::move(object)), endpoint_id_(endpoint_id), linker_(std::move(linker)) {}
void LinkResolved(ObjectLinkerBase::Link* peer_link) override;
void Invalidate(bool on_destruction, bool invalidate_peer) override;
// Enforces serialized access to ObjectLinker and Links created from it. Each method should
// acquire this before execution. This is needed because one thread might be modifying
// internals of Link while another one accesses the internals.
ScopedAccess GetLinkerScopedAccess() {
return linker_ ? linker_->GetScopedAccess() : ScopedAccess();
}
std::optional<Obj> object_;
zx_koid_t endpoint_id_ = ZX_KOID_INVALID;
std::shared_ptr<ObjectLinker> linker_;
std::function<void(PeerObj peer_link)> link_resolved_;
friend class ObjectLinker;
friend class ObjectLinker::Link<!is_import>;
};
using ExportLink = Link<false>;
using ImportLink = Link<true>;
static std::shared_ptr<ObjectLinker> New() {
return std::shared_ptr<ObjectLinker>(new ObjectLinker<Export, Import>());
}
~ObjectLinker() = default;
// Creates an outgoing cross-session ExportLink between two objects,
// which can be used to initiate and close the connection between them.
//
// The ObjectLinker uses the provided |token| to locate the paired ImportLink.
// |token| must reference a pairable kernel object type such as |zx::channel| or |zx::eventpair|.
// |token| may not reference a kernel object that is in use by this ObjectLinker.
//
// If a link cannot be created, |error_reporter| will be used to flag an error.
//
// The objects are linked as soon as the |Initialize()| method is called on
// the links for both objects.
template <typename T, typename = std::enable_if_t<zx::object_traits<T>::has_peer_handle>>
ExportLink CreateExport(Export export_obj, T token, ErrorReporter* error_reporter) {
static_assert(std::is_copy_constructible<Export>::value);
auto access = GetScopedAccess();
const zx_koid_t endpoint_id = CreateEndpoint(std::move(token), error_reporter, false);
return ExportLink(std::move(export_obj), endpoint_id, this->shared_from_this());
}
// Creates an incoming cross-session ImportLink between two objects,
// which can be used to initiate and close the connection between them.
//
// The ObjectLinker uses the provided |token| to locate the paired ExportLink.
// |token| must reference a pairable kernel object type such as |zx::channel| or |zx::eventpair|.
// |token| may not reference a kernel object that is in use by this ObjectLinker.
//
// If a link cannot be created, |error_reporter| will be used to flag an error.
//
// The objects are linked as soon as the |Initialize()| method is called on
// the links for both objects.
template <typename T, typename = std::enable_if_t<zx::object_traits<T>::has_peer_handle>>
ImportLink CreateImport(Import import_obj, T token, ErrorReporter* error_reporter) {
static_assert(std::is_copy_constructible<Import>::value);
auto access = GetScopedAccess();
const zx_koid_t endpoint_id = CreateEndpoint(std::move(token), error_reporter, true);
return ImportLink(std::move(import_obj), endpoint_id, this->shared_from_this());
}
private:
ObjectLinker() = default;
};
template <typename Export, typename Import>
template <bool is_import>
auto ObjectLinker<Export, Import>::Link<is_import>::operator=(Link&& other) -> Link& {
FX_CHECK(!linker_ || !other.linker_ || linker_ == other.linker_);
auto access = GetLinkerScopedAccess();
auto other_access = other.GetLinkerScopedAccess();
// Invalidate the existing Link if its still valid.
Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
// Move data from the other Link and manually invalidate it, so it won't destroy
// its endpoint when it dies.
link_resolved_ = std::move(other.link_resolved_);
link_invalidated_ = std::move(other.link_invalidated_);
object_ = std::move(other.object_);
linker_ = std::move(other.linker_);
endpoint_id_ = std::move(other.endpoint_id_);
other.endpoint_id_ = ZX_KOID_INVALID;
if (initialized()) {
linker_->InitializeEndpoint(this, endpoint_id_, is_import);
}
return *this;
}
template <typename Export, typename Import>
template <bool is_import>
void ObjectLinker<Export, Import>::Link<is_import>::Initialize(
std::function<void(PeerObj peer_object)> link_resolved,
std::function<void(bool on_destruction)> link_invalidated) {
auto access = GetLinkerScopedAccess();
FX_DCHECK(valid());
FX_DCHECK(!initialized());
FX_DCHECK(link_resolved);
link_resolved_ = std::move(link_resolved);
link_invalidated_ = std::move(link_invalidated);
linker_->InitializeEndpoint(this, endpoint_id_, is_import);
}
template <typename Export, typename Import>
template <bool is_import>
std::optional<zx::handle> ObjectLinker<Export, Import>::Link<is_import>::ReleaseToken() {
auto access = GetLinkerScopedAccess();
if (!valid()) {
return std::optional<zx::handle>();
}
zx::handle token = linker_->ReleaseToken(endpoint_id_, is_import);
Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/false);
return std::move(token);
}
template <typename Export, typename Import>
template <bool is_import>
void ObjectLinker<Export, Import>::Link<is_import>::Invalidate(bool on_destruction,
bool invalidate_peer) {
auto access = GetLinkerScopedAccess();
if (valid()) {
linker_->DestroyEndpoint(endpoint_id_, is_import, invalidate_peer);
}
object_.reset();
link_resolved_ = nullptr;
endpoint_id_ = ZX_KOID_INVALID;
LinkInvalidatedLocked(on_destruction);
}
template <typename Export, typename Import>
template <bool is_import>
void ObjectLinker<Export, Import>::Link<is_import>::LinkResolved(
ObjectLinkerBase::Link* peer_link) {
auto access = GetLinkerScopedAccess();
if (link_resolved_) {
auto* typed_peer_link = static_cast<Link<!is_import>*>(peer_link);
FX_DCHECK(typed_peer_link->object_.has_value());
link_resolved_(typed_peer_link->object_.value());
}
}
} // namespace utils
#endif // SRC_UI_SCENIC_LIB_UTILS_OBJECT_LINKER_H_