dart-lang/sdk

[ffi] Nested structs

pingbird opened this issue ยท 17 comments

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.

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.

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.

Yes indeed, that is the workaround for now.

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.

@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.

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

marad commented

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

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 commented

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?)

@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 commented

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?

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;
}

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 commented

Thank you @janzka! You made my day ๐Ÿ˜€

marad commented

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;
}

Any updates on this?

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