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

Open
PixelToast opened this issue Jun 15, 2019 · 13 comments
Open

[ffi] Nested structs #37271

PixelToast opened this issue Jun 15, 2019 · 13 comments

Comments

@PixelToast
Copy link

@PixelToast PixelToast commented Jun 15, 2019

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.

@dcharkes

This comment has been minimized.

Copy link
Contributor

@dcharkes dcharkes commented Jun 17, 2019

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.

@dcharkes dcharkes added this to Flutter MVP in Dart VM FFI via automation Jun 17, 2019
@dcharkes dcharkes moved this from Flutter MVP to 1.0 in Dart VM FFI Jun 17, 2019
@PixelToast

This comment has been minimized.

Copy link
Author

@PixelToast PixelToast commented Jun 18, 2019

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

This comment has been minimized.

Copy link
Contributor

@dcharkes dcharkes commented Jun 18, 2019

Yes indeed, that is the workaround for now.

@timsneath

This comment has been minimized.

Copy link
Member

@timsneath timsneath commented Sep 21, 2019

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

@sjindel-google sjindel-google commented Sep 23, 2019

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

@marad

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Dart VM FFI
  
1.0
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
8 participants
You can’t perform that action at this time.