deno icon indicating copy to clipboard operation
deno copied to clipboard

feat(ext/ffi): structs by value

Open DjDeveloperr opened this issue 2 years ago • 4 comments

Adds support for passing and returning structs as buffers to FFI. Needed for certain system APIs such as AppKit on macOS

DjDeveloperr avatar Jul 03 '22 19:07 DjDeveloperr

@littledivy PTAL

DjDeveloperr avatar Jul 07 '22 00:07 DjDeveloperr

Can tests be added for passing structs by value?

vimirage avatar Jul 26 '22 06:07 vimirage

I do think we should actually be checking the buffer for size validity. Trying to pass a 0 size buffer as an eg. ["u32"] struct should error, I think.

aapoalas avatar Jul 27 '22 04:07 aapoalas

@littledivy please take a look. @DjDeveloperr could you please rebase?

bartlomieju avatar Sep 03 '22 13:09 bartlomieju

@littledivy @aapoalas PTAL

DjDeveloperr avatar Oct 31 '22 07:10 DjDeveloperr

Can tests be added for passing structs by value?

Er, we need nested struct tests.

Furthermore, recursive structs should be rejected.

vimirage avatar Nov 01 '22 23:11 vimirage

Furthermore, recursive structs should be rejected.

Seems to panic when deserializing in Rust land. Should this be handled in JS side? I think it's very edge case, we don't really need a check for this.

DjDeveloperr avatar Nov 02 '22 07:11 DjDeveloperr

Furthermore, recursive structs should be rejected.

Seems to panic when deserializing in Rust land. Should this be handled in JS side? I think it's very edge case, we don't really need a check for this.

Yes; panicking represents a bug - panics shouldn't actually be reached. I expected that this would panic, that's why I mentioned it.

vimirage avatar Nov 02 '22 07:11 vimirage

Shouldn't this be actually handled in serde_v8? Its overflowing stack when deserializing this object.

DjDeveloperr avatar Nov 02 '22 07:11 DjDeveloperr

Shouldn't this be actually handled in serde_v8? Its overflowing stack when deserializing this object.

Yup, that's the issue. Alternatively, you could accept v8 values and check for cycles using that, but... this is definitely a serde_v8 issue that is waiting to be opened.

vimirage avatar Nov 02 '22 07:11 vimirage

Alternatively, you could accept v8 values and check for cycles using that, but... this is definitely a serde_v8 issue that is waiting to be opened.

So, do I need to implement a workaround for this bug of serde_v8 right now?

DjDeveloperr avatar Nov 02 '22 08:11 DjDeveloperr

How does ownership of the buffer work? Are we copying the buffer?

When passing buffer as struct argument, we directly pass pointer from backing store. Libffi doesn't seem to take any ownership of it. Buffer is probably internally copied only once, like pushed onto stack at time of call or something according to ABI.

See: https://github.com/denoland/deno/pull/15060#discussion_r1009281028

When returning struct we pre allocate buffer in JS side and pass its pointer to libffi so return value can be written to it.

DjDeveloperr avatar Nov 03 '22 05:11 DjDeveloperr

Buffer is probably internally copied only once, like pushed onto stack at time of call or something according to ABI.

Ah ok that makes sense. libffi pushes the struct onto the stack.

What happens when structs are too big? On aarch64, pass-by-value structs large enough are actually passed by reference.

From https://devblogs.microsoft.com/oldnewthing/20220823-00/?p=107041 "The AArch64 processor (aka arm64), part 20: The classic calling convention"

If a parameter is a large structure (larger than 16 bytes), then it is passed by address. Small structures are passed by value, packed into integer registers. (Execption: If the small structure consists entirely of floats or entirely of doubles, then it is passed in floating point registers, one for each member.)

littledivy avatar Nov 03 '22 12:11 littledivy

What happens when structs are too big? On aarch64, pass-by-value structs large enough are actually passed by reference.

I'm not sure how libffi handles this internally, but it would be pretty weird (and problematic) if it is taking ownership of arguments only in certain cases.

DjDeveloperr avatar Nov 03 '22 15:11 DjDeveloperr

https://github.com/python/cpython/blob/bb8b931385ba9df4e01f7dd3ce4575d49f60efdf/Modules/_ctypes/callproc.c#L1238-L1250

