Kernel: New handle manager

This handle manager more closely mirrors the behaviour of the CTR-OS
one. In addition object ref-counts and support for DuplicateHandle have
been added.

Note that support for DuplicateHandle is still experimental, since parts
of the kernel still use Handles internally, which will likely cause
troubles if two different handles to the same object are used to e.g.
wait on a synchronization primitive.
pull/8/head
Yuri Kunde Schlesner 10 years ago
parent 23f2142009
commit 7e2903cb74

@ -62,7 +62,8 @@ ResultCode ArbitrateAddress(Handle handle, ArbitrationType type, u32 address, s3
/// Create an address arbiter /// Create an address arbiter
AddressArbiter* CreateAddressArbiter(Handle& handle, const std::string& name) { AddressArbiter* CreateAddressArbiter(Handle& handle, const std::string& name) {
AddressArbiter* address_arbiter = new AddressArbiter; AddressArbiter* address_arbiter = new AddressArbiter;
handle = Kernel::g_handle_table.Create(address_arbiter); // TOOD(yuriks): Fix error reporting
handle = Kernel::g_handle_table.Create(address_arbiter).ValueOr(INVALID_HANDLE);
address_arbiter->name = name; address_arbiter->name = name;
return address_arbiter; return address_arbiter;
} }

@ -129,7 +129,8 @@ ResultCode ClearEvent(Handle handle) {
Event* CreateEvent(Handle& handle, const ResetType reset_type, const std::string& name) { Event* CreateEvent(Handle& handle, const ResetType reset_type, const std::string& name) {
Event* evt = new Event; Event* evt = new Event;
handle = Kernel::g_handle_table.Create(evt); // TOOD(yuriks): Fix error reporting
handle = Kernel::g_handle_table.Create(evt).ValueOr(INVALID_HANDLE);
evt->locked = true; evt->locked = true;
evt->permanent_locked = false; evt->permanent_locked = false;

@ -17,73 +17,89 @@ HandleTable g_handle_table;
u64 g_program_id = 0; u64 g_program_id = 0;
HandleTable::HandleTable() { HandleTable::HandleTable() {
next_id = INITIAL_NEXT_ID; next_generation = 1;
Clear();
} }
Handle HandleTable::Create(Object* obj, int range_bottom, int range_top) { ResultVal<Handle> HandleTable::Create(Object* obj) {
if (range_top > MAX_COUNT) { _dbg_assert_(Kernel, obj != nullptr);
range_top = MAX_COUNT;
} u16 slot = next_free_slot;
if (next_id >= range_bottom && next_id < range_top) { if (slot >= generations.size()) {
range_bottom = next_id++; LOG_ERROR(Kernel, "Unable to allocate Handle, too many slots in use.");
} return ERR_OUT_OF_HANDLES;
for (int i = range_bottom; i < range_top; i++) {
if (!occupied[i]) {
occupied[i] = true;
pool[i] = obj;
pool[i]->handle = i + HANDLE_OFFSET;
return i + HANDLE_OFFSET;
}
} }
LOG_ERROR(Kernel, "Unable to allocate kernel object, too many objects slots in use."); next_free_slot = generations[slot];
return 0;
}
bool HandleTable::IsValid(Handle handle) const { u16 generation = next_generation++;
int index = handle - HANDLE_OFFSET;
if (index < 0)
return false;
if (index >= MAX_COUNT)
return false;
return occupied[index]; // Overflow count so it fits in the 15 bits dedicated to the generation in the handle.
// CTR-OS doesn't use generation 0, so skip straight to 1.
if (next_generation >= (1 << 15)) next_generation = 1;
generations[slot] = generation;
intrusive_ptr_add_ref(obj);
objects[slot] = obj;
Handle handle = generation | (slot << 15);
obj->handle = handle;
return MakeResult<Handle>(handle);
} }
void HandleTable::Clear() { ResultVal<Handle> HandleTable::Duplicate(Handle handle) {
for (int i = 0; i < MAX_COUNT; i++) { Object* object = GetGeneric(handle);
//brutally clear everything, no validation if (object == nullptr) {
if (occupied[i]) LOG_ERROR(Kernel, "Tried to duplicate invalid handle: %08X", handle);
delete pool[i]; return ERR_INVALID_HANDLE;
occupied[i] = false;
} }
pool.fill(nullptr); return Create(object);
next_id = INITIAL_NEXT_ID;
} }
Object* &HandleTable::operator [](Handle handle) ResultCode HandleTable::Close(Handle handle) {
{ if (!IsValid(handle))
_dbg_assert_msg_(Kernel, IsValid(handle), "GRABBING UNALLOCED KERNEL OBJ"); return ERR_INVALID_HANDLE;
return pool[handle - HANDLE_OFFSET];
size_t slot = GetSlot(handle);
u16 generation = GetGeneration(handle);
intrusive_ptr_release(objects[slot]);
objects[slot] = nullptr;
generations[generation] = next_free_slot;
next_free_slot = slot;
return RESULT_SUCCESS;
} }
void HandleTable::List() { bool HandleTable::IsValid(Handle handle) const {
for (int i = 0; i < MAX_COUNT; i++) { size_t slot = GetSlot(handle);
if (occupied[i]) { u16 generation = GetGeneration(handle);
if (pool[i]) {
LOG_DEBUG(Kernel, "KO %i: %s \"%s\"", i + HANDLE_OFFSET, pool[i]->GetTypeName().c_str(), return slot < MAX_COUNT && objects[slot] != nullptr && generations[slot] == generation;
pool[i]->GetName().c_str());
}
}
}
} }
int HandleTable::GetCount() const { Object* HandleTable::GetGeneric(Handle handle) const {
return std::count(occupied.begin(), occupied.end(), true); if (handle == CurrentThread) {
// TODO(yuriks) Directly return the pointer once this is possible.
handle = GetCurrentThreadHandle();
} else if (handle == CurrentProcess) {
LOG_ERROR(Kernel, "Current process (%08X) pseudo-handle not supported", CurrentProcess);
return nullptr;
}
if (!IsValid(handle)) {
return nullptr;
}
return objects[GetSlot(handle)];
} }
Object* HandleTable::CreateByIDType(int type) { void HandleTable::Clear() {
LOG_ERROR(Kernel, "Unimplemented: %d.", type); for (size_t i = 0; i < MAX_COUNT; ++i) {
return nullptr; generations[i] = i + 1;
if (objects[i] != nullptr)
intrusive_ptr_release(objects[i]);
objects[i] = nullptr;
}
next_free_slot = 0;
} }
/// Initialize the kernel /// Initialize the kernel

@ -12,13 +12,17 @@
typedef u32 Handle; typedef u32 Handle;
typedef s32 Result; typedef s32 Result;
const Handle INVALID_HANDLE = 0;
namespace Kernel { namespace Kernel {
// From kernel.h. Declarations duplicated here to avoid a circular header dependency. // TODO: Verify code
class Thread; const ResultCode ERR_OUT_OF_HANDLES(ErrorDescription::OutOfMemory, ErrorModule::Kernel,
Thread* GetCurrentThread(); ErrorSummary::OutOfResource, ErrorLevel::Temporary);
// TOOD: Verify code
const ResultCode ERR_INVALID_HANDLE = InvalidHandle(ErrorModule::Kernel);
enum KernelHandle { enum KernelHandle : Handle {
CurrentThread = 0xFFFF8000, CurrentThread = 0xFFFF8000,
CurrentProcess = 0xFFFF8001, CurrentProcess = 0xFFFF8001,
}; };
@ -61,103 +65,127 @@ public:
LOG_ERROR(Kernel, "(UNIMPLEMENTED)"); LOG_ERROR(Kernel, "(UNIMPLEMENTED)");
return UnimplementedFunction(ErrorModule::Kernel); return UnimplementedFunction(ErrorModule::Kernel);
} }
};
class HandleTable : NonCopyable { private:
public: friend void intrusive_ptr_add_ref(Object*);
HandleTable(); friend void intrusive_ptr_release(Object*);
~HandleTable() {}
// Allocates a handle within the range and inserts the object into the map. unsigned int ref_count = 0;
Handle Create(Object* obj, int range_bottom=INITIAL_NEXT_ID, int range_top=0x7FFFFFFF); };
static Object* CreateByIDType(int type); // Special functions that will later be used by boost::instrusive_ptr to do automatic ref-counting
inline void intrusive_ptr_add_ref(Object* object) {
++object->ref_count;
}
template <class T> inline void intrusive_ptr_release(Object* object) {
void Destroy(Handle handle) { if (--object->ref_count == 0) {
if (Get<T>(handle)) { delete object;
occupied[handle - HANDLE_OFFSET] = false;
delete pool[handle - HANDLE_OFFSET];
}
} }
}
bool IsValid(Handle handle) const; /**
* This class allows the creation of Handles, which are references to objects that can be tested
* for validity and looked up. Here they are used to pass references to kernel objects to/from the
* emulated process. it has been designed so that it follows the same handle format and has
* approximately the same restrictions as the handle manager in the CTR-OS.
*
* Handles contain two sub-fields: a slot index (bits 31:15) and a generation value (bits 14:0).
* The slot index is used to index into the arrays in this class to access the data corresponding
* to the Handle.
*
* To prevent accidental use of a freed Handle whose slot has already been reused, a global counter
* is kept and incremented every time a Handle is created. This is the Handle's "generation". The
* value of the counter is stored into the Handle as well as in the handle table (in the
* "generations" array). When looking up a handle, the Handle's generation must match with the
* value stored on the class, otherwise the Handle is considered invalid.
*
* To find free slots when allocating a Handle without needing to scan the entire object array, the
* generations field of unallocated slots is re-purposed as a linked list of indices to free slots.
* When a Handle is created, an index is popped off the list and used for the new Handle. When it
* is destroyed, it is again pushed onto the list to be re-used by the next allocation. It is
* likely that this allocation strategy differs from the one used in CTR-OS, but this hasn't been
* verified and isn't likely to cause any problems.
*/
class HandleTable final : NonCopyable {
public:
HandleTable();
template <class T> /**
T* Get(Handle handle) { * Allocates a handle for the given object.
if (handle == CurrentThread) { * @return The created Handle or one of the following errors:
return reinterpret_cast<T*>(GetCurrentThread()); * - `ERR_OUT_OF_HANDLES`: the maximum number of handles has been exceeded.
} */
ResultVal<Handle> Create(Object* obj);
if (handle < HANDLE_OFFSET || handle >= HANDLE_OFFSET + MAX_COUNT || !occupied[handle - HANDLE_OFFSET]) { /**
if (handle != 0) { * Returns a new handle that points to the same object as the passed in handle.
LOG_ERROR(Kernel, "Bad object handle %08x", handle); * @return The duplicated Handle or one of the following errors:
} * - `ERR_INVALID_HANDLE`: an invalid handle was passed in.
return nullptr; * - Any errors returned by `Create()`.
} else { */
Object* t = pool[handle - HANDLE_OFFSET]; ResultVal<Handle> Duplicate(Handle handle);
if (t->GetHandleType() != T::HANDLE_TYPE) {
LOG_ERROR(Kernel, "Wrong object type for %08x", handle);
return nullptr;
}
return static_cast<T*>(t);
}
}
// ONLY use this when you know the handle is valid. /**
template <class T> * Closes a handle, removing it from the table and decreasing the object's ref-count.
T *GetFast(Handle handle) { * @return `RESULT_SUCCESS` or one of the following errors:
if (handle == CurrentThread) { * - `ERR_INVALID_HANDLE`: an invalid handle was passed in.
return reinterpret_cast<T*>(GetCurrentThread()); */
} ResultCode Close(Handle handle);
const Handle realHandle = handle - HANDLE_OFFSET; /// Checks if a handle is valid and points to an existing object.
_dbg_assert_(Kernel, realHandle >= 0 && realHandle < MAX_COUNT && occupied[realHandle]); bool IsValid(Handle handle) const;
return static_cast<T*>(pool[realHandle]);
}
template <class T, typename ArgT> /**
void Iterate(bool func(T*, ArgT), ArgT arg) { * Looks up a handle.
int type = T::GetStaticIDType(); * @returns Pointer to the looked-up object, or `nullptr` if the handle is not valid.
for (int i = 0; i < MAX_COUNT; i++) */
{ Object* GetGeneric(Handle handle) const;
if (!occupied[i])
continue;
T* t = static_cast<T*>(pool[i]);
if (t->GetIDType() == type) {
if (!func(t, arg))
break;
}
}
}
bool GetIDType(Handle handle, HandleType* type) const { /**
if ((handle < HANDLE_OFFSET) || (handle >= HANDLE_OFFSET + MAX_COUNT) || * Looks up a handle while verifying its type.
!occupied[handle - HANDLE_OFFSET]) { * @returns Pointer to the looked-up object, or `nullptr` if the handle is not valid or its
LOG_ERROR(Kernel, "Bad object handle %08X", handle); * type differs from the handle type `T::HANDLE_TYPE`.
return false; */
template <class T>
T* Get(Handle handle) const {
Object* object = GetGeneric(handle);
if (object != nullptr && object->GetHandleType() == T::HANDLE_TYPE) {
return static_cast<T*>(object);
} }
Object* t = pool[handle - HANDLE_OFFSET]; return nullptr;
*type = t->GetHandleType();
return true;
} }
Object* &operator [](Handle handle); /// Closes all handles held in this table.
void List();
void Clear(); void Clear();
int GetCount() const;
private: private:
/**
* This is the maximum limit of handles allowed per process in CTR-OS. It can be further
* reduced by ExHeader values, but this is not emulated here.
*/
static const size_t MAX_COUNT = 4096;
static size_t GetSlot(Handle handle) { return handle >> 15; }
static u16 GetGeneration(Handle handle) { return handle & 0x7FFF; }
/// Stores the Object referenced by the handle or null if the slot is empty.
std::array<Object*, MAX_COUNT> objects;
enum { /**
MAX_COUNT = 0x1000, * The value of `next_generation` when the handle was created, used to check for validity. For
HANDLE_OFFSET = 0x100, * empty slots, contains the index of the next free slot in the list.
INITIAL_NEXT_ID = 0x10, */
}; std::array<u16, MAX_COUNT> generations;
/**
* Global counter of the number of created handles. Stored in `generations` when a handle is
* created, and wraps around to 1 when it hits 0x8000.
*/
u16 next_generation;
std::array<Object*, MAX_COUNT> pool; /// Head of the free slots linked list.
std::array<bool, MAX_COUNT> occupied; u16 next_free_slot;
int next_id;
}; };
extern HandleTable g_handle_table; extern HandleTable g_handle_table;

