Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C Blocks API #116

Open
litherum opened this issue Nov 16, 2021 · 16 comments
Open

C Blocks API #116

litherum opened this issue Nov 16, 2021 · 16 comments
Labels
non-breaking Does not require a breaking change (that would block V1.0)

Comments

@litherum
Copy link

litherum commented Nov 16, 2021

C blocks are a more convenient way of handling callbacks. They automatically capture the relevant variables from the enclosing scope.

This:

WGPU_EXPORT void wgpuAdapterRequestDevice(WGPUAdapter adapter, WGPUDeviceDescriptor const * descriptor, WGPURequestDeviceCallback callback, void * userdata);

could turn into this:

WGPU_EXPORT void wgpuAdapterRequestDevice(WGPUAdapter adapter, WGPUDeviceDescriptor const * descriptor, WGPURequestDeviceCallback callback, void * userdata);
#if defined(__BLOCKS__)
WGPU_EXPORT void wgpuAdapterRequestDeviceWithBlock(WGPUAdapter adapter, WGPUDeviceDescriptor const * descriptor, void (^callback)(WGPURequestDeviceStatus status, WGPUDevice device, char const * message))
#endif

(Of course, typedefs could make this more readable.)

@Kangz
Copy link
Collaborator

Kangz commented Nov 16, 2021

Blocks are a non-standard C extension so I don't think they should be in the header. Can they be decayed into a function and a void*? This would make it fairly easy to write a companion header with support for blocks.

The current header is auto-generated with Dawn's code generators so it would be relatively easy to add a code generator for webgpu_with_blocks.h.

@litherum
Copy link
Author

Can they be decayed into a function and a void*

Yes. Support for blocks would be a convenience.

@kainino0x
Copy link
Collaborator

Can they be decayed into a function and a void*

Yes. Support for blocks would be a convenience.

Can you point to documentation on how to decay a block into a function pointer and void*? I tried to find information online but I wasn't able to find anything at all.

The current header is auto-generated with Dawn's code generators so it would be relatively easy to add a code generator for webgpu_with_blocks.h.

I think if we do this we could even explicitly make it Objective-C (a .h/.m pair where the .m converts blocks to function+userdata pointer).

