// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ppapi/proxy/ppb_flash_file_proxy.h" #include #include #include #include "base/bind.h" #include "base/logging.h" #include "base/message_loop_proxy.h" #include "base/synchronization/lock.h" #include "base/synchronization/waitable_event.h" #include "build/build_config.h" #include "ipc/ipc_channel_proxy.h" #include "ipc/ipc_message.h" #include "ipc/ipc_sync_message.h" #include "ppapi/c/pp_errors.h" #include "ppapi/c/pp_file_info.h" #include "ppapi/c/private/ppb_flash_file.h" #include "ppapi/proxy/plugin_dispatcher.h" #include "ppapi/proxy/ppapi_messages.h" #include "ppapi/shared_impl/ppapi_globals.h" #include "ppapi/shared_impl/resource.h" #include "ppapi/shared_impl/resource_tracker.h" namespace ppapi { namespace proxy { namespace { // Given an error code and a handle result from a Pepper API call, converts to a // PlatformFileForTransit by sharing with the other side, closing the original // handle, possibly also updating the error value if an error occurred. IPC::PlatformFileForTransit PlatformFileToPlatformFileForTransit( Dispatcher* dispatcher, int32_t* error, base::PlatformFile file) { if (*error != PP_OK) return IPC::InvalidPlatformFileForTransit(); IPC::PlatformFileForTransit out_handle = dispatcher->ShareHandleWithRemote(file, true); if (out_handle == IPC::InvalidPlatformFileForTransit()) *error = PP_ERROR_NOACCESS; return out_handle; } void FreeDirContents(PP_Instance /* instance */, PP_DirContents_Dev* contents) { for (int32_t i = 0; i < contents->count; ++i) delete[] contents->entries[i].name; delete[] contents->entries; delete contents; } } // namespace // ModuleLocalThreadAdapter ---------------------------------------------------- // TODO(yzshen): Refactor to use IPC::SyncMessageFilter. class ModuleLocalThreadAdapter : public base::RefCountedThreadSafe { class Filter; public: ModuleLocalThreadAdapter(); void AddInstanceRouting(PP_Instance instance, Dispatcher* dispatcher); void ClearInstanceRouting(PP_Instance instance); void ClearFilter(Dispatcher* dispatcher, Filter* filter); bool OnModuleLocalMessageReceived(const IPC::Message& msg); // Called on the I/O thread when the channel is being destroyed and the // given message will never be issued a reply. void OnModuleLocalMessageFailed(int message_id); bool Send(PP_Instance instance, IPC::Message* msg); private: class Filter : public IPC::ChannelProxy::MessageFilter { public: explicit Filter(Dispatcher* dispatcher); ~Filter(); void Send(IPC::Message* msg); virtual void OnFilterAdded(IPC::Channel* channel); virtual void OnFilterRemoved(); virtual bool OnMessageReceived(const IPC::Message& message); private: // DO NOT DEREFERENCE! This is used only for tracking. Dispatcher* dispatcher_; IPC::Channel* channel_; // Holds the IPC messages that were sent before the channel was connected. // These will be sent ASAP. std::vector pre_connect_pending_messages_; // Holds the IDs of the sync messages we're currently waiting on for this // channel. This tracking allows us to cancel those requests if the // remote process crashes and we're cleaning up this filter (without just // deadlocking the waiting thread(s). std::set pending_requests_for_filter_; }; void SendFromIOThread(Dispatcher* dispatcher, IPC::Message* msg); // Internal version of OnModuleLocalMessageFailed which assumes the lock // is already held. void OnModuleLocalMessageFailedLocked(int message_id); base::Lock lock_; scoped_refptr main_thread_; // Will be NULL before an instance routing is added. scoped_refptr io_thread_; typedef std::map InstanceToDispatcher; InstanceToDispatcher instance_to_dispatcher_; // The filters are owned by the channel. typedef std::map DispatcherToFilter; DispatcherToFilter dispatcher_to_filter_; // Tracks all messages with currently waiting threads. This does not own // the pointer, the pointer lifetime is managed by Send(). typedef std::map SyncRequestMap; SyncRequestMap pending_sync_requests_; }; ModuleLocalThreadAdapter* g_module_local_thread_adapter = NULL; ModuleLocalThreadAdapter::Filter::Filter(Dispatcher* dispatcher) : dispatcher_(dispatcher), channel_(NULL) { } ModuleLocalThreadAdapter::Filter::~Filter() { } void ModuleLocalThreadAdapter::Filter::Send(IPC::Message* msg) { if (channel_) { int message_id = IPC::SyncMessage::GetMessageId(*msg); if (channel_->Send(msg)) pending_requests_for_filter_.insert(message_id); else // Message lost, notify adapter so it can unblock. g_module_local_thread_adapter->OnModuleLocalMessageFailed(message_id); } else { // No channel, save this message for when it's connected. pre_connect_pending_messages_.push_back(msg); } } void ModuleLocalThreadAdapter::Filter::OnFilterAdded(IPC::Channel* channel) { DCHECK(!channel_); channel_ = channel; // Now that we have a channel, process all pending messages. for (size_t i = 0; i < pre_connect_pending_messages_.size(); i++) Send(pre_connect_pending_messages_[i]); pre_connect_pending_messages_.clear(); } void ModuleLocalThreadAdapter::Filter::OnFilterRemoved() { DCHECK(channel_); channel_ = NULL; g_module_local_thread_adapter->ClearFilter(dispatcher_, this); for (std::set::iterator i = pending_requests_for_filter_.begin(); i != pending_requests_for_filter_.end(); ++i) { g_module_local_thread_adapter->OnModuleLocalMessageFailed(*i); } } bool ModuleLocalThreadAdapter::Filter::OnMessageReceived( const IPC::Message& message) { if (!message.is_reply() || message.routing_id() != API_ID_PPB_FLASH_FILE_MODULELOCAL) return false; if (g_module_local_thread_adapter->OnModuleLocalMessageReceived(message)) { // The message was consumed, this means we can remove the message ID from // the list of messages this channel is waiting on. pending_requests_for_filter_.erase(IPC::SyncMessage::GetMessageId(message)); return true; } return false; } ModuleLocalThreadAdapter::ModuleLocalThreadAdapter() : main_thread_(base::MessageLoopProxy::current()) { } void ModuleLocalThreadAdapter::AddInstanceRouting(PP_Instance instance, Dispatcher* dispatcher) { base::AutoLock lock(lock_); // Now that we've had contact with a dispatcher, we can set up the IO thread. DCHECK(main_thread_->BelongsToCurrentThread()); if (!io_thread_.get()) io_thread_ = dispatcher->GetIPCMessageLoop(); // Set up the instance -> dispatcher routing. DCHECK(instance_to_dispatcher_.find(instance) == instance_to_dispatcher_.end()); instance_to_dispatcher_[instance] = dispatcher; DispatcherToFilter::iterator found_filter = dispatcher_to_filter_.find(dispatcher); if (found_filter == dispatcher_to_filter_.end()) { // Need to set up a filter for this dispatcher to intercept the messages. Filter* filter = new Filter(dispatcher); dispatcher_to_filter_[dispatcher] = filter; dispatcher->AddIOThreadMessageFilter(filter); } } void ModuleLocalThreadAdapter::ClearInstanceRouting(PP_Instance instance) { // The dispatcher->filter mapping is cleaned up by ClearFilter which is // initiated by the channel. instance_to_dispatcher_.erase(instance); } void ModuleLocalThreadAdapter::ClearFilter(Dispatcher* dispatcher, Filter* filter) { // DANGER! Don't dereference the dispatcher, it's just used to identify // which filter to remove. The dispatcher may not even exist any more. // // Since the dispatcher may be gone, there's a potential for ambiguity if // another one is created on the main thread before this code runs on the // I/O thread. So we check that the filter matches to avoid this rare case. base::AutoLock lock(lock_); if (dispatcher_to_filter_[dispatcher] == filter) dispatcher_to_filter_.erase(dispatcher); } bool ModuleLocalThreadAdapter::OnModuleLocalMessageReceived( const IPC::Message& msg) { base::AutoLock lock(lock_); int message_id = IPC::SyncMessage::GetMessageId(msg); SyncRequestMap::iterator found = pending_sync_requests_.find(message_id); if (found == pending_sync_requests_.end()) { // Not waiting for this event. This will happen for sync messages to the // main thread which use the "regular" sync channel code path. return false; } IPC::PendingSyncMsg& info = *found->second; if (!msg.is_reply_error()) info.deserializer->SerializeOutputParameters(msg); info.done_event->Signal(); return true; } void ModuleLocalThreadAdapter::OnModuleLocalMessageFailed(int message_id) { base::AutoLock lock(lock_); OnModuleLocalMessageFailedLocked(message_id); } bool ModuleLocalThreadAdapter::Send(PP_Instance instance, IPC::Message* msg) { // Compute the dispatcher corresponding to this message. Dispatcher* dispatcher = NULL; { base::AutoLock lock(lock_); InstanceToDispatcher::iterator found = instance_to_dispatcher_.find(instance); if (found == instance_to_dispatcher_.end()) { NOTREACHED(); delete msg; return false; } dispatcher = found->second; } if (main_thread_->BelongsToCurrentThread()) { // Easy case: We're on the same thread as the dispatcher, so we don't need // a lock to access it, and we can just use the normal sync channel stuff // to handle the message. Actually, we MUST use the normal sync channel // stuff since there may be incoming sync messages that need processing. // The code below doesn't handle any nested message loops. return dispatcher->Send(msg); } // Background thread case // ---------------------- // 1. Generate tracking info, stick in pending_sync_messages_map. // 2. Kick off the request. This is done on the I/O thread. // 3. Filter on the I/O thread notices reply, writes the reply data and // signals the event. We block on the event while this is happening. // 4. Remove tracking info. // Generate the tracking info. and copied IPC::SyncMessage* sync_msg = static_cast(msg); int message_id = IPC::SyncMessage::GetMessageId(*sync_msg); base::WaitableEvent event(true, false); scoped_ptr deserializer( sync_msg->GetReplyDeserializer()); // We own this pointer once retrieved. IPC::PendingSyncMsg info(message_id, deserializer.get(), &event); // Add the tracking information to our map. { base::AutoLock lock(lock_); pending_sync_requests_[message_id] = &info; } // This is a bit dangerous. We use the dispatcher pointer as the routing // ID for this message. While we don't dereference it, there is an // exceedingly remote possibility that while this is going to the background // thread the connection will be shut down and a new one will be created with // a dispatcher at the same address. It could potentially get sent to a // random place, but it should actually still work (since the Flash file // operations are global). io_thread_->PostTask(FROM_HERE, base::Bind(&ModuleLocalThreadAdapter::SendFromIOThread, this, dispatcher, msg)); // Now we block the current thread waiting for the reply. event.Wait(); { // Clear our tracking info for this message now that we're done. base::AutoLock lock(lock_); DCHECK(pending_sync_requests_.find(message_id) != pending_sync_requests_.end()); pending_sync_requests_.erase(message_id); } return true; } void ModuleLocalThreadAdapter::SendFromIOThread(Dispatcher* dispatcher, IPC::Message* msg) { // DO NOT DEREFERENCE DISPATCHER. Used as a lookup only. base::AutoLock lock(lock_); DispatcherToFilter::iterator found = dispatcher_to_filter_.find(dispatcher); // The dispatcher could have been destroyed by the time we got here since // we're on another thread. Need to unblock the caller. if (found == dispatcher_to_filter_.end()) { OnModuleLocalMessageFailedLocked(IPC::SyncMessage::GetMessageId(*msg)); delete msg; return; } // Takes ownership of pointer. found->second->Send(msg); } void ModuleLocalThreadAdapter::OnModuleLocalMessageFailedLocked( int message_id) { lock_.AssertAcquired(); // Unblock the thread waiting for the message that will never come. SyncRequestMap::iterator found = pending_sync_requests_.find(message_id); if (found == pending_sync_requests_.end()) { NOTREACHED(); return; } found->second->done_event->Signal(); } // PPB_Flash_File_ModuleLocal -------------------------------------------------- namespace { bool CreateThreadAdapterForInstance(PP_Instance instance) { if (!g_module_local_thread_adapter) { g_module_local_thread_adapter = new ModuleLocalThreadAdapter(); g_module_local_thread_adapter->AddRef(); // Leaked, this object is global. } PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance); if (!dispatcher) { NOTREACHED(); return false; } g_module_local_thread_adapter->AddInstanceRouting(instance, dispatcher); return true; } void ClearThreadAdapterForInstance(PP_Instance instance) { if (g_module_local_thread_adapter) g_module_local_thread_adapter->ClearInstanceRouting(instance); } int32_t OpenModuleLocalFile(PP_Instance instance, const char* path, int32_t mode, PP_FileHandle* file) { if (!g_module_local_thread_adapter) return PP_ERROR_FAILED; int32_t result = PP_ERROR_FAILED; IPC::PlatformFileForTransit transit; g_module_local_thread_adapter->Send(instance, new PpapiHostMsg_PPBFlashFile_ModuleLocal_OpenFile( API_ID_PPB_FLASH_FILE_MODULELOCAL, instance, path, mode, &transit, &result)); *file = IPC::PlatformFileForTransitToPlatformFile(transit); return result; } int32_t RenameModuleLocalFile(PP_Instance instance, const char* from_path, const char* to_path) { if (!g_module_local_thread_adapter) return PP_ERROR_FAILED; int32_t result = PP_ERROR_FAILED; g_module_local_thread_adapter->Send(instance, new PpapiHostMsg_PPBFlashFile_ModuleLocal_RenameFile( API_ID_PPB_FLASH_FILE_MODULELOCAL, instance, from_path, to_path, &result)); return result; } int32_t DeleteModuleLocalFileOrDir(PP_Instance instance, const char* path, PP_Bool recursive) { if (!g_module_local_thread_adapter) return PP_ERROR_FAILED; int32_t result = PP_ERROR_FAILED; g_module_local_thread_adapter->Send(instance, new PpapiHostMsg_PPBFlashFile_ModuleLocal_DeleteFileOrDir( API_ID_PPB_FLASH_FILE_MODULELOCAL, instance, path, recursive, &result)); return result; } int32_t CreateModuleLocalDir(PP_Instance instance, const char* path) { if (!g_module_local_thread_adapter) return PP_ERROR_FAILED; int32_t result = PP_ERROR_FAILED; g_module_local_thread_adapter->Send(instance, new PpapiHostMsg_PPBFlashFile_ModuleLocal_CreateDir( API_ID_PPB_FLASH_FILE_MODULELOCAL, instance, path, &result)); return result; } int32_t QueryModuleLocalFile(PP_Instance instance, const char* path, PP_FileInfo* info) { if (!g_module_local_thread_adapter) return PP_ERROR_FAILED; int32_t result = PP_ERROR_FAILED; g_module_local_thread_adapter->Send(instance, new PpapiHostMsg_PPBFlashFile_ModuleLocal_QueryFile( API_ID_PPB_FLASH_FILE_MODULELOCAL, instance, path, info, &result)); return result; } int32_t GetModuleLocalDirContents(PP_Instance instance, const char* path, PP_DirContents_Dev** contents) { if (!g_module_local_thread_adapter) return PP_ERROR_FAILED; int32_t result = PP_ERROR_FAILED; std::vector entries; g_module_local_thread_adapter->Send(instance, new PpapiHostMsg_PPBFlashFile_ModuleLocal_GetDirContents( API_ID_PPB_FLASH_FILE_MODULELOCAL, instance, path, &entries, &result)); if (result != PP_OK) return result; // Copy the serialized dir entries to the output struct. *contents = new PP_DirContents_Dev; (*contents)->count = static_cast(entries.size()); (*contents)->entries = new PP_DirEntry_Dev[entries.size()]; for (size_t i = 0; i < entries.size(); i++) { const SerializedDirEntry& source = entries[i]; PP_DirEntry_Dev* dest = &(*contents)->entries[i]; char* name_copy = new char[source.name.size() + 1]; memcpy(name_copy, source.name.c_str(), source.name.size() + 1); dest->name = name_copy; dest->is_dir = PP_FromBool(source.is_dir); } return result; } const PPB_Flash_File_ModuleLocal flash_file_modulelocal_interface = { &CreateThreadAdapterForInstance, &ClearThreadAdapterForInstance, &OpenModuleLocalFile, &RenameModuleLocalFile, &DeleteModuleLocalFileOrDir, &CreateModuleLocalDir, &QueryModuleLocalFile, &GetModuleLocalDirContents, &FreeDirContents, }; InterfaceProxy* CreateFlashFileModuleLocalProxy(Dispatcher* dispatcher) { return new PPB_Flash_File_ModuleLocal_Proxy(dispatcher); } } // namespace PPB_Flash_File_ModuleLocal_Proxy::PPB_Flash_File_ModuleLocal_Proxy( Dispatcher* dispatcher) : InterfaceProxy(dispatcher), ppb_flash_file_module_local_impl_(NULL) { if (!dispatcher->IsPlugin()) { ppb_flash_file_module_local_impl_ = static_cast( dispatcher->local_get_interface()( PPB_FLASH_FILE_MODULELOCAL_INTERFACE)); } } PPB_Flash_File_ModuleLocal_Proxy::~PPB_Flash_File_ModuleLocal_Proxy() { } // static const PPB_Flash_File_ModuleLocal* PPB_Flash_File_ModuleLocal_Proxy::GetInterface() { return &flash_file_modulelocal_interface; } bool PPB_Flash_File_ModuleLocal_Proxy::OnMessageReceived( const IPC::Message& msg) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(PPB_Flash_File_ModuleLocal_Proxy, msg) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_ModuleLocal_OpenFile, OnMsgOpenFile) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_ModuleLocal_RenameFile, OnMsgRenameFile) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_ModuleLocal_DeleteFileOrDir, OnMsgDeleteFileOrDir) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_ModuleLocal_CreateDir, OnMsgCreateDir) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_ModuleLocal_QueryFile, OnMsgQueryFile) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_ModuleLocal_GetDirContents, OnMsgGetDirContents) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() // TODO(brettw) handle bad messages! return handled; } void PPB_Flash_File_ModuleLocal_Proxy::OnMsgOpenFile( PP_Instance instance, const std::string& path, int32_t mode, IPC::PlatformFileForTransit* file_handle, int32_t* result) { base::PlatformFile file; *result = ppb_flash_file_module_local_impl_->OpenFile( instance, path.c_str(), mode, &file); *file_handle = PlatformFileToPlatformFileForTransit( dispatcher(), result, file); } void PPB_Flash_File_ModuleLocal_Proxy::OnMsgRenameFile( PP_Instance instance, const std::string& from_path, const std::string& to_path, int32_t* result) { *result = ppb_flash_file_module_local_impl_->RenameFile( instance, from_path.c_str(), to_path.c_str()); } void PPB_Flash_File_ModuleLocal_Proxy::OnMsgDeleteFileOrDir( PP_Instance instance, const std::string& path, PP_Bool recursive, int32_t* result) { *result = ppb_flash_file_module_local_impl_->DeleteFileOrDir( instance, path.c_str(), recursive); } void PPB_Flash_File_ModuleLocal_Proxy::OnMsgCreateDir(PP_Instance instance, const std::string& path, int32_t* result) { *result = ppb_flash_file_module_local_impl_->CreateDir( instance, path.c_str()); } void PPB_Flash_File_ModuleLocal_Proxy::OnMsgQueryFile(PP_Instance instance, const std::string& path, PP_FileInfo* info, int32_t* result) { *result = ppb_flash_file_module_local_impl_->QueryFile( instance, path.c_str(), info); } void PPB_Flash_File_ModuleLocal_Proxy::OnMsgGetDirContents( PP_Instance instance, const std::string& path, std::vector* entries, int32_t* result) { PP_DirContents_Dev* contents = NULL; *result = ppb_flash_file_module_local_impl_->GetDirContents( instance, path.c_str(), &contents); if (*result != PP_OK) return; // Convert the list of entries to the serialized version. entries->resize(contents->count); for (int32_t i = 0; i < contents->count; i++) { (*entries)[i].name.assign(contents->entries[i].name); (*entries)[i].is_dir = PP_ToBool(contents->entries[i].is_dir); } ppb_flash_file_module_local_impl_->FreeDirContents(instance, contents); } // PPB_Flash_File_FileRef ------------------------------------------------------ namespace { int32_t OpenFileRefFile(PP_Resource file_ref_id, int32_t mode, PP_FileHandle* file) { Resource* file_ref = PpapiGlobals::Get()->GetResourceTracker()->GetResource(file_ref_id); if (!file_ref) return PP_ERROR_BADRESOURCE; PluginDispatcher* dispatcher = PluginDispatcher::GetForResource(file_ref); if (!dispatcher) return PP_ERROR_BADARGUMENT; int32_t result = PP_ERROR_FAILED; IPC::PlatformFileForTransit transit; dispatcher->Send(new PpapiHostMsg_PPBFlashFile_FileRef_OpenFile( API_ID_PPB_FLASH_FILE_FILEREF, file_ref->host_resource(), mode, &transit, &result)); *file = IPC::PlatformFileForTransitToPlatformFile(transit); return result; } int32_t QueryFileRefFile(PP_Resource file_ref_id, PP_FileInfo* info) { Resource* file_ref = PpapiGlobals::Get()->GetResourceTracker()->GetResource(file_ref_id); if (!file_ref) return PP_ERROR_BADRESOURCE; PluginDispatcher* dispatcher = PluginDispatcher::GetForResource(file_ref); if (!dispatcher) return PP_ERROR_BADARGUMENT; int32_t result = PP_ERROR_FAILED; dispatcher->Send(new PpapiHostMsg_PPBFlashFile_FileRef_QueryFile( API_ID_PPB_FLASH_FILE_FILEREF, file_ref->host_resource(), info, &result)); return result; } const PPB_Flash_File_FileRef flash_file_fileref_interface = { &OpenFileRefFile, &QueryFileRefFile, }; InterfaceProxy* CreateFlashFileFileRefProxy(Dispatcher* dispatcher) { return new PPB_Flash_File_FileRef_Proxy(dispatcher); } } // namespace PPB_Flash_File_FileRef_Proxy::PPB_Flash_File_FileRef_Proxy( Dispatcher* dispatcher) : InterfaceProxy(dispatcher), ppb_flash_file_fileref_impl_(NULL) { if (!dispatcher->IsPlugin()) { ppb_flash_file_fileref_impl_ = static_cast( dispatcher->local_get_interface()(PPB_FLASH_FILE_FILEREF_INTERFACE)); } } PPB_Flash_File_FileRef_Proxy::~PPB_Flash_File_FileRef_Proxy() { } // static const PPB_Flash_File_FileRef* PPB_Flash_File_FileRef_Proxy::GetInterface() { return &flash_file_fileref_interface; } bool PPB_Flash_File_FileRef_Proxy::OnMessageReceived( const IPC::Message& msg) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(PPB_Flash_File_FileRef_Proxy, msg) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_FileRef_OpenFile, OnMsgOpenFile) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBFlashFile_FileRef_QueryFile, OnMsgQueryFile) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() // TODO(brettw) handle bad messages! return handled; } void PPB_Flash_File_FileRef_Proxy::OnMsgOpenFile( const HostResource& host_resource, int32_t mode, IPC::PlatformFileForTransit* file_handle, int32_t* result) { base::PlatformFile file; *result = ppb_flash_file_fileref_impl_->OpenFile( host_resource.host_resource(), mode, &file); *file_handle = PlatformFileToPlatformFileForTransit( dispatcher(), result, file); } void PPB_Flash_File_FileRef_Proxy::OnMsgQueryFile( const HostResource& host_resource, PP_FileInfo* info, int32_t* result) { *result = ppb_flash_file_fileref_impl_->QueryFile( host_resource.host_resource(), info); } } // namespace proxy } // namespace ppapi