#ifdef CTYPES_PASS_BY_REF_HACK
        size_t size = atypes[i]->size;
        if (IS_PASS_BY_REF(size)) {
            void *tmp = alloca(size);
            if (atypes[i]->type == FFI_TYPE_STRUCT)
                memcpy(tmp, args[i].value.p, size);
            else
                memcpy(tmp, (void*)&args[i].value, size);

            avalues[i] = tmp;
        }
        else
#endif

We need to do this workaround for large structs. Code from cpython above. libffi issue: https://github.com/libffi/libffi/issues/694

littledivy avatar Nov 04 '22 02:11 littledivy

https://github.com/python/cpython/blob/bb8b931385ba9df4e01f7dd3ce4575d49f60efdf/Modules/_ctypes/callproc.c#L1238-L1250

#ifdef CTYPES_PASS_BY_REF_HACK
        size_t size = atypes[i]->size;
        if (IS_PASS_BY_REF(size)) {
            void *tmp = alloca(size);
            if (atypes[i]->type == FFI_TYPE_STRUCT)
                memcpy(tmp, args[i].value.p, size);
            else
                memcpy(tmp, (void*)&args[i].value, size);

            avalues[i] = tmp;
        }
        else
#endif

We need to do this workaround for large structs. Code from cpython above. libffi issue: https://github.com/libffi/libffi/issues/694

Does Deno support any Risc-V platforms? If not, x64 and Aarch64 are already supported: https://github.com/libffi/libffi/pull/678.

vimirage avatar Nov 04 '22 14:11 vimirage

Found an issue where the typings were not working for a struct return value like this:

export const CXCursorT = { struct: ["u8", "int32", "pointer", "pointer", "pointer"] } as const;

Or I think it wasn't working, at least. Worth a check, possibly also worth a test to see if the types can handle structs with varying types of members.

aapoalas avatar Nov 16 '22 08:11 aapoalas

Also how is struct padding defined? Right now it is only "packed" structs IIUC

littledivy avatar Nov 22 '22 13:11 littledivy

Also how is struct padding defined? Right now it is only "packed" structs IIUC

It's only C repr structs, non-packed. libffi does not give any options for structs, they're C repr always. Doing a packed struct needs to be done manually with u8s.

aapoalas avatar Nov 22 '22 14:11 aapoalas

Oh i misunderstood the PR, the calls seems to accept the raw buffer from the user instead of a JS object. We could make this inbuilt and have JIT unwrapping for object -> buffer conversion?

call({ x: 1, y : 2 });

// call(...) codegen:
const u8 = new Uint8Array(size);
function call(obj) {
  const { x, y } = obj;
  u8[0] = x;
  u8[1] = y;
  ffi_call(u8)
}

I think this would be unnecessary / can be done in user-land fairly easily. From my libclang bindings where I'm using this branch I'm often not at all interested in the struct internals, the C API just returns structs and accepts the same structs as parameters so I'm passing the Uint8Array back and forth.

aapoalas avatar Nov 22 '22 14:11 aapoalas

Oh i misunderstood the PR, the calls seems to accept the raw buffer from the user instead of a JS object. We could make this inbuilt and have JIT unwrapping for object -> buffer conversion?


call({ x: 1, y : 2 });



// call(...) codegen:

const u8 = new Uint8Array(size);

function call(obj) {

  const { x, y } = obj;

  u8[0] = x;

  u8[1] = y;

  ffi_call(u8)

}

I don't think we should do this, at least only for passing structs. As AapoAlas stated like it is easily doable in user land. If we do however add struct serialization, it should be complete and also support deserialization (e.g for returning). It's also useful for APIs that expect structs by pointer too. But I think that's for another PR.

DjDeveloperr avatar Nov 28 '22 05:11 DjDeveloperr

I want to land this, let's get the CI green. @DjDeveloperr Do you need help with something?

Passing structs in nonblocking calls doesn't seem to work, I couldn't find the reason though. In normal sync calls, struct is passed correctly by value but in nonblocking calls the struct value is actually a pointer to another pointer of struct's memory, which is weird because both type of calls use same function for parsing struct arguments.

After some debugging, found that when we pass the same struct buffer as argument to both sync & async calls, literally same libffi Arg structs are created (which is expected). But I don't get why at the time of call the value is becoming like that only in async calls.

Also, the windows fastci is failing for unknown reason...

EDIT: Commented out the test case for it. CI is green now but it's still a bug. Seems windows fastci had seg fault because of this (unlike on Linux where I tested, it had no segmentation fault).

DjDeveloperr avatar Dec 04 '22 12:12 DjDeveloperr

LGTM.

aapoalas avatar Jan 06 '23 20:01 aapoalas