| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| #ifndef RUNTIME_VM_FFI_CALLBACK_METADATA_H_ |
| #define RUNTIME_VM_FFI_CALLBACK_METADATA_H_ |
| |
| #include "platform/growable_array.h" |
| #include "platform/utils.h" |
| #include "vm/hash_map.h" |
| #include "vm/lockers.h" |
| #include "vm/virtual_memory.h" |
| |
| namespace dart { |
| |
| class ApiState; |
| class Closure; |
| class Function; |
| class Isolate; |
| class PersistentHandle; |
| |
| // Stores metadata related to FFI callbacks (Dart functions that are assigned a |
| // function pointer that can be invoked by native code). This is essentially a |
| // map from trampoline pointer to Metadata, with some logic to assign and memory |
| // manage those trampolines. |
| // |
| // In the past, callbacks were primarily identified by an integer ID, but in |
| // this class we identify them by their trampoline pointer to solve a very |
| // specific issue. The trampolines are allocated in pages. On iOS in AOT mode, |
| // we can't create new executable memory, but we can duplicate existing memory. |
| // When we were using numeric IDs to identify the trampolines, each trampoline |
| // page was different, because the IDs were embedded in the machine code. So we |
| // couldn't use trampolines in AOT mode. But if we key the metadata table by the |
| // trampoline pointer, then the trampoline just has to look up the PC at the |
| // start of the trampoline function, so the machine code will always be the |
| // same. This means we can just duplicate the trampoline page, allowing us to |
| // unify the FFI callback implementation across JIT and AOT, even on iOS. |
| class FfiCallbackMetadata { |
| public: |
| class Metadata; |
| class MetadataEntry; |
| |
| // The address of the allocated trampoline. |
| using Trampoline = uword; |
| |
| enum class TrampolineType : uint8_t { |
| kSync = 0, |
| kSyncStackDelta4 = 1, // Only used by TARGET_ARCH_IA32 |
| kAsync = 2, |
| kSyncIsolateGroupBound = 3, |
| kSyncIsolateGroupBoundStackDelta4 = 4, // Only used by TARGET_ARCH_IA32 |
| }; |
| |
| enum RuntimeFunctions { |
| kGetFfiCallbackMetadata, |
| kExitTemporaryIsolate, |
| kExitIsolateGroupBoundIsolate, |
| kNumRuntimeFunctions, |
| }; |
| |
| static void Init(); |
| static void Cleanup(); |
| |
| // Returns the FfiCallbackMetadata singleton. |
| static FfiCallbackMetadata* Instance(); |
| |
| // Creates an async callback trampoline for the given function and associates |
| // it with the send_port. |
| Trampoline CreateAsyncFfiCallback(Isolate* isolate, |
| Zone* zone, |
| const Function& function, |
| Dart_Port send_port, |
| MetadataEntry** list_head); |
| |
| // Creates an isolate- or isolategroup- local callback trampoline for |
| // the given function. |
| Trampoline CreateLocalFfiCallback(Isolate* isolate, |
| IsolateGroup* isolate_group, |
| Zone* zone, |
| const Function& function, |
| const Closure& closure, |
| MetadataEntry** list_head); |
| |
| // Deletes a single trampoline. |
| void DeleteCallback(Trampoline trampoline, MetadataEntry** list_head); |
| |
| // Deletes all the trampolines in the list. |
| void DeleteAllCallbacks(MetadataEntry** list_head); |
| |
| // FFI callback metadata for any sync or async trampoline. |
| class Metadata { |
| union { |
| Isolate* target_isolate_; |
| IsolateGroup* target_isolate_group_; |
| }; |
| TrampolineType trampoline_type_; |
| |
| // Note: This is a pointer into an an Instructions object. This is only |
| // safe because Instructions objects are never moved by the GC. |
| uword target_entry_point_; |
| |
| // For async callbacks, this is the send port. For sync callbacks this |
| // is a persistent handle to the callback's closure, or null. |
| uint64_t context_; |
| |
| Metadata(Isolate* target_isolate, |
| TrampolineType trampoline_type, |
| uword target_entry_point, |
| uint64_t context) |
| : target_isolate_(target_isolate), |
| trampoline_type_(trampoline_type), |
| target_entry_point_(target_entry_point), |
| context_(context) {} |
| |
| Metadata(IsolateGroup* target_isolate_group, |
| TrampolineType trampoline_type, |
| uword target_entry_point, |
| uint64_t context) |
| : target_isolate_group_(target_isolate_group), |
| trampoline_type_(trampoline_type), |
| target_entry_point_(target_entry_point), |
| context_(context) {} |
| |
| public: |
| friend class FfiCallbackMetadata; |
| bool IsSameCallback(const Metadata& other) const { |
| // Not checking the list links, because they can change when other |
| // callbacks are deleted. |
| return target_isolate_ == other.target_isolate_ && |
| trampoline_type_ == other.trampoline_type_ && |
| target_entry_point_ == other.target_entry_point_ && |
| context_ == other.context_; |
| } |
| |
| // Whether the callback is still alive. |
| bool IsLive() const { |
| return target_isolate_ != 0 || target_isolate_group_ != 0; |
| } |
| |
| // The target isolate. The isolate that owns the callback. Sync callbacks |
| // must be invoked on this isolate. Async callbacks will send a message to |
| // this isolate. |
| Isolate* target_isolate() const { |
| ASSERT(IsLive()); |
| return target_isolate_; |
| } |
| |
| IsolateGroup* target_isolate_group() const { |
| ASSERT(IsLive()); |
| return target_isolate_group_; |
| } |
| |
| // The Dart entrypoint for the callback, which the trampoline invokes. |
| uword target_entry_point() const { |
| ASSERT(IsLive()); |
| return target_entry_point_; |
| } |
| |
| // The persistent handle to the closure that the NativeCallable.isolateLocal |
| // is wrapping. |
| PersistentHandle* closure_handle() const { |
| ASSERT(IsLive()); |
| ASSERT(trampoline_type_ == TrampolineType::kSync || |
| trampoline_type_ == TrampolineType::kSyncStackDelta4 || |
| trampoline_type_ == TrampolineType::kSyncIsolateGroupBound || |
| trampoline_type_ == |
| TrampolineType::kSyncIsolateGroupBoundStackDelta4); |
| return reinterpret_cast<PersistentHandle*>(context_); |
| } |
| |
| bool is_isolate_group_bound() const { |
| return trampoline_type_ == TrampolineType::kSyncIsolateGroupBound || |
| trampoline_type_ == |
| TrampolineType::kSyncIsolateGroupBoundStackDelta4; |
| } |
| // ApiState associated with an isolate group associated with this metadata. |
| ApiState* api_state() const; |
| |
| // For async callbacks, this is the send port. For sync callbacks this is a |
| // persistent handle to the callback's closure, or null. |
| uint64_t context() const { |
| ASSERT(IsLive()); |
| return context_; |
| } |
| |
| // The send port that the async callback will send a message to. |
| Dart_Port send_port() const { |
| ASSERT(IsLive()); |
| ASSERT(trampoline_type_ == TrampolineType::kAsync); |
| return static_cast<Dart_Port>(context_); |
| } |
| |
| // Tells FfiCallbackTrampolineStub how to call into the entry point. Mostly |
| // it's just a flag for whether this is a sync or async callback, but on |
| // IA32 it also encodes whether there's a stack delta of 4 to deal with. |
| TrampolineType trampoline_type() const { return trampoline_type_; } |
| }; |
| |
| // Metadata linked into a double-linked list. |
| class MetadataEntry { |
| Metadata metadata_; |
| |
| union { |
| // IsLive() |
| struct { |
| // Links in the Isolate's list of callbacks. |
| MetadataEntry* list_prev_; |
| MetadataEntry* list_next_; |
| }; |
| |
| // !IsLive() |
| MetadataEntry* free_list_next_; |
| }; |
| |
| public: |
| friend class Metadata; |
| friend class FfiCallbackMetadata; |
| MetadataEntry(Isolate* target_isolate, |
| TrampolineType trampoline_type, |
| uword target_entry_point, |
| uint64_t context, |
| MetadataEntry* list_prev, |
| MetadataEntry* list_next) |
| : metadata_(target_isolate, |
| trampoline_type, |
| target_entry_point, |
| context), |
| list_prev_(list_prev), |
| list_next_(list_next) {} |
| |
| MetadataEntry(IsolateGroup* target_isolate_group, |
| TrampolineType trampoline_type, |
| uword target_entry_point, |
| uint64_t context, |
| MetadataEntry* list_prev, |
| MetadataEntry* list_next) |
| : metadata_(target_isolate_group, |
| trampoline_type, |
| target_entry_point, |
| context), |
| list_prev_(list_prev), |
| list_next_(list_next) {} |
| |
| // To efficiently delete all the callbacks for a isolate, they are stored in |
| // a linked list. Since we also need to delete async callbacks at arbitrary |
| // times, the list must be doubly linked. |
| MetadataEntry* list_prev() { |
| ASSERT(metadata_.IsLive()); |
| return list_prev_; |
| } |
| MetadataEntry* list_next() { |
| ASSERT(metadata_.IsLive()); |
| return list_next_; |
| } |
| |
| Metadata* metadata() { return &metadata_; } |
| }; |
| |
| // Returns the Metadata object for the given trampoline. |
| Metadata LookupMetadataForTrampoline(Trampoline trampoline) const; |
| |
| // The mutex that guards creation and destruction of callbacks. |
| Mutex* lock() { return &lock_; } |
| |
| // The number of trampolines that can be stored on a single page. |
| static constexpr intptr_t NumCallbackTrampolinesPerPage() { |
| return (kPageSize - kNativeCallbackSharedStubSize) / |
| kNativeCallbackTrampolineSize; |
| } |
| |
| // Size of the trampoline page. Ideally we'd use VirtualMemory::PageSize(), |
| // but that varies across machines, and we need it to be consistent between |
| // host and target since it affects stub code generation. So kPageSize may be |
| // an overestimate of the target's VirtualMemory::PageSize(), but we try to |
| // get it as close as possible to avoid wasting memory. |
| #if defined(DART_TARGET_OS_LINUX) && defined(TARGET_ARCH_ARM64) |
| static constexpr intptr_t kPageSize = 64 * KB; |
| #elif defined(DART_TARGET_OS_ANDROID) && defined(TARGET_ARCH_IS_64_BIT) |
| static constexpr intptr_t kPageSize = 64 * KB; |
| #elif defined(DART_TARGET_OS_MACOS) && defined(TARGET_ARCH_ARM64) |
| static constexpr intptr_t kPageSize = 16 * KB; |
| #elif defined(DART_TARGET_OS_FUCHSIA) |
| // Fuchsia only gets one page, so make it big. |
| // TODO(https://dartbug.com/52579): Remove. |
| static constexpr intptr_t kPageSize = 64 * KB; |
| #else |
| static constexpr intptr_t kPageSize = 4 * KB; |
| #endif |
| static constexpr intptr_t kPageMask = ~(kPageSize - 1); |
| |
| // Each time we allocate new virtual memory for trampolines we allocate an |
| // [RX][RW] area: |
| // |
| // * [RX] 2 pages fully containing [StubCode::FfiCallbackTrampoline()] |
| // * [RW] pages sufficient to hold |
| // - `kNumRuntimeFunctions` x [uword] function pointers |
| // - `NumCallbackTrampolinesPerPage()` x [MetadataEntry] objects |
| static constexpr intptr_t RXMappingSize() { return 2 * kPageSize; } |
| static constexpr intptr_t RWMappingSize() { |
| return Utils::RoundUp( |
| kNumRuntimeFunctions * compiler::target::kWordSize + |
| sizeof(MetadataEntry) * NumCallbackTrampolinesPerPage(), |
| kPageSize); |
| } |
| static constexpr intptr_t MappingSize() { |
| return RXMappingSize() + RWMappingSize(); |
| } |
| static constexpr intptr_t MappingAlignment() { |
| return Utils::RoundUpToPowerOfTwo(MappingSize()); |
| } |
| static constexpr intptr_t MappingStart(uword address) { |
| const uword mask = MappingAlignment() - 1; |
| return address & ~mask; |
| } |
| static constexpr uword RuntimeFunctionOffset(uword function_index) { |
| return RXMappingSize() + function_index * compiler::target::kWordSize; |
| } |
| static constexpr intptr_t MetadataOffset() { |
| return RuntimeFunctionOffset(kNumRuntimeFunctions); |
| } |
| |
| #if defined(TARGET_ARCH_X64) |
| static constexpr intptr_t kNativeCallbackTrampolineSize = 12; |
| static constexpr intptr_t kNativeCallbackSharedStubSize = 376; |
| static constexpr intptr_t kNativeCallbackTrampolineStackDelta = 2; |
| #elif defined(TARGET_ARCH_IA32) |
| static constexpr intptr_t kNativeCallbackTrampolineSize = 10; |
| static constexpr intptr_t kNativeCallbackSharedStubSize = 193; |
| static constexpr intptr_t kNativeCallbackTrampolineStackDelta = 4; |
| #elif defined(TARGET_ARCH_ARM) |
| static constexpr intptr_t kNativeCallbackTrampolineSize = 8; |
| static constexpr intptr_t kNativeCallbackSharedStubSize = 328; |
| static constexpr intptr_t kNativeCallbackTrampolineStackDelta = 4; |
| #elif defined(TARGET_ARCH_ARM64) |
| static constexpr intptr_t kNativeCallbackTrampolineSize = 8; |
| static constexpr intptr_t kNativeCallbackSharedStubSize = 428; |
| static constexpr intptr_t kNativeCallbackTrampolineStackDelta = 2; |
| #elif defined(TARGET_ARCH_RISCV32) |
| static constexpr intptr_t kNativeCallbackTrampolineSize = 8; |
| static constexpr intptr_t kNativeCallbackSharedStubSize = 302; |
| static constexpr intptr_t kNativeCallbackTrampolineStackDelta = 2; |
| #elif defined(TARGET_ARCH_RISCV64) |
| static constexpr intptr_t kNativeCallbackTrampolineSize = 8; |
| static constexpr intptr_t kNativeCallbackSharedStubSize = 302; |
| static constexpr intptr_t kNativeCallbackTrampolineStackDelta = 2; |
| #else |
| #error What architecture? |
| #endif |
| |
| static void EnsureOnlyTriviallyImmutableValuesInClosure( |
| Zone* zone, |
| ClosurePtr closure_ptr); |
| |
| // Visible for testing. |
| MetadataEntry* MetadataEntryOfTrampoline(Trampoline trampoline) const; |
| Trampoline TrampolineOfMetadataEntry(MetadataEntry* metadata) const; |
| |
| private: |
| FfiCallbackMetadata(); |
| ~FfiCallbackMetadata(); |
| void EnsureStubPageLocked(); |
| void AddToFreeListLocked(MetadataEntry* entry); |
| void DeleteCallbackLocked(MetadataEntry* entry); |
| void FillRuntimeFunction(VirtualMemory* page, uword index, void* function); |
| VirtualMemory* AllocateTrampolinePage(); |
| void EnsureFreeListNotEmptyLocked(); |
| Trampoline CreateMetadataEntry(Isolate* target_isolate, |
| IsolateGroup* target_isolate_group, |
| TrampolineType trampoline_type, |
| uword target_entry_point, |
| uint64_t context, |
| MetadataEntry** list_head); |
| Trampoline CreateSyncFfiCallbackImpl(Isolate* isolate, |
| IsolateGroup* isolate_group, |
| Zone* zone, |
| const Function& function, |
| PersistentHandle* closure, |
| MetadataEntry** list_head); |
| Trampoline TryAllocateFromFreeListLocked(); |
| static uword GetEntryPoint(Zone* zone, const Function& function); |
| static PersistentHandle* CreatePersistentHandle(IsolateGroup* isolate_group, |
| const Closure& closure); |
| |
| static FfiCallbackMetadata* singleton_; |
| |
| mutable Mutex lock_; |
| VirtualMemory* stub_page_ = nullptr; |
| MallocGrowableArray<VirtualMemory*> trampoline_pages_; |
| uword offset_of_first_trampoline_in_page_ = 0; |
| MetadataEntry* free_list_head_ = nullptr; |
| MetadataEntry* free_list_tail_ = nullptr; |
| |
| #if defined(DART_TARGET_OS_FUCHSIA) || \ |
| (defined(SIMULATOR_FFI) && defined(HOST_ARCH_ARM64)) |
| // TODO(https://dartbug.com/52579): Remove. |
| // On Fuchsia, we cannot duplicate the page containing the trampoline stub |
| // unless we plumb through from the embedder the VMO handle that was used to |
| // load the VM isolate snapshot. |
| // On simulator FFI, SimulatorFfiCallbackTrampoline cannot be duplicated |
| // because it contains a PC-relative call. It would need to be replaced with |
| // something like normal stub's PC-relative loading to a corresponding data |
| // page, or if we can assume the initial-exec code model a TLS load. |
| VirtualMemory* original_metadata_page_ = nullptr; |
| #endif // defined(DART_TARGET_OS_FUCHSIA) |
| |
| DISALLOW_COPY_AND_ASSIGN(FfiCallbackMetadata); |
| }; |
| |
| } // namespace dart |
| |
| #endif // RUNTIME_VM_FFI_CALLBACK_METADATA_H_ |