(Though at that point we've just created Objective-C WebGPU bindings. Which is good! 🙂)

@doug-moen
Copy link

Can you point to documentation on how to decay a block into a function pointer and void*?

One approach is to cast a pointer to the block to void* and use a fixed function that converts the void* back to a block before calling it.

@kainino0x
Copy link
Collaborator

Excellent point, I found people doing the opposite, but didn't think about that!

@litherum
Copy link
Author

litherum commented Jan 22, 2023

Ping about adding a blocks API? Would be super useful...

@Kangz
Copy link
Collaborator

Kangz commented Jan 27, 2023

We could have a header for ObjC extensions but it's not clear how to do it:

Can you point to documentation on how to decay a block into a function pointer and void*? I tried to find information online but I wasn't able to find anything at all.

@litherum
Copy link
Author

(to be clear, blocks are not an ObjC extension; they're a C extension)

@litherum
Copy link
Author

It certainly is possible to do as suggested above, but real first-class blocks support would definitely be convenient.

To be honest, I'm surprised at the amount of hesitation here; this is a non-breaking change, that is invisible from the perspective of someone who doesn't have the extension enabled in their compiler. It seems like only upside to me.

@kainino0x
Copy link
Collaborator

(to be clear, blocks are not an ObjC extension; they're a C extension)

IIUC they're, somewhat separately, an Objective-C built-in feature, a C extension, and a C++ extension. I think it's a fair question whether we should support niche C extensions, or if we should just have Objective-C bindings instead.

It certainly is possible to do as suggested above, but real first-class blocks support would definitely be convenient.

To be honest, I'm surprised at the amount of hesitation here; this is a non-breaking change, that is invisible from the perspective of someone who doesn't have the extension enabled in their compiler. It seems like only upside to me.

I read it as being a concern not from the user side, but from the implementation side. If it's in the header then all implementations need to implement it (when __BLOCKS__ is available).

That said there is a strategy suggested above and it does seem to work.

// modified from sample on wikipedia

#include <stdio.h>
#include <Block.h>

typedef int (^IntBlock)();
typedef int (*IntFunction)(void*);

IntBlock MakeCounter(int start, int increment) {
    __block int i = start;
    
    return Block_copy( ^(void) {
        int ret = i;
        i += increment;
        return ret;
    });
}

int callCCallback(IntFunction fn, void* userdata) {
    return fn(userdata);
}

int runBlock(void* block_ptr) {
    const IntBlock block = (IntBlock) block_ptr;
    return block();
}

int main(void) {
    IntBlock mycounter = MakeCounter(5, 2);

    printf("Call 1: %d\n", callCCallback(runBlock, mycounter));
    printf("Call 2: %d\n", callCCallback(runBlock, mycounter));
    printf("Call 3: %d\n", callCCallback(runBlock, mycounter));
    
    Block_release(mycounter);
    return 0;
}
Call 1: 5
Call 2: 7
Call 3: 9

@kainino0x
Copy link
Collaborator

I think it's a fair question whether we should support niche C extensions, or if we should just have Objective-C bindings instead.

Forgot to say also: Since this won't affect existing entrypoints (would need new entrypoints) there's even less need for blocks in this header, vs just having a webgpu_objc.h/.m library similar to Dawn's webgpu_cpp.h/cpp.

@litherum
Copy link
Author

litherum commented Jun 3, 2023

Using this approach requires manual memory management - explicit calls to Block_copy() and Block_release(), which is something of an antipattern in modern code. It's also not type-safe, as you have to cast the void* to the appropriate block type, which is error-prone.

I used this approach in WebKit/WebKit#14651. It works for the places where there's a one-shot callback (like mapAsync()) because it's clear where to call Block_copy() and Block_release():

  • The callsite to mapAsync() should Block_copy() the callback block to be run
  • The C function should run the callback, and then Block_release() it

However, this doesn't work for multi-use callbacks, like wgpuDeviceSetUncapturedErrorCallback(). Here, the C function can't Block_release() the block, because the block may need to be run again in the future.

The way this works with blocks is that the Device understands the ownership of the block, and so when the Device is destroyed, it can invoke Block_release() itself on its retained block.

It used to be possible to work around this by having whichever object owns the WGPUDevice also retain the uncaptured error block, and release it at the same time as it releases the device. But this doesn't work any more now that WGPU objects are all reference-counted. There is no single owner, so there is no way for an observer to know when the WGPUDevice actually dies and it's safe to Block_release() the callback.

@kainino0x
Copy link
Collaborator

Using this approach requires manual memory management - explicit calls to Block_copy() and Block_release(), which is something of an antipattern in modern code. It's also not type-safe, as you have to cast the void* to the appropriate block type, which is error-prone.

IIUC this only affects bindings. I think it's a non-issue to have to get the memory management right in the bindings code, which only has to be written once. It doesn't seem any different from having to get the memory management right in the webgpu.h implementation (of which there will be at least 3 - more than there will need to be objective-C or Clang C bindings).

I think this can be handled in bindings just fine even in the UncapturedError case. Fundamentally, the C bindings have to provide the info needed to memory manage UncapturedError's userdata (which, here, is the Block itself), by guaranteeing that the UncapturedError callback is never called after the DeviceLost callback. So in the DeviceLost callback you can free the userdata (here, the Block). The bindings can listen for a DeviceLost callback even if the Objective-C application didn't ask for one.

@litherum
Copy link
Author

Is the DeviceLost callback supposed to fire even when the device dies of natural causes (its refcount goes to 0)?

@Kangz
Copy link
Collaborator

Kangz commented Jun 15, 2023

At least that's what happens in Dawn at least with WGPUDeviceLostReason_Destroyed.

@kainino0x
Copy link
Collaborator

All callbacks except UncapturedError are guaranteed to fire "eventually", so that the lifetime of the userdata pointer can be managed. In this case it's less clear to me because dropping the device doesn't necessarily mean it'll be freed - it may still have internal refs from other objects. But I'd say yes, it will get called, at least no later than when the actual internal refcount does reach 0 (you've cleaned up everything that references the device).

@kainino0x kainino0x added the non-breaking Does not require a breaking change (that would block V1.0) label Aug 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
non-breaking Does not require a breaking change (that would block V1.0)
Projects
None yet
Development

No branches or pull requests

4 participants