Search code examples
c++templatesc++20

How to wrap class method to C function if it exists and to `nullptr` if it do not exist?


I am making generic wrapper of C++ class into C struct with function table. C function table is a part of library public interface and I have no control over it. Some functions can be set to NULL and may have special behavior that can't be achieved with non-null function implementation.

I am attempting to make template function that automatically generate C function table from arbitrary C++ class. If C++ class have no method that correspond to function from C function table, NULL should be set to table entry. How to detect presence of C++ class method and switch between lambda wrapper over method and NULL?

C interface:

struct device_module_info {
    module_info info;

    status_t (*init_device)(void *driverCookie, void **_deviceCookie);
    void (*uninit_device)(void *deviceCookie);
    void (*device_removed)(void *deviceCookie);

    status_t (*open)(void *deviceCookie, const char *path, int openMode,
                    void **_cookie);
    status_t (*close)(void *cookie);
    status_t (*free)(void *cookie);
    status_t (*read)(void *cookie, off_t pos, void *buffer, size_t *_length);
    status_t (*write)(void *cookie, off_t pos, const void *buffer,
                    size_t *_length);
    status_t (*io)(void *cookie, io_request *request);
    status_t (*control)(void *cookie, uint32 op, void *buffer, size_t length);
    status_t (*select)(void *cookie, uint8 event, selectsync *sync);
    status_t (*deselect)(void *cookie, uint8 event, selectsync *sync);
};

Example C++ class to be wrapped:

class DeviceHandleImpl: public ImplBase<DeviceHandle, DeviceHandleImpl, gDeviceImplClass> {
public:
    status_t Close();
    status_t Free();
    status_t Read(off_t pos, void *buffer, size_t *_length);
    status_t Write(off_t pos, const void *buffer, size_t *_length);
    status_t IO(io_request *request);
    status_t Control(uint32 op, void *buffer, size_t length);
    status_t Select(uint8 event, selectsync *sync);
    status_t Deselect(uint8 event, selectsync *sync);
};

device_module_info gDeviceImplClass = GetDeviceClass<
    gDeviceImplClass, gDriverImplClass, DeviceImpl, DeviceHandleImpl
>({
    .name = "testDevice"
});

My current attempt of wrapper generator function:

template <const device_module_info& selfClass, const driver_module_info& driverClass, typename DeviceImpl, typename DeviceHandleImpl>
constexpr device_module_info GetDeviceClass(const module_info& info)
{
    const device_module_info cls = {
        .info = info,
        .init_device = [](void *driverCookie, void **_deviceCookie) {
            Device device;
            status_t res = DeviceImpl::InitDevice({&driverClass, driverCookie}, device);
            *_deviceCookie = device.fInst;
            return res;
        },
        .uninit_device = [](void *deviceCookie) {
            return static_cast<DeviceImpl*>(deviceCookie)->UninitDevice();
        },
        .device_removed = [](void *deviceCookie) {
            return static_cast<DeviceImpl*>(deviceCookie)->DeviceRemoved();
        },
        .open = [](void *deviceCookie, const char *path, int openMode, void **_cookie) {
            DeviceHandle handle;
            status_t res = static_cast<DeviceImpl*>(deviceCookie)->Open(path, openMode, handle);
            *_cookie = handle.fInst;
            return res;
        },
        .close = [](void *cookie) {
            return static_cast<DeviceHandleImpl*>(cookie)->Close();
        },
        .free = [](void *cookie) {
            return static_cast<DeviceHandleImpl*>(cookie)->Free();
        },
        // `DeviceHandleImpl::Read` method may not exist, how to set entry to `nullptr` in this case?
        .read = [](void *cookie, off_t pos, void *buffer, size_t *_length) {
            return static_cast<DeviceHandleImpl*>(cookie)->Read(pos, buffer, _length);
        },
        .write = [](void *cookie, off_t pos, const void *buffer, size_t *_length) {
            return static_cast<DeviceHandleImpl*>(cookie)->Write(pos, buffer, _length);
        },
        .io = [](void *cookie, io_request *request) {
            return static_cast<DeviceHandleImpl*>(cookie)->IO(request);
        },
        .control = [](void *cookie, uint32 op, void *buffer, size_t length) {
            return static_cast<DeviceHandleImpl*>(cookie)->Control(op, buffer, length);
        },
        .select = [](void *cookie, uint8 event, selectsync *sync) {
            return static_cast<DeviceHandleImpl*>(cookie)->Select(event, sync);
        },
        .deselect = [](void *cookie, uint8 event, selectsync *sync) {
            return static_cast<DeviceHandleImpl*>(cookie)->Deselect(event, sync);
        },
    };
    return cls;
}

Solution

  • There are several ways of doing this using requires, depending on how strictly you want to check the method.

    E.g.:

    device_module_info cls{};
    if constexpr (requires(DeviceHandleImpl h, off_t pos, void *buffer, size_t *_length){
        h.Read(pos, buffer, _length);
    })
        cls.read = /*some lambda*/;
    

    This only checks that the method exists and that the argument types are correct. If the return type is wrong, you get a compilation error.

    You could check the return type too. This checks for exact match:

    if constexpr (requires(DeviceHandleImpl h, off_t pos, void *buffer, size_t *_length) {
        requires std::same_as<status_t, decltype(h.Read(pos, buffer, _length))>;
    })
        cls.read = /*some lambda*/;
    

    Or this checks for convertibility:

    if constexpr (requires(DeviceHandleImpl h, off_t pos, void *buffer, size_t *_length) {
        { h.Read(pos, buffer, _length) } -> std::convertible_to<status_t>;
    })
        cls.read = /*some lambda*/;
    

    All of the approaches above allow implicit conversions in arguments. I don't think there's an easy way to prevent that.