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

[ffi] Nested structs #37271

Closed
pingbird opened this issue Jun 15, 2019 · 17 comments
Closed

[ffi] Nested structs #37271

pingbird opened this issue Jun 15, 2019 · 17 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-ffi

Comments

@pingbird
Copy link

Right now it isn't possible to represent nested structs using ffi, for example the following C data structure:

struct Foo {
    int foo;
};

struct Bar {
    struct Foo foo;
    int bar;
};

In Dart:

@struct class Foo extends Pointer<Void> {
    @Uint32() int foo;
}

@struct class Bar extends Pointer<Void> {
    Foo foo;
    @Uint32() int bar;
}

Does not compile.

@dgrove dgrove added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-ffi labels Jun 16, 2019
@dcharkes
Copy link
Contributor

Hey @PixelToast, at the moment we only support pointers to structs inside structs, see https://github.com/dart-lang/sdk/blob/master/samples/ffi/coordinate.dart#L18-L19.

We do want to support nested (by value) structs in the future, but we have to change the structs API first, see #37229.

@pingbird
Copy link
Author

It's possible to implement nested structs without by-value structs, for example:

struct Foo {
    uint32_t foo;
};

struct Bar {
    uint32_t bar;
    struct Foo foo;
};

in dart:

@struct class Foo extends Pointer<Void> {
    @Uint32() int foo;
}

@struct class Bar extends Pointer<Void> {
  @Uint32() int bar;
  @Uint32() int _padding;
  Foo get foo => offsetBy(4).cast<Foo>();
  set foo(Foo other) {
    foo.foo = other.foo;
  }
}

Works fine, but requires you to know the alignment, size, and offset of Foo.

@dcharkes
Copy link
Contributor

Yes indeed, that is the workaround for now.

@timsneath
Copy link
Contributor

This is a pretty common pattern in Win32 APIs, for example CONSOLE_SCREEN_BUFFER_INFO_EX, which wraps a number of structs. Now that #37229 seems to be done, this would be good to consider.

@dcharkes
Copy link
Contributor

dcharkes commented Sep 23, 2019

@timsneath

Yeah, @mkustermann suggested last week that this might be the next thing to work on (with more urgency than finalizers).

We can take a look at this next.

@sjindel-google
Copy link
Contributor

I think we should prioritize finalizers more, because nested structs can be worked around in a way that finalizers cannot.

@marad
Copy link

marad commented Jan 1, 2020

Hey @PixelToast I'm trying to use SendInput function which uses INPUT struct. I've created something like what you suggested:

class KeyboardInput extends Struct {
  @Uint32() int type;
  @Uint16() int virtualCode;
  @Uint16() int scanCode;
  @Uint32() int flags;
  @Uint32() int time;
  Pointer dwExtraInfo;
  @Uint32() int padding;
}

Unfortunately this does not work with the SendInput function. Additionally I don't understand why sizeOf<KeyboardInput>() gives me size of 32 instead of 28 (but with padding field removed it correctly says 24).

I'd appreciate any help with this.

I also tried just allocating Pointer<Uint8> of size 28 and manually set all the bytes. Still didn't work (but it worked in C++). I've described it in more depth on my post on Reddit

@dcharkes
Copy link
Contributor

dcharkes commented Jan 2, 2020

Additionally I don't understand why sizeOf<KeyboardInput>() gives me size of 32 instead of 28

We conflated size and alignment in the API (back when we did not have structs at all). I've filed an issue to address this.

I even checked the byte representation of the struct in Dart and in C++ and they are exact same bytes.

That is quite curious. Do you have the source code for the reproduction somewhere?

@marad
Copy link

marad commented Jan 3, 2020

I actually do. I created custom DLL with custom implementation of the SendInput function which simply printed all bytes recived for the message and also each field's value.

What I discovered was that actually the struct should use more 64-bit values:

class KeyboardInput extends Struct {
  @Uint64() int type; 
  @Uint16() int virtualCode;
  @Uint16() int scanCode;
  @Uint64() int flags;
  @Uint64() int time;
  Pointer dwExtraInfo;
}

This SOMEWHAT works, but not really. Take a look at the code and the comments:

import 'dart:ffi';
import 'package:ffi/ffi.dart';

const INPUT_KEYBOARD = 1;
const KEYEVENTF_UNICODE = 0x0004;

DynamicLibrary _user32 = DynamicLibrary.open("user32.dll");
DynamicLibrary _kernel32 = DynamicLibrary.open("kernel32.dll");

typedef _SendInput_C = Uint32 Function(Uint32 cInputs, Pointer pInputs, Uint32 cbSize);
typedef _SendInput_Dart = int Function(int cInputs, Pointer pInputs, int cbSize);
var SendInput = _user32.lookupFunction<_SendInput_C, _SendInput_Dart>("SendInput");