@ -87,7 +87,7 @@ void ReleaseThreadMutexes(Handle thread) {
// Release every mutex that the thread holds, and resume execution on the waiting threads // Release every mutex that the thread holds, and resume execution on the waiting threads
for (MutexMap::iterator iter = locked.first; iter != locked.second; ++iter) { for (MutexMap::iterator iter = locked.first; iter != locked.second; ++iter) {
Mutex* mutex = g_handle_table.GetFast<Mutex>(iter->second); Mutex* mutex = g_handle_table.Get<Mutex>(iter->second);
ResumeWaitingThread(mutex); ResumeWaitingThread(mutex);
} }
@ -136,7 +136,8 @@ ResultCode ReleaseMutex(Handle handle) {
*/ */
Mutex* CreateMutex(Handle& handle, bool initial_locked, const std::string& name) { Mutex* CreateMutex(Handle& handle, bool initial_locked, const std::string& name) {
Mutex* mutex = new Mutex; Mutex* mutex = new Mutex;
handle = Kernel::g_handle_table.Create(mutex); // TODO(yuriks): Fix error reporting
handle = Kernel::g_handle_table.Create(mutex).ValueOr(INVALID_HANDLE);
mutex->locked = mutex->initial_locked = initial_locked; mutex->locked = mutex->initial_locked = initial_locked;
mutex->name = name; mutex->name = name;

@ -57,7 +57,8 @@ ResultCode CreateSemaphore(Handle* handle, s32 initial_count,
ErrorSummary::WrongArgument, ErrorLevel::Permanent); ErrorSummary::WrongArgument, ErrorLevel::Permanent);
Semaphore* semaphore = new Semaphore; Semaphore* semaphore = new Semaphore;
*handle = g_handle_table.Create(semaphore); // TOOD(yuriks): Fix error reporting
*handle = g_handle_table.Create(semaphore).ValueOr(INVALID_HANDLE);
// When the semaphore is created, some slots are reserved for other threads, // When the semaphore is created, some slots are reserved for other threads,
// and the rest is reserved for the caller thread // and the rest is reserved for the caller thread

@ -32,7 +32,8 @@ public:
*/ */
SharedMemory* CreateSharedMemory(Handle& handle, const std::string& name) { SharedMemory* CreateSharedMemory(Handle& handle, const std::string& name) {
SharedMemory* shared_memory = new SharedMemory; SharedMemory* shared_memory = new SharedMemory;
handle = Kernel::g_handle_table.Create(shared_memory); // TOOD(yuriks): Fix error reporting
handle = Kernel::g_handle_table.Create(shared_memory).ValueOr(INVALID_HANDLE);
shared_memory->name = name; shared_memory->name = name;
return shared_memory; return shared_memory;
} }

@ -341,7 +341,8 @@ Thread* CreateThread(Handle& handle, const char* name, u32 entry_point, s32 prio
Thread* thread = new Thread; Thread* thread = new Thread;
handle = Kernel::g_handle_table.Create(thread); // TOOD(yuriks): Fix error reporting
handle = Kernel::g_handle_table.Create(thread).ValueOr(INVALID_HANDLE);
thread_queue.push_back(handle); thread_queue.push_back(handle);
thread_ready_queue.prepare(priority); thread_ready_queue.prepare(priority);

@ -77,9 +77,6 @@ Handle ArbitrateHighestPriorityThread(u32 arbiter, u32 address);
/// Arbitrate all threads currently waiting... /// Arbitrate all threads currently waiting...
void ArbitrateAllThreads(u32 arbiter, u32 address); void ArbitrateAllThreads(u32 arbiter, u32 address);
/// Gets the current thread
Thread* GetCurrentThread();
/// Gets the current thread handle /// Gets the current thread handle
Handle GetCurrentThreadHandle(); Handle GetCurrentThreadHandle();

@ -133,7 +133,7 @@ public:
case FileCommand::Close: case FileCommand::Close:
{ {
LOG_TRACE(Service_FS, "Close %s %s", GetTypeName().c_str(), GetName().c_str()); LOG_TRACE(Service_FS, "Close %s %s", GetTypeName().c_str(), GetName().c_str());
Kernel::g_handle_table.Destroy<File>(GetHandle()); backend->Close();
break; break;
} }
@ -189,7 +189,7 @@ public:
case DirectoryCommand::Close: case DirectoryCommand::Close:
{ {
LOG_TRACE(Service_FS, "Close %s %s", GetTypeName().c_str(), GetName().c_str()); LOG_TRACE(Service_FS, "Close %s %s", GetTypeName().c_str(), GetName().c_str());
Kernel::g_handle_table.Destroy<Directory>(GetHandle()); backend->Close();
break; break;
} }
@ -283,7 +283,8 @@ ResultVal<Handle> OpenFileFromArchive(ArchiveHandle archive_handle, const FileSy
} }
auto file = Common::make_unique<File>(std::move(backend), path); auto file = Common::make_unique<File>(std::move(backend), path);
Handle handle = Kernel::g_handle_table.Create(file.release()); // TOOD(yuriks): Fix error reporting
Handle handle = Kernel::g_handle_table.Create(file.release()).ValueOr(INVALID_HANDLE);
return MakeResult<Handle>(handle); return MakeResult<Handle>(handle);
} }
@ -388,7 +389,8 @@ ResultVal<Handle> OpenDirectoryFromArchive(ArchiveHandle archive_handle, const F
} }
auto directory = Common::make_unique<Directory>(std::move(backend), path); auto directory = Common::make_unique<Directory>(std::move(backend), path);
Handle handle = Kernel::g_handle_table.Create(directory.release()); // TOOD(yuriks): Fix error reporting
Handle handle = Kernel::g_handle_table.Create(directory.release()).ValueOr(INVALID_HANDLE);
return MakeResult<Handle>(handle); return MakeResult<Handle>(handle);
} }

