|  | // 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_ |