Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7b20ab16 authored by Atneya Nair's avatar Atneya Nair Committed by Android (Google) Code Review
Browse files

Merge "Add move only type erased function" into main

parents 37d9acdc 9dee501c
Loading
Loading
Loading
Loading
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * 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.
 */

#pragma once

#include <cstddef>
#include <future>
#include <memory>

namespace android::mediautils {

// Essentially std::function <void()>, but supports moveable types (and binds to any return type).
// The lack of moveable is fixed in C++23, but we don't yet have it.
// Also, SBO for std::packaged_task size, which is what we are using this for
class Runnable {
  private:
    // src == nullptr => destroy the dest, otherwise move from src storage to dst, destroying src
    using move_destroy_fptr_t = void (*)(std::byte* dest, std::byte* src) noexcept;
    using call_fptr_t = void (*)(std::byte* storage);

    struct VTable {
        move_destroy_fptr_t move_destroy;
        call_fptr_t invoke;
    };

    static void empty_move_destroy(std::byte*, std::byte*) noexcept {}
    static constexpr VTable empty_vtable{.move_destroy = empty_move_destroy, .invoke = nullptr};

    template <typename T>
    static T& transmogrify(std::byte* addr) {
        return *std::launder(reinterpret_cast<T*>(addr));
    }

    template <typename T>
    static void move_destroy_impl(std::byte* dest, std::byte* src) noexcept {
        if (src) {
            std::construct_at(&transmogrify<T>(dest), std::move(transmogrify<T>(src)));
            transmogrify<T>(src).~T();
        } else {
            transmogrify<T>(dest).~T();
        }
    }

    template <typename T>
    static void call_impl(std::byte* addr) {
        std::invoke(transmogrify<T>(addr));
    }

  public:
    static constexpr size_t STORAGE_SIZE = sizeof(std::packaged_task<int()>);

    Runnable() = default;

    Runnable(std::nullptr_t) {}

    Runnable(const Runnable& o) = delete;

    Runnable(Runnable&& o) noexcept {
        // ask other vtable to move their storage into ours
        o.v.move_destroy(storage_, o.storage_);
        std::swap(v, o.v);
    }

    template <typename F>
        requires(std::is_invocable_v<std::decay_t<F>> &&
                 !std::is_same_v<std::decay_t<F>, Runnable> &&
                 std::is_move_constructible_v<std::decay_t<F>> &&
                 sizeof(std::decay_t<F>) <= STORAGE_SIZE)
    explicit Runnable(F&& task)
        : v{move_destroy_impl<std::decay_t<F>>, call_impl<std::decay_t<F>>} {
        std::construct_at(&transmogrify<std::decay_t<F>>(storage_), std::forward<F>(task));
    }

    Runnable& operator=(const Runnable& o) = delete;

    Runnable& operator=(Runnable&& o) {
        // destroy ourselves
        v.move_destroy(storage_, nullptr);
        v = empty_vtable;
        // ask other vtable to move their storage into ours
        o.v.move_destroy(storage_, o.storage_);
        std::swap(v, o.v);
        return *this;
    }

    ~Runnable() { v.move_destroy(storage_, nullptr); }

    operator bool() const { return v.invoke != nullptr; }

    void operator()() {
        if (*this) v.invoke(storage_);
    }

  private:
    VTable v = empty_vtable;
    alignas(alignof(std::max_align_t)) std::byte storage_[STORAGE_SIZE];
};
}  // namespace android::mediautils
+1 −0
Original line number Diff line number Diff line
@@ -285,5 +285,6 @@ cc_test {
    defaults: ["libmediautils_tests_defaults"],
    srcs: [
        "jthread_tests.cpp",
        "runnable_tests.cpp",
    ],
}
+139 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * 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.
 */

#define LOG_TAG "runnable_tests"

#include <mediautils/Runnable.h>

#include <gtest/gtest.h>

using namespace android::mediautils;

struct Func {
    inline static int sMoveCtor = 0;
    inline static int sDtor = 0;
    // accumulator for call operator of this object
    inline static int sSum = 0;
    static constexpr int VAL1 = 7;
    static constexpr int VAL2 = 4;

    Func(int v) : value(v) {}
    Func(const Func&) = delete;
    Func(Func&& other) : value(other.value) { sMoveCtor++; }
    Func& operator=(const Func&) = delete;
    ~Func() { sDtor++; }

    void operator()() { sSum += value; }

  private:
    const int value;
};

class RunnableTests : public ::testing::Test {
  protected:
    void SetUp() override {
        Func::sMoveCtor = 0;
        Func::sDtor = 0;
        Func::sSum = 0;
    }
};

TEST_F(RunnableTests, testEmpty) {
    Runnable r1{};
    Runnable r2{nullptr};
    // empty func should do nothing, instead of crash
    r1();
    r2();
    EXPECT_FALSE(r1);
    EXPECT_FALSE(r2);
}

static int foo() {
    return 5;
}

struct Copy {
    Copy() {}
    Copy(const Copy&) {}
    Copy(Copy&&) {}
    void operator()(){}
};

TEST_F(RunnableTests, testCompile) {
    const Copy b{};
    Runnable r1{std::move(b)};
    Runnable r2{b};
    Runnable r4{foo};
    std::unique_ptr<int> ptr;
    auto move_only = [ptr = std::move(ptr)](){};
    Runnable r5{std::move(move_only)};
    auto copyable = [](){};
    Runnable r6{copyable};
}

TEST_F(RunnableTests, testBool) {
    Runnable r1{[]() {}};
    EXPECT_TRUE(r1);
}

TEST_F(RunnableTests, testCall) {
    Runnable r1{Func{Func::VAL1}};
    EXPECT_TRUE(r1);
    r1();
    EXPECT_EQ(Func::sSum, Func::VAL1);
}

TEST_F(RunnableTests, testDtor) {
    {
        Runnable r1{Func{Func::VAL1}};
    }
    EXPECT_EQ(Func::sDtor, 2);
}

TEST_F(RunnableTests, testMoveCtor) {
    {
        Runnable moved_from{Func{Func::VAL1}};
        EXPECT_EQ(Func::sMoveCtor, 1);
        EXPECT_EQ(Func::sDtor, 1);
        Runnable r1{std::move(moved_from)};
        EXPECT_EQ(Func::sDtor, 2);  // impl detail that we destroy internal obj after move
        EXPECT_EQ(Func::sMoveCtor, 2);
        EXPECT_TRUE(r1);
        EXPECT_FALSE(moved_from);
        r1();
        EXPECT_EQ(Func::sSum, Func::VAL1);
    }
    EXPECT_EQ(Func::sDtor, 3);
}

TEST_F(RunnableTests, testMoveAssign) {
    {
        Runnable r1{Func{Func::VAL2}};
        Runnable moved_from{Func{Func::VAL1}};
        EXPECT_EQ(Func::sMoveCtor, 2);
        EXPECT_EQ(Func::sDtor, 2);
        r1();
        EXPECT_EQ(Func::sSum, 4);
        r1 = std::move(moved_from);
        EXPECT_EQ(Func::sDtor, 4);  // impl detail that we destroy internal obj after move
        EXPECT_EQ(Func::sMoveCtor, 3);
        EXPECT_TRUE(r1);
        EXPECT_FALSE(moved_from);
        r1();  // value should now hold Func::VAL1
        EXPECT_EQ(Func::sSum, Func::VAL2 + Func::VAL1);
    }
    EXPECT_EQ(Func::sDtor, 5);
}