diff --git a/Ladybird/Android/src/main/cpp/ALooperEventLoopImplementation.cpp b/Ladybird/Android/src/main/cpp/ALooperEventLoopImplementation.cpp new file mode 100644 index 0000000000..58b2421b9b --- /dev/null +++ b/Ladybird/Android/src/main/cpp/ALooperEventLoopImplementation.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ALooperEventLoopImplementation.h" +#include "JNIHelpers.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Ladybird { + +EventLoopThreadData& EventLoopThreadData::the() +{ + static thread_local EventLoopThreadData s_thread_data; + return s_thread_data; +} + +static ALooperEventLoopImplementation& current_impl() +{ + return verify_cast(Core::EventLoop::current().impl()); +} + +ALooperEventLoopManager::ALooperEventLoopManager(JavaVM* vm, jobject timer_service) + : m_vm(vm) + , m_timer_service(timer_service) +{ + JavaEnvironment env(m_vm); + + jclass timer_class = env.get()->FindClass("org/serenityos/ladybird/TimerExecutorService$Timer"); + if (!timer_class) + TODO(); + m_timer_class = reinterpret_cast(env.get()->NewGlobalRef(timer_class)); + env.get()->DeleteLocalRef(timer_class); + + m_timer_constructor = env.get()->GetMethodID(m_timer_class, "", "(J)V"); + if (!m_timer_constructor) + TODO(); + + jclass timer_service_class = env.get()->GetObjectClass(m_timer_service); + + m_register_timer = env.get()->GetMethodID(timer_service_class, "registerTimer", "(Lorg/serenityos/ladybird/TimerExecutorService$Timer;ZJ)J"); + if (!m_register_timer) + TODO(); + + m_unregister_timer = env.get()->GetMethodID(timer_service_class, "unregisterTimer", "(J)Z"); + if (!m_unregister_timer) + TODO(); + env.get()->DeleteLocalRef(timer_service_class); +} + +ALooperEventLoopManager::~ALooperEventLoopManager() +{ + JavaEnvironment env(m_vm); + + env.get()->DeleteGlobalRef(m_timer_service); + env.get()->DeleteGlobalRef(m_timer_class); +} + +NonnullOwnPtr ALooperEventLoopManager::make_implementation() +{ + return ALooperEventLoopImplementation::create(); +} + +int ALooperEventLoopManager::register_timer(Core::EventReceiver& receiver, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible visibility) +{ + JavaEnvironment env(m_vm); + auto& thread_data = EventLoopThreadData::the(); + + auto timer = env.get()->NewObject(m_timer_class, m_timer_constructor, reinterpret_cast(&thread_data)); + + long millis = milliseconds; + long timer_id = env.get()->CallLongMethod(m_timer_service, m_register_timer, timer, !should_reload, millis); + + // FIXME: Is there a race condition here? Maybe we should take a lock on the timers... + thread_data.timers.set(timer_id, { receiver.make_weak_ptr(), visibility }); + + return timer_id; +} + +bool ALooperEventLoopManager::unregister_timer(int timer_id) +{ + if (auto timer = EventLoopThreadData::the().timers.take(timer_id); timer.has_value()) { + JavaEnvironment env(m_vm); + return env.get()->CallBooleanMethod(m_timer_service, m_unregister_timer, timer_id); + } + return false; +} + +void ALooperEventLoopManager::register_notifier(Core::Notifier& notifier) +{ + EventLoopThreadData::the().notifiers.set(¬ifier); + current_impl().register_notifier(notifier); +} + +void ALooperEventLoopManager::unregister_notifier(Core::Notifier& notifier) +{ + EventLoopThreadData::the().notifiers.remove(¬ifier); + current_impl().unregister_notifier(notifier); +} + +void ALooperEventLoopManager::did_post_event() +{ + current_impl().poke(); +} + +ALooperEventLoopImplementation::ALooperEventLoopImplementation() + : m_event_loop(ALooper_prepare(0)) +{ + auto ret = pipe2(m_pipe, O_CLOEXEC | O_NONBLOCK); + VERIFY(ret == 0); + + ALooper_acquire(m_event_loop); + + ret = ALooper_addFd(m_event_loop, m_pipe[0], ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, &ALooperEventLoopImplementation::looper_callback, this); + VERIFY(ret == 1); +} + +ALooperEventLoopImplementation::~ALooperEventLoopImplementation() +{ + ALooper_removeFd(m_event_loop, m_pipe[0]); + ALooper_release(m_event_loop); + + ::close(m_pipe[0]); + ::close(m_pipe[1]); +} + +int ALooperEventLoopImplementation::exec() +{ + while (!m_exit_requested.load(MemoryOrder::memory_order_acquire)) + pump(PumpMode::WaitForEvents); + return m_exit_code; +} + +size_t ALooperEventLoopImplementation::pump(Core::EventLoopImplementation::PumpMode mode) +{ + auto num_events = Core::ThreadEventQueue::current().process(); + + int timeout_ms = mode == Core::EventLoopImplementation::PumpMode::WaitForEvents ? -1 : 0; + auto ret = ALooper_pollAll(timeout_ms, nullptr, nullptr, nullptr); + + // We don't expect any non-callback FDs to be ready + VERIFY(ret <= 0); + + if (ret == ALOOPER_POLL_ERROR) + m_exit_requested.store(true, MemoryOrder::memory_order_release); + + num_events += Core::ThreadEventQueue::current().process(); + return num_events; +} + +void ALooperEventLoopImplementation::quit(int code) +{ + m_exit_code = code; + m_exit_requested.store(true, MemoryOrder::memory_order_release); + wake(); +} + +void ALooperEventLoopImplementation::wake() +{ + ALooper_wake(m_event_loop); +} + +void ALooperEventLoopImplementation::post_event(Core::EventReceiver& receiver, NonnullOwnPtr&& event) +{ + m_thread_event_queue.post_event(receiver, move(event)); + + if (&m_thread_event_queue != &Core::ThreadEventQueue::current()) + wake(); +} + +int ALooperEventLoopImplementation::looper_callback(int fd, int events, void* data) +{ + auto& impl = *static_cast(data); + (void)impl; // FIXME: Do we need to do anything with the instance here? + + if (events & ALOOPER_EVENT_INPUT) { + int msg = 0; + while (read(fd, &msg, sizeof(msg)) == sizeof(msg)) { + // Do nothing, we don't actually care what the message was, just that it was posted + } + } + return 1; +} + +void ALooperEventLoopImplementation::poke() +{ + int msg = 0xCAFEBABE; + (void)write(m_pipe[1], &msg, sizeof(msg)); +} + +static int notifier_callback(int fd, int, void* data) +{ + auto& notifier = *static_cast(data); + + VERIFY(fd == notifier.fd()); + + Core::NotifierActivationEvent event(notifier.fd()); + notifier.dispatch_event(event); + + // Wake up from ALooper_pollAll, and service this event on the event queue + current_impl().poke(); + + return 1; +} + +void ALooperEventLoopImplementation::register_notifier(Core::Notifier& notifier) +{ + auto event_flags = 0; + switch (notifier.type()) { + case Core::Notifier::Type::Read: + event_flags = ALOOPER_EVENT_INPUT; + break; + case Core::Notifier::Type::Write: + event_flags = ALOOPER_EVENT_OUTPUT; + break; + case Core::Notifier::Type::Exceptional: + case Core::Notifier::Type::None: + TODO(); + } + + auto ret = ALooper_addFd(m_event_loop, notifier.fd(), ALOOPER_POLL_CALLBACK, event_flags, ¬ifier_callback, ¬ifier); + VERIFY(ret == 1); +} + +void ALooperEventLoopImplementation::unregister_notifier(Core::Notifier& notifier) +{ + ALooper_removeFd(m_event_loop, notifier.fd()); +} + +} diff --git a/Ladybird/Android/src/main/cpp/ALooperEventLoopImplementation.h b/Ladybird/Android/src/main/cpp/ALooperEventLoopImplementation.h new file mode 100644 index 0000000000..6920ee5e47 --- /dev/null +++ b/Ladybird/Android/src/main/cpp/ALooperEventLoopImplementation.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +extern "C" struct ALooper; + +namespace Ladybird { + +class ALooperEventLoopManager : public Core::EventLoopManager { +public: + ALooperEventLoopManager(JavaVM*, jobject timer_service); + virtual ~ALooperEventLoopManager() override; + virtual NonnullOwnPtr make_implementation() override; + + virtual int register_timer(Core::EventReceiver&, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override; + virtual bool unregister_timer(int timer_id) override; + + virtual void register_notifier(Core::Notifier&) override; + virtual void unregister_notifier(Core::Notifier&) override; + + virtual void did_post_event() override; + + // FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them. + virtual int register_signal(int, Function) override { return 0; } + virtual void unregister_signal(int) override { } + +private: + JavaVM* m_vm { nullptr }; + jobject m_timer_service { nullptr }; + jmethodID m_register_timer { nullptr }; + jmethodID m_unregister_timer { nullptr }; + jclass m_timer_class { nullptr }; + jmethodID m_timer_constructor { nullptr }; +}; + +class ALooperEventLoopImplementation : public Core::EventLoopImplementation { +public: + static NonnullOwnPtr create() { return adopt_own(*new ALooperEventLoopImplementation); } + + virtual ~ALooperEventLoopImplementation() override; + + virtual int exec() override; + virtual size_t pump(PumpMode) override; + virtual void quit(int) override; + virtual void wake() override; + virtual void post_event(Core::EventReceiver& receiver, NonnullOwnPtr&&) override; + + // FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them. + virtual void unquit() override { } + virtual bool was_exit_requested() const override { return false; } + virtual void notify_forked_and_in_child() override { } + + void poke(); + +private: + friend class ALooperEventLoopManager; + + ALooperEventLoopImplementation(); + + static int looper_callback(int fd, int events, void* data); + + void register_notifier(Core::Notifier&); + void unregister_notifier(Core::Notifier&); + + ALooper* m_event_loop { nullptr }; + int m_pipe[2] {}; + int m_exit_code { 0 }; + Atomic m_exit_requested { false }; +}; + +struct TimerData { + WeakPtr receiver; + Core::TimerShouldFireWhenNotVisible visibility; +}; + +struct EventLoopThreadData { + static EventLoopThreadData& the(); + + HashMap timers; + HashTable notifiers; +}; + +} diff --git a/Ladybird/Android/src/main/cpp/JNIHelpers.h b/Ladybird/Android/src/main/cpp/JNIHelpers.h new file mode 100644 index 0000000000..ae7dc455d0 --- /dev/null +++ b/Ladybird/Android/src/main/cpp/JNIHelpers.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +class JavaEnvironment { +public: + JavaEnvironment(JavaVM* vm) + : m_vm(vm) + { + auto ret = m_vm->GetEnv(reinterpret_cast(&m_env), JNI_VERSION_1_6); + if (ret == JNI_EDETACHED) { + ret = m_vm->AttachCurrentThread(&m_env, nullptr); + VERIFY(ret == JNI_OK); + m_did_attach_thread = true; + } else if (ret == JNI_EVERSION) { + VERIFY_NOT_REACHED(); + } else { + VERIFY(ret == JNI_OK); + } + + VERIFY(m_env != nullptr); + } + + ~JavaEnvironment() + { + if (m_did_attach_thread) + m_vm->DetachCurrentThread(); + } + + JNIEnv* get() const { return m_env; } + +private: + JavaVM* m_vm = nullptr; + JNIEnv* m_env = nullptr; + bool m_did_attach_thread = false; +}; diff --git a/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp b/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp index 66b07d0a40..29722f75b0 100644 --- a/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp +++ b/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp @@ -4,15 +4,40 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include "ALooperEventLoopImplementation.h" +#include #include +#include +#include #include #include +OwnPtr s_main_event_loop; +RefPtr s_timer; + extern "C" JNIEXPORT void JNICALL -Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv* env, jobject /* thiz */, jstring resource_dir) +Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv* env, jobject /* thiz */, jstring resource_dir, jobject timer_service) { char const* raw_resource_dir = env->GetStringUTFChars(resource_dir, nullptr); s_serenity_resource_root = raw_resource_dir; __android_log_print(ANDROID_LOG_INFO, "Ladybird", "Serenity resource dir is %s", s_serenity_resource_root.characters()); env->ReleaseStringUTFChars(resource_dir, raw_resource_dir); + + jobject timer_service_ref = env->NewGlobalRef(timer_service); + JavaVM* vm = nullptr; + jint ret = env->GetJavaVM(&vm); + VERIFY(ret == 0); + Core::EventLoopManager::install(*new Ladybird::ALooperEventLoopManager(vm, timer_service_ref)); + s_main_event_loop = make(); + + s_timer = MUST(Core::Timer::create_repeating(1000, [] { + __android_log_print(ANDROID_LOG_DEBUG, "Ladybird", "EventLoop is alive!"); + })); + s_timer->start(); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_serenityos_ladybird_LadybirdActivity_execMainEventLoop(JNIEnv*, jobject /* thiz */) +{ + s_main_event_loop->pump(Core::EventLoop::WaitMode::PollForEvents); } diff --git a/Ladybird/Android/src/main/cpp/TimerExecutorService.cpp b/Ladybird/Android/src/main/cpp/TimerExecutorService.cpp new file mode 100644 index 0000000000..5ae7fb8e02 --- /dev/null +++ b/Ladybird/Android/src/main/cpp/TimerExecutorService.cpp @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "ALooperEventLoopImplementation.h" +#include +#include + +extern "C" JNIEXPORT void JNICALL Java_org_serenityos_ladybird_TimerExecutorService_00024Timer_nativeRun(JNIEnv*, jobject /* thiz */, jlong native_data, jlong id) +{ + auto& thread_data = *reinterpret_cast(native_data); + + if (auto timer_data = thread_data.timers.get(id); timer_data.has_value()) { + auto receiver = timer_data->receiver.strong_ref(); + if (!receiver) + return; + + if (timer_data->visibility == Core::TimerShouldFireWhenNotVisible::No) + if (!receiver->is_visible_for_timer_purposes()) + return; + + Core::TimerEvent event(id); + + // FIXME: Should the dispatch happen on the thread that registered the timer? + receiver->dispatch_event(event); + } +} diff --git a/Ladybird/Android/src/main/cpp/WebContentService.cpp b/Ladybird/Android/src/main/cpp/WebContentService.cpp index bcb3de600d..5e0e85e383 100644 --- a/Ladybird/Android/src/main/cpp/WebContentService.cpp +++ b/Ladybird/Android/src/main/cpp/WebContentService.cpp @@ -14,4 +14,7 @@ Java_org_serenityos_ladybird_WebContentService_nativeHandleTransferSockets(JNIEn __android_log_print(ANDROID_LOG_INFO, "WebContent", "New binding received, sockets %d and %d", ipc_socket, fd_passing_socket); ::close(ipc_socket); ::close(fd_passing_socket); + + // FIXME: Create a new thread to start WebContent processing + // Make sure to create IPC sockets *in that thread*! } diff --git a/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp b/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp index 740020a490..23c4fe7220 100644 --- a/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp +++ b/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include "JNIHelpers.h" #include #include #include @@ -24,39 +25,6 @@ Gfx::BitmapFormat to_gfx_bitmap_format(i32 f) } } -class JavaEnvironment { -public: - JavaEnvironment(JavaVM* vm) - : m_vm(vm) - { - auto ret = m_vm->GetEnv(reinterpret_cast(&m_env), JNI_VERSION_1_6); - if (ret == JNI_EDETACHED) { - ret = m_vm->AttachCurrentThread(&m_env, nullptr); - VERIFY(ret == JNI_OK); - m_did_attach_thread = true; - } else if (ret == JNI_EVERSION) { - VERIFY_NOT_REACHED(); - } else { - VERIFY(ret == JNI_OK); - } - - VERIFY(m_env != nullptr); - } - - ~JavaEnvironment() - { - if (m_did_attach_thread) - m_vm->DetachCurrentThread(); - } - - JNIEnv* get() const { return m_env; } - -private: - JavaVM* m_vm = nullptr; - JNIEnv* m_env = nullptr; - bool m_did_attach_thread = false; -}; - class WebViewImplementationNative : public WebView::ViewImplementation { public: WebViewImplementationNative(jobject thiz) diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt index 75ca1e00d2..79e287a712 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt @@ -8,7 +8,6 @@ package org.serenityos.ladybird import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import android.util.AttributeSet import org.serenityos.ladybird.databinding.ActivityMainBinding class LadybirdActivity : AppCompatActivity() { @@ -20,12 +19,16 @@ class LadybirdActivity : AppCompatActivity() { super.onCreate(savedInstanceState) resourceDir = TransferAssets.transferAssets(this) - initNativeCode(resourceDir) + initNativeCode(resourceDir, timerService) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) view = binding.webView + + mainExecutor.execute { + callNativeEventLoopForever() + } } override fun onDestroy() { @@ -34,12 +37,23 @@ class LadybirdActivity : AppCompatActivity() { } private lateinit var view: WebView + private var timerService = TimerExecutorService() /** * A native method that is implemented by the 'ladybird' native library, * which is packaged with this application. */ - private external fun initNativeCode(resourceDir: String) + private external fun initNativeCode(resourceDir: String, timerService: TimerExecutorService) + + // FIXME: Instead of doing this, can we push a message to the message queue of the java Looper + // when an event is pushed to the main thread, and use that to clear out the + // Core::ThreadEventQueues? + private fun callNativeEventLoopForever() { + execMainEventLoop() + mainExecutor.execute { callNativeEventLoopForever() } + } + + private external fun execMainEventLoop(); companion object { // Used to load the 'ladybird' library on application startup. diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/TimerExecutorService.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/TimerExecutorService.kt new file mode 100644 index 0000000000..a7ca12fdb1 --- /dev/null +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/TimerExecutorService.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +package org.serenityos.ladybird + +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class TimerExecutorService { + + private val executor = Executors.newSingleThreadScheduledExecutor() + + class Timer(private var nativeData: Long) : Runnable { + override fun run() { + nativeRun(nativeData, id) + } + + private external fun nativeRun(nativeData: Long, id: Long) + var id: Long = 0 + } + + fun registerTimer(timer: Timer, singleShot: Boolean, milliseconds: Long): Long { + val id = ++nextId + timer.id = id + val handle: ScheduledFuture<*> = if (singleShot) executor.schedule( + timer, + milliseconds, + TimeUnit.MILLISECONDS + ) else executor.scheduleAtFixedRate( + timer, + milliseconds, + milliseconds, + TimeUnit.MILLISECONDS + ) + timers[id] = handle + return id + } + + fun unregisterTimer(id: Long): Boolean { + val timer = timers[id] ?: return false + return timer.cancel(false) + } + + private var nextId: Long = 0 + private val timers: HashMap> = hashMapOf() + +} diff --git a/Ladybird/CMakeLists.txt b/Ladybird/CMakeLists.txt index 2ab90dfb68..840e1abb3d 100644 --- a/Ladybird/CMakeLists.txt +++ b/Ladybird/CMakeLists.txt @@ -148,8 +148,10 @@ elseif(ANDROID) ${SOURCES} Android/src/main/cpp/LadybirdActivity.cpp Android/src/main/cpp/WebViewImplementationNative.cpp + Android/src/main/cpp/ALooperEventLoopImplementation.cpp + Android/src/main/cpp/TimerExecutorService.cpp ) - target_link_libraries(ladybird PRIVATE log jnigraphics) + target_link_libraries(ladybird PRIVATE log jnigraphics android) else() # TODO: Check for other GUI frameworks here when we move them in-tree # For now, we can export a static library of common files for chromes to link to diff --git a/Ladybird/WebContent/CMakeLists.txt b/Ladybird/WebContent/CMakeLists.txt index 5654bdc5f4..d27a78c444 100644 --- a/Ladybird/WebContent/CMakeLists.txt +++ b/Ladybird/WebContent/CMakeLists.txt @@ -53,8 +53,12 @@ else() ) if (ANDROID) - target_sources(webcontent PRIVATE ../Android/src/main/cpp/WebContentService.cpp) - target_link_libraries(webcontent PRIVATE log) + target_sources(webcontent PRIVATE + ../Android/src/main/cpp/WebContentService.cpp + ../Android/src/main/cpp/ALooperEventLoopImplementation.cpp + ../Android/src/main/cpp/TimerExecutorService.cpp + ) + target_link_libraries(webcontent PRIVATE log android) endif() add_executable(WebContent main.cpp)