@ -56,7 +56,8 @@ Manager::~Manager() {
/// Add a service to the manager (does not create it though) /// Add a service to the manager (does not create it though)
void Manager::AddService(Interface* service) { void Manager::AddService(Interface* service) {
m_port_map[service->GetPortName()] = Kernel::g_handle_table.Create(service); // TOOD(yuriks): Fix error reporting
m_port_map[service->GetPortName()] = Kernel::g_handle_table.Create(service).ValueOr(INVALID_HANDLE);
m_services.push_back(service); m_services.push_back(service);
} }

@ -54,7 +54,8 @@ public:
/// Allocates a new handle for the service /// Allocates a new handle for the service
Handle CreateHandle(Kernel::Object *obj) { Handle CreateHandle(Kernel::Object *obj) {
Handle handle = Kernel::g_handle_table.Create(obj); // TODO(yuriks): Fix error reporting
Handle handle = Kernel::g_handle_table.Create(obj).ValueOr(INVALID_HANDLE);
m_handles.push_back(handle); m_handles.push_back(handle);
return handle; return handle;
} }
@ -62,7 +63,7 @@ public:
/// Frees a handle from the service /// Frees a handle from the service
template <class T> template <class T>
void DeleteHandle(const Handle handle) { void DeleteHandle(const Handle handle) {
Kernel::g_handle_table.Destroy<T>(handle); Kernel::g_handle_table.Close(handle);
m_handles.erase(std::remove(m_handles.begin(), m_handles.end(), handle), m_handles.end()); m_handles.erase(std::remove(m_handles.begin(), m_handles.end(), handle), m_handles.end());
} }

@ -119,11 +119,9 @@ static Result WaitSynchronization1(Handle handle, s64 nano_seconds) {
// TODO(bunnei): Do something with nano_seconds, currently ignoring this // TODO(bunnei): Do something with nano_seconds, currently ignoring this
bool wait_infinite = (nano_seconds == -1); // Used to wait until a thread has terminated bool wait_infinite = (nano_seconds == -1); // Used to wait until a thread has terminated
if (!Kernel::g_handle_table.IsValid(handle)) { Kernel::Object* object = Kernel::g_handle_table.GetGeneric(handle);
if (object == nullptr)
return InvalidHandle(ErrorModule::Kernel).raw; return InvalidHandle(ErrorModule::Kernel).raw;
}
Kernel::Object* object = Kernel::g_handle_table.GetFast<Kernel::Object>(handle);
_dbg_assert_(Kernel, object != nullptr);
LOG_TRACE(Kernel_SVC, "called handle=0x%08X(%s:%s), nanoseconds=%lld", handle, object->GetTypeName().c_str(), LOG_TRACE(Kernel_SVC, "called handle=0x%08X(%s:%s), nanoseconds=%lld", handle, object->GetTypeName().c_str(),
object->GetName().c_str(), nano_seconds); object->GetName().c_str(), nano_seconds);
@ -150,10 +148,9 @@ static Result WaitSynchronizationN(s32* out, Handle* handles, s32 handle_count,
// Iterate through each handle, synchronize kernel object // Iterate through each handle, synchronize kernel object
for (s32 i = 0; i < handle_count; i++) { for (s32 i = 0; i < handle_count; i++) {
if (!Kernel::g_handle_table.IsValid(handles[i])) { Kernel::Object* object = Kernel::g_handle_table.GetGeneric(handles[i]);
if (object == nullptr)
return InvalidHandle(ErrorModule::Kernel).raw; return InvalidHandle(ErrorModule::Kernel).raw;
}
Kernel::Object* object = Kernel::g_handle_table.GetFast<Kernel::Object>(handles[i]);
LOG_TRACE(Kernel_SVC, "\thandle[%d] = 0x%08X(%s:%s)", i, handles[i], object->GetTypeName().c_str(), LOG_TRACE(Kernel_SVC, "\thandle[%d] = 0x%08X(%s:%s)", i, handles[i], object->GetTypeName().c_str(),
object->GetName().c_str()); object->GetName().c_str());
@ -321,19 +318,12 @@ static Result CreateEvent(Handle* evt, u32 reset_type) {
/// Duplicates a kernel handle /// Duplicates a kernel handle
static Result DuplicateHandle(Handle* out, Handle handle) { static Result DuplicateHandle(Handle* out, Handle handle) {
LOG_WARNING(Kernel_SVC, "(STUBBED) called handle=0x%08X", handle); ResultVal<Handle> out_h = Kernel::g_handle_table.Duplicate(handle);
if (out_h.Succeeded()) {
// Translate kernel handles -> real handles *out = *out_h;
if (handle == Kernel::CurrentThread) { LOG_TRACE(Kernel_SVC, "duplicated 0x%08X to 0x%08X", handle, *out);
handle = Kernel::GetCurrentThreadHandle();
} }
_assert_msg_(KERNEL, (handle != Kernel::CurrentProcess), return out_h.Code().raw;
"(UNIMPLEMENTED) process handle duplication!");
// TODO(bunnei): FixMe - This is a hack to return the handle that we were asked to duplicate.
*out = handle;
return 0;
} }
/// Signals an event /// Signals an event

Loading…
Cancel
Save