typedef _GetLastError_C = Uint32 Function();
typedef _GetLastError_Dart = int Function();
var GetLastError = _kernel32.lookupFunction<_GetLastError_C, _GetLastError_Dart>("GetLastError");

class KeyboardInput extends Struct {
  @Uint64() int type; 
  @Uint16() int virtualCode;
  @Uint16() int scanCode;
  @Uint64() int flags;
  @Uint64() int time;
  Pointer dwExtraInfo;

  factory KeyboardInput.allocate({int vkey=0, int scan=0, int flags=0}) =>
    allocate<KeyboardInput>().ref
      ..type = INPUT_KEYBOARD
      ..virtualCode = vkey
      ..scanCode = scan
      ..flags = flags
      ..time = 0
      ..dwExtraInfo = nullptr
      ;
}

void main() {
  var zKeyVirtualCoe = 0x5a;
  var event = KeyboardInput.allocate(vkey: zKeyVirtualCoe);
  // this loop will send less than 10 'z' characters (sometimes even zero)
  for(var i=0; i < 10; i++) { 
    print('Sending $i-th virtual character');
    var written = SendInput(1, event.addressOf, 40);
    print('Written $written');
    print('Error ${GetLastError()}');
  }


  // this does not work at all (despite the fact that it says 'written 1' - so it sent something)
  var unicode = KeyboardInput.allocate(scan: 'x'.codeUnitAt(0), flags: KEYEVENTF_UNICODE);
  for(var i=0; i < 10; i++) {
    print('Sending $i-th unicode character');
    var written = SendInput(1, unicode.addressOf, 40); // note the size of 40 instead of 28
    print('Written $written');
    print('Error ${GetLastError()}');
  }

  print('Struct size: ${sizeOf<KeyboardInput>()}');
}

I run this program 4 times and this are the results:

zzzzzzzzz // first time
zzzzzzzz // second time
 // third time (no output at all!)
zzzzzzz // fourth time

