-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[vm/ffi] Unwrap extension types to their representation types #54944
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
Comments
NativeType
s in type argumentsNativeType
s in FFI type arguments
@halildurmus Can you detail a bit more about the use case where this is useful? |
This comment was marked as duplicate.
This comment was marked as duplicate.
Thanks for the clear explanation! ❤️ (And thanks for continuing development on package:win32!) So if I understand you correctly, you only want to use extension types as type markers in C function signatures and as type arguments to As for the question of marker types, I wonder if extension types are the right solution yes or no. One alternative is to generate Both of these solutions will work today. Though, there might be downsides to doing this that I haven't thought of. WDYT? Any thoughts, ideas? (Side note: We have |
Thanks!
Exactly.
Yes. Some Win32 APIs directly accept native integer types and structs by value. As you said, given that we can't create values of
I couldn't understand this option. Let's say the |
I meant to say @AbiSpecificIntegerMapping({
Abi.windowsArm64: Int32(),
Abi.windowsIA32: Int32(),
Abi.windowsX64: Int32(),
})
final class APTTYPE extends AbiSpecificInteger {
const APTTYPE();
} |
Right, static members should be fine. I can make a fix for that. Are all the types that you want to wrap with documentation integer types? |
No. I also want to wrap native string types PSTR ( There is an important difference between PWSTR and BSTR types. BSTR has a four-byte integer prefix that contains the number of bytes in the following data string. This means one can't use the I came up with this example using extension types: import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart' hide BSTR;
extension type const PWSTR(Pointer<Utf16> _) implements Pointer<Utf16> {
PWSTR.fromString(String string) : this(string.toNativeUtf16());
void free() => calloc.free(this);
}
extension type const BSTR(Pointer<Utf16> _) implements Pointer<Utf16> {
factory BSTR.fromString(String string) {
final psz = string.toNativeUtf16();
final pbstr = SysAllocString(psz);
calloc.free(psz);
return BSTR(pbstr);
}
void free() => SysFreeString(this);
BSTR clone() => BSTR(SysAllocString(this));
int get byteLength => SysStringByteLen(this);
int get length => SysStringLen(this);
BSTR operator +(BSTR other) {
final pbstrResult = calloc<Pointer<Utf16>>().cast<BSTR>();
VarBstrCat(this, other, pbstrResult);
final result = BSTR(pbstrResult.value);
calloc.free(pbstrResult);
return result;
}
}
void foo(PWSTR pwstr) {
print(pwstr.toDartString());
}
void bar(BSTR bstr) {
print(bstr.toDartString());
}
void baz(Pointer<PWSTR> pwstr) {
print(pwstr.value.toDartString());
}
void main() {
final pwstr = PWSTR.fromString('I am a happy PWSTR');
foo(pwstr);
pwstr.free();
final bstr = BSTR.fromString('I am a happy BSTR');
bar(bstr);
bstr.free();
final pwstr2 = PWSTR.fromString('I am a happy PWSTR2');
final ppwstr = calloc<Pointer<Utf16>>().cast<PWSTR>()..value = pwstr2;
baz(ppwstr);
pwstr2.free();
calloc.free(ppwstr);
} I must say, I love this! I think using extension types to wrap these types is the way to go. It's much more powerful compared to the AbiSpecificIntegers. WDYT?
No, I don't think so. |
You could define a new Your API would be centered around Would this work? I kind of like the extension types as a general wrapper method, because if we allow it, it would work for any native type anywhere in the FFI. I am slightly worried that it might conflict with us wanting to migrate the @lrhn @mkustermann Any thoughts about allowing users to wrap any
The only use case I can think of is custom code generators. Users using |
We have 2 different kinds of types:
For some kinds they differ: e.g. integer & float types The fundamental reason we have this distinction is because we could not represent the C types in dart (e.g. differentiate between integer types). We may be able to bridge this gap in a future redesign of the FFI (e.g. using extension types). Extension types are a way to add type-safety (separate type, mostly explicit conversion to/from representatation type) and behavior (methods on the extension types) to a dart representation type. Both of these features are much more relevant to the Dart objects that code interacts with and not much relevant for C type annotations (see distinction above). So I'm not sure whether it makes any sense to use them on the C-only types (as the title of this issue says: "Allow extension types that implement NativeTypes in FFI type arguments"). To me it makes much more sense to use them to represent the Dart objects the programmer interacts with. In terms of future-proofing the FFI: One can have extension types on other extension types. That means if we changed The extension types do have the implicit constructor which isn't allowed to have any user-written logic in it. Which makes it fit into FFI, as we would "construct" those objects (not really, as they are desugared) internally in the C interop. Representing enums extension type const AptType(int value) {
static const AptType current = AptType(0);
...
}
@Native<Void Function(Int16)>()
external void foo(AptType arg);
// If we ever make `Int16` an extension type on `int`, `AptType` could become extension type on `Int16`
@Native<Void Function(AptType)>()
external void foo(AptType arg);
// ... or the shorter form
@Native()
external void foo(AptType arg);
class MyStruct extends Struct {
@Int16()
external AptType value;
// If we ever make `Int16` an extension type on `int`, `AptType` could become extension type on `Int16`
external AptType value;
} Representing special strings (which are pointers) extension type const PWSTR(Pointer<Utf16> _) {
PWSTR.fromString(String string) ...;
void free() => calloc.free(this);
}
@Native<Pointer<Utf16> Function(Pointer<Utf16>, Pointer<Utf16>)>()
external PWSTR concat(PWSTR, PWSTR);
// But we could allow
@Native<PWSTR Function(PWSTR, PWSTR)>()
external PWSTR concat(PWSTR, PWSTR);
// ... which would allow
@Native()
external PWSTR concat(PWSTR, PWSTR);
class MyStruct extends Struct {
external PWSTR value; // Reason to allow PWSTR in native & dart function types (see above)
} The difficulty starts when one would like to use a pointer to a type that extension on Pointer<AptType> {
AptType get value = AptType(this.cast<Int16>().value);
void set value(AptType value) => this.cast<Int16>().value = (value as int);
}
@Native<Void Function(Pointer<AptType>)>()
void foo(Pointer<AptType> arg);
// Or simply
@Native()
void foo(Pointer<AptType> arg);
class MyStruct extends Struct {
external Pointer<AptType> value;
} This doesn't work as Now, strictly speaking we could remove the bound on If we made An alternative to the last point above would be an This ability to use typed objects to represent enums without runtime cost (i.e. extensions on integers) seem also very appealing to use by So overall I think making the FFI static analysis / FFI kernel transformer simply unwrap all extension types to their representation types before doing the checking would seem fine to me. |
Interesting!! I'm just wondering, would these ideas have some synergy (or the opposite) with dart-lang/language#3614? That's a proposal to allow some extension types to be assignable from their representation type: extension type Int32(int _) implements int {}
extension type AptType(Int32 _) implements Int32 {}
Int32 i32 = 42; // OK.
AptType aptType = i32; // Could be OK if the proposal is generalized slightly.
AptType aptType2 = 42; // Perhaps --- that would be yet another generalization.
To me this sounds like it could erase some typing distinctions that we'd want to maintain. Would it be sufficient to erase the extension type when it's used to select a C type? |
NativeType
s in FFI type arguments
The original request looks like a non-issue to allow. If you do If you do If you do So, to allow it, the extension type must implement a concrete native type that could be used somewhere, and must also have a representation type that is a subtype of that type. I don't think there are many useful type hierarchies where among native types, so that probably just means implementing the representation type. Wrapping pointers is probably where the fun is. extension type BStr(Pointer<Uint16> _) implements Pointer<Uint16> {
int get length => _.cast<Uint32>().value;
Uint16 operator[](int index) => _[2 + IndexError.check(index, length)];
} I'm not sure I'd make that type implement (Can you wrap an About making the native types themselves be extensions, that's precisely the kind of thing extension types are made for, so it's no surprise it seems like a good fit. The places where it might fall short is that extension types are not good at being abstract supertypes. I'd probably use real classes for the sealed class NativeType {}
sealed class NativeIntegerType extends NativeType< {}
final class Uint32 extends NativeIntegerType {
static const int size = 4;
}
final class Uint16 extends NativeIntegerType {}
// ...
extension type Pointer<T extends NativeType>(int address) {
Pointer<R> cast<R extends NativeType>() => Pointer<R>(address);
bool operator <(Pointer<NativeType> other) => address < other.address;
bool operator <=(Pointer<NativeType> other) => address <= other.address;
bool operator >(Pointer<NativeType> other) => address > other.address;
bool operator >=(Pointer<NativeType> other) => address >= other.address;
}
extension Uint32Pointer on Pointer<Uint32> {
int get value => _readUint32(address);
set value(int value) { _writeUint32(address, value); }
Pointer<Uint32> operator +(int offset) => Pointer<Uint32>(address + offset * Uint32.size);
Pointer<Uint32> operator -(int offset) => Pointer<Uint32>(address - offset * Uint32.size);
int distance(Pointer<Uint32> other) => (other.address - this.address) ~/ Uint32.size;
int operator [](int offset) => (this + offset).value;
operator []=(int offset, int value) { (this + offset).value = value; }
} (The |
Just to clarify, the pointer for the BSTR type points to the first character of the data string, not to the length prefix. So your example should be modified as follows: import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
extension type BStr(Pointer<Uint16> _) implements Pointer<Uint16> {
int get length =>
Pointer<Uint32>.fromAddress(address - sizeOf<Uint32>()).value;
}
void main() {
final bstr = BStr(SysAllocString('Hello, world!'.toNativeUtf16()).cast());
print(bstr.length); // 26 = 13 * sizeOf<Uint16>() (excluding the terminator)
print(bstr[0]); // 72 (H)
print(bstr[12]); // 33 (!)
print(bstr[13]); // 0 (NUL)
} Also, Win32 API provides String Manipulation Functions for working with BSTR types. Here's a simple example that uses these APIs: import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
extension type BStr(Pointer<Uint16> _) implements Pointer<Uint16> {
factory BStr.fromString(String string) {
final psz = string.toNativeUtf16();
final pbstr = SysAllocString(psz);
calloc.free(psz);
return BStr(pbstr.cast());
}
int get length => SysStringLen(this.cast());
void free() => SysFreeString(this.cast());
} |
TL;DR: I would like to use extension types like
typedef
for native types in function signatures to provide users with clearer information and thus enable easier consumption of Win32 APIs within Dart.I'm currently working on
v6
of thepackage:win32
with a focus on enhancing the user experience when using the Win32 APIs.Let's take a specific example with the CoGetApartmentType function, which is defined in C as follows:
In
package:win32
, this function is projected into Dart as follows:However, it's not immediately clear to the user what the
pAptType
andpAptQualifier
parameters represent. Users need to refer back to the Microsoft documentation to understand that they are meant to beAPTTYPE
andAPTTYPEQUALIFIER
enums. Even if I chose to represent these enums astypedef
s, they would still seePointer<Int32>
type when they hover over the function (issue #39332) and I wouldn't be able to define enum values.(I should also note that currently, the projected APIs in
package:win32
are manually defined in a JSON file, including the C function definition as documentation, which is extracted from the Microsoft documentation website. However, inv6
, instead of manually defining the projected APIs in a JSON file, I plan to project the entire Win32 API set (excluding a few ancient APIs) using Microsoft's metadata. Consequently, users won't see these manually added C definitions when they hover over the function in their IDE. Instead, they'll just see the function signatureint CoGetApartmentType(Pointer<Int32> pAptType, Pointer<Int32> pAptQualifier)
, which isn't very helpful.)To address this, I would like to project Win32 enums like
APTTYPE
andAPTTYPEQUALIFIER
as Dart extension types, as shown below:With this approach, users will see the exact enum types (e.g.,
Pointer<APTTYPE>
) when hovering over the function, instead of seeingPointer<Int32>
, and they'll be able to allocate memory for the enums usingcalloc
, as demonstrated below:cc @dcharkes
The text was updated successfully, but these errors were encountered: