582 lines
25 KiB
C++
582 lines
25 KiB
C++
//* Copyright 2017 The NXT Authors
|
|
//*
|
|
//* Licensed under the Apache License, Version 2.0 (the "License");
|
|
//* you may not use this file except in compliance with the License.
|
|
//* You may obtain a copy of the License at
|
|
//*
|
|
//* http://www.apache.org/licenses/LICENSE-2.0
|
|
//*
|
|
//* Unless required by applicable law or agreed to in writing, software
|
|
//* distributed under the License is distributed on an "AS IS" BASIS,
|
|
//* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//* See the License for the specific language governing permissions and
|
|
//* limitations under the License.
|
|
|
|
#include "wire/Wire.h"
|
|
#include "wire/WireCmd.h"
|
|
|
|
#include "common/Assert.h"
|
|
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace nxt {
|
|
namespace wire {
|
|
|
|
//* Client side implementation of the API, will serialize everything to memory to send to the server side.
|
|
namespace client {
|
|
|
|
class Device;
|
|
|
|
struct BuilderCallbackData {
|
|
bool Call(nxtBuilderErrorStatus status, const char* message) {
|
|
if (canCall && callback != nullptr) {
|
|
canCall = true;
|
|
callback(status, message, userdata1, userdata2);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//* For help with development, prints all builder errors by default.
|
|
nxtBuilderErrorCallback callback = nullptr;
|
|
nxtCallbackUserdata userdata1 = 0;
|
|
nxtCallbackUserdata userdata2 = 0;
|
|
bool canCall = true;
|
|
};
|
|
|
|
//* All non-Device objects of the client side have:
|
|
//* - A pointer to the device to get where to serialize commands
|
|
//* - The external reference count
|
|
//* - An ID that is used to refer to this object when talking with the server side
|
|
struct ObjectBase {
|
|
ObjectBase(Device* device, uint32_t refcount, uint32_t id)
|
|
:device(device), refcount(refcount), id(id) {
|
|
}
|
|
|
|
Device* device;
|
|
uint32_t refcount;
|
|
uint32_t id;
|
|
|
|
BuilderCallbackData builderCallback;
|
|
};
|
|
|
|
{% set special_objects = [
|
|
"device",
|
|
"buffer",
|
|
] %}
|
|
{% for type in by_category["object"] if not type.name.canonical_case() in special_objects %}
|
|
struct {{type.name.CamelCase()}} : ObjectBase {
|
|
using ObjectBase::ObjectBase;
|
|
};
|
|
{% endfor %}
|
|
|
|
struct Buffer : ObjectBase {
|
|
using ObjectBase::ObjectBase;
|
|
|
|
~Buffer() {
|
|
//* Callbacks need to be fired in all cases, as they can handle freeing resources
|
|
//* so we call them with "Unknown" status.
|
|
ClearMapRequests(NXT_BUFFER_MAP_ASYNC_STATUS_UNKNOWN);
|
|
|
|
if (mappedData) {
|
|
free(mappedData);
|
|
}
|
|
}
|
|
|
|
void ClearMapRequests(nxtBufferMapAsyncStatus status) {
|
|
for (auto& it : readRequests) {
|
|
it.second.callback(status, nullptr, it.second.userdata);
|
|
}
|
|
readRequests.clear();
|
|
}
|
|
|
|
//* We want to defer all the validation to the server, which means we could have multiple
|
|
//* map request in flight at a single time and need to track them separately.
|
|
//* On well-behaved applications, only one request should exist at a single time.
|
|
struct MapReadRequestData {
|
|
nxtBufferMapReadCallback callback = nullptr;
|
|
nxtCallbackUserdata userdata = 0;
|
|
uint32_t size = 0;
|
|
};
|
|
std::map<uint32_t, MapReadRequestData> readRequests;
|
|
uint32_t readRequestSerial = 0;
|
|
|
|
//* Only one mapped pointer can be active at a time because Unmap clears all the in-flight requests.
|
|
void* mappedData = nullptr;
|
|
};
|
|
|
|
//* TODO(cwallez@chromium.org): Do something with objects before they are destroyed ?
|
|
//* - Call still uncalled builder callbacks
|
|
template<typename T>
|
|
class ObjectAllocator {
|
|
public:
|
|
struct ObjectAndSerial {
|
|
ObjectAndSerial(std::unique_ptr<T> object, uint32_t serial)
|
|
: object(std::move(object)), serial(serial) {
|
|
}
|
|
std::unique_ptr<T> object;
|
|
uint32_t serial;
|
|
};
|
|
|
|
ObjectAllocator(Device* device) : mDevice(device) {
|
|
// ID 0 is nullptr
|
|
mObjects.emplace_back(nullptr, 0);
|
|
}
|
|
|
|
ObjectAndSerial* New() {
|
|
uint32_t id = GetNewId();
|
|
T* result = new T(mDevice, 1, id);
|
|
auto object = std::unique_ptr<T>(result);
|
|
|
|
if (id >= mObjects.size()) {
|
|
ASSERT(id == mObjects.size());
|
|
mObjects.emplace_back(std::move(object), 0);
|
|
} else {
|
|
ASSERT(mObjects[id].object == nullptr);
|
|
//* TODO(cwallez@chromium.org): investigate if overflows could cause bad things to happen
|
|
mObjects[id].serial++;
|
|
mObjects[id].object = std::move(object);
|
|
}
|
|
|
|
return &mObjects[id];
|
|
}
|
|
void Free(T* obj) {
|
|
FreeId(obj->id);
|
|
mObjects[obj->id].object = nullptr;
|
|
}
|
|
|
|
T* GetObject(uint32_t id) {
|
|
if (id >= mObjects.size()) {
|
|
return nullptr;
|
|
}
|
|
return mObjects[id].object.get();
|
|
}
|
|
|
|
uint32_t GetSerial(uint32_t id) {
|
|
if (id >= mObjects.size()) {
|
|
return 0;
|
|
}
|
|
return mObjects[id].serial;
|
|
}
|
|
|
|
private:
|
|
uint32_t GetNewId() {
|
|
if (mFreeIds.empty()) {
|
|
return mCurrentId ++;
|
|
}
|
|
uint32_t id = mFreeIds.back();
|
|
mFreeIds.pop_back();
|
|
return id;
|
|
}
|
|
void FreeId(uint32_t id) {
|
|
mFreeIds.push_back(id);
|
|
}
|
|
|
|
// 0 is an ID reserved to represent nullptr
|
|
uint32_t mCurrentId = 1;
|
|
std::vector<uint32_t> mFreeIds;
|
|
std::vector<ObjectAndSerial> mObjects;
|
|
Device* mDevice;
|
|
};
|
|
|
|
//* The client wire uses the global NXT device to store its global data such as the serializer
|
|
//* and the object id allocators.
|
|
class Device : public ObjectBase {
|
|
public:
|
|
Device(CommandSerializer* serializer)
|
|
: ObjectBase(this, 1, 1),
|
|
{% for type in by_category["object"] if not type.name.canonical_case() == "device" %}
|
|
{{type.name.camelCase()}}(this),
|
|
{% endfor %}
|
|
mSerializer(serializer) {
|
|
}
|
|
|
|
void* GetCmdSpace(size_t size) {
|
|
return mSerializer->GetCmdSpace(size);
|
|
}
|
|
|
|
{% for type in by_category["object"] if not type.name.canonical_case() == "device" %}
|
|
ObjectAllocator<{{type.name.CamelCase()}}> {{type.name.camelCase()}};
|
|
{% endfor %}
|
|
|
|
void HandleError(const char* message) {
|
|
if (errorCallback) {
|
|
errorCallback(message, errorUserdata);
|
|
}
|
|
}
|
|
|
|
nxtDeviceErrorCallback errorCallback = nullptr;
|
|
nxtCallbackUserdata errorUserdata;
|
|
|
|
private:
|
|
CommandSerializer* mSerializer = nullptr;
|
|
};
|
|
|
|
//* Implementation of the client API functions.
|
|
{% for type in by_category["object"] %}
|
|
{% set Type = type.name.CamelCase() %}
|
|
|
|
{% for method in type.methods %}
|
|
{% set Suffix = as_MethodSuffix(type.name, method.name) %}
|
|
|
|
{{as_backendType(method.return_type)}} Client{{Suffix}}(
|
|
{{-as_backendType(type)}} self
|
|
{%- for arg in method.arguments -%}
|
|
, {{as_annotated_backendType(arg)}}
|
|
{%- endfor -%}
|
|
) {
|
|
Device* device = self->device;
|
|
wire::{{Suffix}}Cmd cmd;
|
|
|
|
//* Create the structure going on the wire on the stack and fill it with the value
|
|
//* arguments so it can compute its size.
|
|
{
|
|
//* Value objects are stored as IDs
|
|
{% for arg in method.arguments if arg.annotation == "value" %}
|
|
{% if arg.type.category == "object" %}
|
|
cmd.{{as_varName(arg.name)}} = {{as_varName(arg.name)}}->id;
|
|
{% else %}
|
|
cmd.{{as_varName(arg.name)}} = {{as_varName(arg.name)}};
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
cmd.self = self->id;
|
|
|
|
//* The length of const char* is considered a value argument.
|
|
{% for arg in method.arguments if arg.length == "strlen" %}
|
|
cmd.{{as_varName(arg.name)}}Strlen = strlen({{as_varName(arg.name)}});
|
|
{% endfor %}
|
|
}
|
|
|
|
//* Allocate space to send the command and copy the value args over.
|
|
size_t requiredSize = cmd.GetRequiredSize();
|
|
auto allocCmd = reinterpret_cast<decltype(cmd)*>(device->GetCmdSpace(requiredSize));
|
|
*allocCmd = cmd;
|
|
|
|
//* In the allocated space, write the non-value arguments.
|
|
{% for arg in method.arguments if arg.annotation != "value" %}
|
|
{% set argName = as_varName(arg.name) %}
|
|
{% if arg.length == "strlen" %}
|
|
memcpy(allocCmd->GetPtr_{{argName}}(), {{argName}}, allocCmd->{{argName}}Strlen + 1);
|
|
{% elif arg.length == "constant_one" %}
|
|
memcpy(allocCmd->GetPtr_{{argName}}(), {{argName}}, sizeof(*{{argName}}));
|
|
{% elif arg.type.category == "object" %}
|
|
auto {{argName}}Storage = reinterpret_cast<uint32_t*>(allocCmd->GetPtr_{{argName}}());
|
|
for (size_t i = 0; i < {{as_varName(arg.length.name)}}; i++) {
|
|
{{argName}}Storage[i] = {{argName}}[i]->id;
|
|
}
|
|
{% else %}
|
|
memcpy(allocCmd->GetPtr_{{argName}}(), {{argName}}, {{as_varName(arg.length.name)}} * sizeof(*{{argName}}));
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
//* For object creation, store the object ID the client will use for the result.
|
|
{% if method.return_type.category == "object" %}
|
|
auto* allocation = self->device->{{method.return_type.name.camelCase()}}.New();
|
|
|
|
{% if type.is_builder %}
|
|
//* We are in GetResult, so the callback that should be called is the
|
|
//* currently set one. Copy it over to the created object and prevent the
|
|
//* builder from calling the callback on destruction.
|
|
allocation->object->builderCallback = self->builderCallback;
|
|
self->builderCallback.canCall = false;
|
|
{% endif %}
|
|
|
|
allocCmd->resultId = allocation->object->id;
|
|
allocCmd->resultSerial = allocation->serial;
|
|
return allocation->object.get();
|
|
{% endif %}
|
|
}
|
|
{% endfor %}
|
|
|
|
{% if type.is_builder %}
|
|
void Client{{as_MethodSuffix(type.name, Name("set error callback"))}}({{Type}}* self,
|
|
nxtBuilderErrorCallback callback,
|
|
nxtCallbackUserdata userdata1,
|
|
nxtCallbackUserdata userdata2) {
|
|
self->builderCallback.callback = callback;
|
|
self->builderCallback.userdata1 = userdata1;
|
|
self->builderCallback.userdata2 = userdata2;
|
|
}
|
|
{% endif %}
|
|
|
|
{% if not type.name.canonical_case() == "device" %}
|
|
//* When an object's refcount reaches 0, notify the server side of it and delete it.
|
|
void Client{{as_MethodSuffix(type.name, Name("release"))}}({{Type}}* obj) {
|
|
obj->refcount --;
|
|
|
|
if (obj->refcount > 0) {
|
|
return;
|
|
}
|
|
|
|
obj->builderCallback.Call(NXT_BUILDER_ERROR_STATUS_UNKNOWN, "Unknown");
|
|
|
|
wire::{{as_MethodSuffix(type.name, Name("destroy"))}}Cmd cmd;
|
|
cmd.objectId = obj->id;
|
|
|
|
size_t requiredSize = cmd.GetRequiredSize();
|
|
auto allocCmd = reinterpret_cast<decltype(cmd)*>(obj->device->GetCmdSpace(requiredSize));
|
|
*allocCmd = cmd;
|
|
|
|
obj->device->{{type.name.camelCase()}}.Free(obj);
|
|
}
|
|
|
|
void Client{{as_MethodSuffix(type.name, Name("reference"))}}({{Type}}* obj) {
|
|
obj->refcount ++;
|
|
}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
void ClientBufferMapReadAsync(Buffer* buffer, uint32_t start, uint32_t size, nxtBufferMapReadCallback callback, nxtCallbackUserdata userdata) {
|
|
uint32_t serial = buffer->readRequestSerial++;
|
|
ASSERT(buffer->readRequests.find(serial) == buffer->readRequests.end());
|
|
|
|
Buffer::MapReadRequestData request;
|
|
request.callback = callback;
|
|
request.userdata = userdata;
|
|
request.size = size;
|
|
buffer->readRequests[serial] = request;
|
|
|
|
wire::BufferMapReadAsyncCmd cmd;
|
|
cmd.bufferId = buffer->id;
|
|
cmd.requestSerial = serial;
|
|
cmd.start = start;
|
|
cmd.size = size;
|
|
|
|
size_t requiredSize = cmd.GetRequiredSize();
|
|
auto allocCmd = reinterpret_cast<decltype(cmd)*>(buffer->device->GetCmdSpace(requiredSize));
|
|
*allocCmd = cmd;
|
|
}
|
|
|
|
void ClientBufferMapWriteAsync(Buffer*, uint32_t, uint32_t, nxtBufferMapWriteCallback, nxtCallbackUserdata) {
|
|
// TODO(cwallez@chromium.org): Implement the wire for BufferMapWriteAsync
|
|
ASSERT(false);
|
|
}
|
|
|
|
void ProxyClientBufferUnmap(Buffer* buffer) {
|
|
//* Invalidate the local pointer, and cancel all other in-flight requests that would turn into
|
|
//* errors anyway (you can't double map). This prevents race when the following happens, where
|
|
//* the application code would have unmapped a buffer but still receive a callback:
|
|
//* - Client -> Server: MapRequest1, Unmap, MapRequest2
|
|
//* - Server -> Client: Result of MapRequest1
|
|
//* - Unmap locally on the client
|
|
//* - Server -> Client: Result of MapRequest2
|
|
if (buffer->mappedData) {
|
|
free(buffer->mappedData);
|
|
buffer->mappedData = nullptr;
|
|
}
|
|
buffer->ClearMapRequests(NXT_BUFFER_MAP_ASYNC_STATUS_UNKNOWN);
|
|
|
|
ClientBufferUnmap(buffer);
|
|
}
|
|
|
|
void ClientDeviceReference(Device*) {
|
|
}
|
|
|
|
void ClientDeviceRelease(Device*) {
|
|
}
|
|
|
|
void ClientDeviceSetErrorCallback(Device* self, nxtDeviceErrorCallback callback, nxtCallbackUserdata userdata) {
|
|
self->errorCallback = callback;
|
|
self->errorUserdata = userdata;
|
|
}
|
|
|
|
// Some commands don't have a custom wire format, but need to be handled manually to update
|
|
// some client-side state tracking. For these we have to functions:
|
|
// - An autogenerated Client{{suffix}} method that sends the command on the wire
|
|
// - A manual ProxyClient{{suffix}} method that will be inserted in the proctable instead of
|
|
// the autogenerated one, and that will have to call Client{{suffix}}
|
|
{% set proxied_commands = ["BufferUnmap"] %}
|
|
|
|
nxtProcTable GetProcs() {
|
|
nxtProcTable table;
|
|
{% for type in by_category["object"] %}
|
|
{% for method in native_methods(type) %}
|
|
{% set suffix = as_MethodSuffix(type.name, method.name) %}
|
|
{% if suffix in proxied_commands %}
|
|
table.{{as_varName(type.name, method.name)}} = reinterpret_cast<{{as_cProc(type.name, method.name)}}>(ProxyClient{{suffix}});
|
|
{% else %}
|
|
table.{{as_varName(type.name, method.name)}} = reinterpret_cast<{{as_cProc(type.name, method.name)}}>(Client{{suffix}});
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
return table;
|
|
}
|
|
|
|
class Client : public CommandHandler {
|
|
public:
|
|
Client(Device* device) : mDevice(device) {
|
|
}
|
|
|
|
const uint8_t* HandleCommands(const uint8_t* commands, size_t size) override {
|
|
while (size > sizeof(ReturnWireCmd)) {
|
|
ReturnWireCmd cmdId = *reinterpret_cast<const ReturnWireCmd*>(commands);
|
|
|
|
bool success = false;
|
|
switch (cmdId) {
|
|
case ReturnWireCmd::DeviceErrorCallback:
|
|
success = HandleDeviceErrorCallbackCmd(&commands, &size);
|
|
break;
|
|
{% for type in by_category["object"] if type.is_builder %}
|
|
case ReturnWireCmd::{{type.name.CamelCase()}}ErrorCallback:
|
|
success = Handle{{type.name.CamelCase()}}ErrorCallbackCmd(&commands, &size);
|
|
break;
|
|
{% endfor %}
|
|
case ReturnWireCmd::BufferMapReadAsyncCallback:
|
|
success = HandleBufferMapReadAsyncCallback(&commands, &size);
|
|
break;
|
|
default:
|
|
success = false;
|
|
}
|
|
|
|
if (!success) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
if (size != 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
return commands;
|
|
}
|
|
|
|
private:
|
|
Device* mDevice = nullptr;
|
|
|
|
//* Helper function for the getting of the command data in command handlers.
|
|
//* Checks there is enough data left, updates the buffer / size and returns
|
|
//* the command (or nullptr for an error).
|
|
template<typename T>
|
|
static const T* GetCommand(const uint8_t** commands, size_t* size) {
|
|
if (*size < sizeof(T)) {
|
|
return nullptr;
|
|
}
|
|
|
|
const T* cmd = reinterpret_cast<const T*>(*commands);
|
|
|
|
size_t cmdSize = cmd->GetRequiredSize();
|
|
if (*size < cmdSize) {
|
|
return nullptr;
|
|
}
|
|
|
|
*commands += cmdSize;
|
|
*size -= cmdSize;
|
|
|
|
return cmd;
|
|
}
|
|
|
|
bool HandleDeviceErrorCallbackCmd(const uint8_t** commands, size_t* size) {
|
|
const auto* cmd = GetCommand<ReturnDeviceErrorCallbackCmd>(commands, size);
|
|
if (cmd == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
if (cmd->GetMessage()[cmd->messageStrlen] != '\0') {
|
|
return false;
|
|
}
|
|
|
|
mDevice->HandleError(cmd->GetMessage());
|
|
|
|
return true;
|
|
}
|
|
|
|
{% for type in by_category["object"] if type.is_builder %}
|
|
{% set Type = type.name.CamelCase() %}
|
|
bool Handle{{Type}}ErrorCallbackCmd(const uint8_t** commands, size_t* size) {
|
|
const auto* cmd = GetCommand<Return{{Type}}ErrorCallbackCmd>(commands, size);
|
|
if (cmd == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
if (cmd->GetMessage()[cmd->messageStrlen] != '\0') {
|
|
return false;
|
|
}
|
|
|
|
auto* builtObject = mDevice->{{type.built_type.name.camelCase()}}.GetObject(cmd->builtObjectId);
|
|
uint32_t objectSerial = mDevice->{{type.built_type.name.camelCase()}}.GetSerial(cmd->builtObjectId);
|
|
|
|
//* The object might have been deleted or a new object created with the same ID.
|
|
if (builtObject == nullptr || objectSerial != cmd->builtObjectSerial) {
|
|
return true;
|
|
}
|
|
|
|
bool called = builtObject->builderCallback.Call(static_cast<nxtBuilderErrorStatus>(cmd->status), cmd->GetMessage());
|
|
|
|
// Unhandled builder errors are forwarded to the device
|
|
if (!called && cmd->status != NXT_BUILDER_ERROR_STATUS_SUCCESS && cmd->status != NXT_BUILDER_ERROR_STATUS_UNKNOWN) {
|
|
mDevice->HandleError(("Unhandled builder error: " + std::string(cmd->GetMessage())).c_str());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
{% endfor %}
|
|
|
|
bool HandleBufferMapReadAsyncCallback(const uint8_t** commands, size_t* size) {
|
|
const auto* cmd = GetCommand<ReturnBufferMapReadAsyncCallbackCmd>(commands, size);
|
|
if (cmd == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
auto* buffer = mDevice->buffer.GetObject(cmd->bufferId);
|
|
uint32_t bufferSerial = mDevice->buffer.GetSerial(cmd->bufferId);
|
|
|
|
//* The buffer might have been deleted or recreated so this isn't an error.
|
|
if (buffer == nullptr || bufferSerial != cmd->bufferSerial) {
|
|
return true;
|
|
}
|
|
|
|
//* The requests can have been deleted via an Unmap so this isn't an error.
|
|
auto requestIt = buffer->readRequests.find(cmd->requestSerial);
|
|
if (requestIt == buffer->readRequests.end()) {
|
|
return true;
|
|
}
|
|
|
|
auto request = requestIt->second;
|
|
// Delete the request before calling the callback otherwise the callback could be fired a second time if for example buffer.Unmap() is called inside the callback.
|
|
buffer->readRequests.erase(requestIt);
|
|
|
|
//* On success, we copy the data locally because the IPC buffer isn't valid outside of this function
|
|
if (cmd->status == NXT_BUFFER_MAP_ASYNC_STATUS_SUCCESS) {
|
|
|
|
//* The server didn't send the right amount of data, this is an error and could cause
|
|
//* the application to crash if we did call the callback.
|
|
if (request.size != cmd->dataLength) {
|
|
return false;
|
|
}
|
|
|
|
if (buffer->mappedData != nullptr) {
|
|
return false;
|
|
}
|
|
buffer->mappedData = malloc(request.size);
|
|
memcpy(buffer->mappedData, cmd->GetData(), request.size);
|
|
|
|
request.callback(static_cast<nxtBufferMapAsyncStatus>(cmd->status), buffer->mappedData, request.userdata);
|
|
} else {
|
|
request.callback(static_cast<nxtBufferMapAsyncStatus>(cmd->status), nullptr, request.userdata);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
CommandHandler* NewClientDevice(nxtProcTable* procs, nxtDevice* device, CommandSerializer* serializer) {
|
|
auto clientDevice = new client::Device(serializer);
|
|
|
|
*device = reinterpret_cast<nxtDeviceImpl*>(clientDevice);
|
|
*procs = client::GetProcs();
|
|
|
|
return new client::Client(clientDevice);
|
|
}
|
|
|
|
}
|
|
}
|