What I don't understand now is:

  • why C++ version I did before said it was only 28 bytes?
  • why this implementation with virtual key code sometimes work and sometimes does not?
  • why the unicode version does not work at all? (maybe I'm doing something wrong?)

@Hexer10
Copy link

Hexer10 commented Jan 3, 2020

@marad I've setup a WindowsHook and ran your code a couple of times and the result was indeed quite incosistent:

1:

Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255
Pressed key: 255

2:

Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0

3:

Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0
Pressed key: 0

4:

Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90
Pressed key: 90

@marad
Copy link

marad commented Feb 7, 2020

I'm starting to suspect that there is something funky going on with 32bit vs 64bit.

I've created a simple DLL library that contains only function mySendInput which has exactly the same arguments and return value that windows SendInput has. It does nothing but print the data that was sent to it.

I originally intended to use it with my dart code to see what's going on, but dart can only load the 64bit version. When I try to load the 32bit version it crashes with:

Unhandled exception:
Invalid argument(s): Failed to load dynamic library (193)
#0      _open (dart:ffi-patch/ffi_dynamic_library_patch.dart:13)
#1      new DynamicLibrary.open (dart:ffi-patch/ffi_dynamic_library_patch.dart:22)
#2      _user32 (file:///c:/dev/personal/dart_send_input/dart/test.dart:8)
#3      _user32 (file:///c:/dev/personal/dart_send_input/dart/test.dart:8)
#4      SendInput (file:///c:/dev/personal/dart_send_input/dart/test.dart:13)
#5      SendInput (file:///c:/dev/personal/dart_send_input/dart/test.dart:13)
#6      main (file:///c:/dev/personal/dart_send_input/dart/test.dart:44)
#7      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:307)
#8      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174)

This error means that it cannot load the 32bit version and I get it, but... I've also created a simple C++ code to test it with original SendInput as well as my test one:

#include <windows.h>
#include <iostream>
using namespace std;

typedef UINT (__cdecl *SENDINPUTPROC)(UINT cInputs, LPINPUT pInputs, int cbSize);

int WINAPI WinMain (HINSTANCE hThisInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR lpszArgument,
                     int nCmdShow)
{
    //HINSTANCE user32 = LoadLibrary("user32.dll");
    HINSTANCE user32 = LoadLibrary("TestDLL32.dll");
    SENDINPUTPROC MySendInput;
    if (user32 != NULL) {
        cout << "Library loaded!" << endl;
        INPUT input;
        input.type = 1;
        input.ki.wVk = 0x5a;
        input.ki.wScan = 0;
        input.ki.dwExtraInfo = 0;
        input.ki.dwFlags = 0;
        input.ki.time = 0;
        MySendInput = (SENDINPUTPROC) GetProcAddress(user32, "mySendInput");
        MySendInput(1, &input, sizeof(input));
    } else {
        cout << "Library not loaded!" << endl;
        return 1;
    }

    return 0;
}

And this loaded my 32bit DLL just fine, but couldn't load the 64bit one. The strange thing is that when I simply load the user32.dll, both solutions work. Is window doing some behind-the-courtains magic so that both can load it?

@janzka
Copy link

janzka commented Mar 30, 2020

I've managed to get SendInput to work consistently with:

class KEYBDINPUT extends Struct {
  @Uint32() int type;
  @Uint32() int _padding;
  @Uint16() int wVk;
  @Uint16() int wScan;
  @Uint32() int dwFlags;
  @Uint32() int time;
  Pointer dwExtraInfo;
  @Uint64() int _padding2;
}

@timsneath
Copy link
Contributor

timsneath commented Mar 30, 2020

Thanks @janzka -- this one is non-intuitive. I've added SendInput to my Win32 wrapper package, which folk can find here: https://pub.dev/packages/win32. There's also a sample in the example directory.

@marad
Copy link

marad commented Apr 4, 2020

Thank you @janzka! You made my day 😀

@marad
Copy link

marad commented Apr 8, 2020

For completeness I'll include also the mouse input struct that I manged to get working (also non-intuitive):

class MouseInput extends Struct {
  @Uint32() int type;
  @Uint32() int _padding;
  @Uint32() int dx;
  @Uint32() int dy;
  @Uint32() int mouseData;
  @Uint32() int flags;
  @Uint32() int time;
  Pointer extraInfo;
}

jpnurmi added a commit to jpnurmi/assimp.dart that referenced this issue Aug 26, 2020
Many structs end up empty, because dart:ffi does not yet support nested
structs: dart-lang/sdk#37271

```
Running in Directory: '/home/jpnurmi/Projects/dart_assimp'
[WARNING]: Prefer adding Key 'description' to your config.
Input Headers: [/usr/include/assimp/cexport.h, /usr/include/assimp/cfileio.h, /usr/include/assimp/cimport.h, /usr/include/assimp/postprocess.h, /usr/include/assimp/scene.h, /usr/include/assimp/version.h]
[WARNING]: Removed All Struct Members from aiRay(aiRay), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiExportDataBlob(aiExportDataBlob), Nested Structures not supported.
[WARNING]: Skipped Function 'aiGetPredefinedLogStream', struct pass/return by value not supported.
[WARNING]: Removed All Struct Members from aiTexture(aiTexture), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiAABB(aiAABB), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiBone(aiBone), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiAnimMesh(aiAnimMesh), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMesh(aiMesh), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiLight(aiLight), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiCamera(aiCamera), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiUVTransform(aiUVTransform), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMaterialProperty(aiMaterialProperty), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiVectorKey(aiVectorKey), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiQuatKey(aiQuatKey), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiNodeAnim(aiNodeAnim), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMeshAnim(aiMeshAnim), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMeshMorphAnim(aiMeshMorphAnim), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiAnimation(aiAnimation), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiNode(aiNode), Nested Structures not supported.
Finished, Bindings generated in /home/jpnurmi/Projects/dart_assimp/lib/src/bindings.dart
```
jpnurmi added a commit to jpnurmi/assimp.dart that referenced this issue Aug 26, 2020
Many structs end up empty, because dart:ffi does not yet support nested
structs: dart-lang/sdk#37271

```
Running in Directory: '/home/jpnurmi/Projects/dart_assimp'
[WARNING]: Prefer adding Key 'description' to your config.
Input Headers: [/usr/include/assimp/cexport.h, /usr/include/assimp/cfileio.h, /usr/include/assimp/cimport.h, /usr/include/assimp/postprocess.h, /usr/include/assimp/scene.h, /usr/include/assimp/version.h]
[WARNING]: Removed All Struct Members from aiRay(aiRay), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiExportDataBlob(aiExportDataBlob), Nested Structures not supported.
[WARNING]: Skipped Function 'aiGetPredefinedLogStream', struct pass/return by value not supported.
[WARNING]: Removed All Struct Members from aiTexture(aiTexture), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiAABB(aiAABB), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiBone(aiBone), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiAnimMesh(aiAnimMesh), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMesh(aiMesh), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiLight(aiLight), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiCamera(aiCamera), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiUVTransform(aiUVTransform), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMaterialProperty(aiMaterialProperty), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiVectorKey(aiVectorKey), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiQuatKey(aiQuatKey), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiNodeAnim(aiNodeAnim), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMeshAnim(aiMeshAnim), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiMeshMorphAnim(aiMeshMorphAnim), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiAnimation(aiAnimation), Nested Structures not supported.
[WARNING]: Removed All Struct Members from aiNode(aiNode), Nested Structures not supported.
Finished, Bindings generated in /home/jpnurmi/Projects/dart_assimp/lib/src/bindings.dart
```
@provokateurin
Copy link

Any updates on this?

@dcharkes
Copy link
Contributor

We're currently working on #36730, which is a prerequisite for this.

@dcharkes dcharkes added this to the January 2021 milestone Dec 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-ffi
Projects
None yet
Development

No branches or pull requests

9 participants