Back

Nov 21, 2024

Nov 21, 2024

How to Extract Device Information with Dart FFI on Windows

Learn how to extract device information on Windows using Dart FFI. Follow code examples to effectively access and utilize device data in your Dart applications.

I’m Rob Vrooman, a software engineer here at Pieces for Developers. My main responsibilities are to develop and maintain plugins and integrations that interface with our core application. In my recent projects, I’ve been exploring the capabilities of Dart’s Foreign Function Interface (FFI) to interact with native Windows libraries. This journey has led me to delve into obtaining detailed device information using dxgi.dll. These tools have proven invaluable for accessing low-level system details and understanding development with Dart FFI.


Setting the Stage

The primary goal of this project is to gather device information so that the Pieces application can recommend the most powerful local Large Language Model (LLM) able to run effectively on the user's system. Much of our existing codebase is written in Dart, and by leveraging Dart’s FFI, I can call native C functions and interact with system libraries.


Extracting Device Information with Dart FFI on Windows

Setup

First, in your Dart project's pubspec.yaml file, be sure to include the latest 'ffi' package. If you will be working with Windows APIs, it is also recommended that you use the 'win32' package.

    dependencies:
        ffi: ^2.1.3
        win32: ^5.7.1

Using dxgi.dll for Device Information

dxgi.dll (DirectX Graphics Infrastructure) provides a set of APIs to enumerate graphics adapters and outputs. By interfacing with this DLL, I am able to retrieve detailed information about the video controllers installed on the system, such as the adapter description, memory size, and driver version.

Implementation Details

Often, when I read blogs, I think to myself, "Where is the code??". To not bury the lead, here is what the dxgi.dll query ended up looking like:

GPUInfo? _dxgiQuery(int deviceIndex) {
    GPUInfo? gpuInfo = null;
    using((Arena arena) {
        final pFactory = arena<Pointer<win32.COMObject>>();
        int hresult = dxgi.CreateDXGIFactory1(dxgi.IID_IDXGIFactory4, pFactory.cast());
        if (win32.SUCCEEDED(hresult)) {
            final pAdapter = arena<Pointer<win32.COMObject>>();
            dxgi.IDXGIFactory1Object factory = dxgi.IDXGIFactory1Object(pFactory.cast());
            hresult = factory.EnumAdapters1(deviceIndex, pAdapter.cast());
            if (win32.SUCCEEDED(hresult)) {
            dxgi.IDXGIAdapter1Object dxAdapter = dxgi.IDXGIAdapter1Object(pAdapter.cast());
            final deviceDesc = arena<dxgi.DXGI_ADAPTER_DESC>();
            hresult = dxAdapter.GetDesc(deviceDesc);
            if (win32.SUCCEEDED(hresult)) {
                List<int> charCodes = [];
                // Max 128 chars in Description
                for (int i = 0; i < 128; i++) {
                int char = deviceDesc.ref.Description[i];
                // Null terminated string.
                if (char == win32.NULL) break;
                    charCodes.add(char);
                }
                gpuInfo = GPUInfo(
                deviceIndex: deviceIndex,
                type: String.fromCharCodes(charCodes),
                dedicatedVRam: ByteInfo(value: deviceDesc.ref.DedicatedVideoMemory),
                );
            } else {
                print('DeviceIndex: $deviceIndex FAILED: GetDesc');
            }
            } else {
            print('DeviceIndex: $deviceIndex FAILED: EnumAdapters1');
            }
        } else {
            print('DeviceIndex: $deviceIndex FAILED: CreateDXGIFactory1');
        }
    });
    return gpuInfo;
}

Let's go into more detail about what exactly is going on here.

GPUInfo

GPUInfo? gpuInfo = null;

A plain old Dart object meant to hold the extracted data.

Arena

using((Arena arena) {
    // ... scoped code
 }

The Arena class in dart FFI is basically a helper utility that ensures allocated memory is freed upon exiting the scope. Behind the scenes, it can use malloc or calloc to perform the allocation. The default allocator is calloc.

CreateDXGIFactory1

    final pFactory = arena<Pointer<win32.COMObject>>();
    int hresult = dxgi.CreateDXGIFactory1(dxgi.IID_IDXGIFactory4, pFactory.cast());
    if (win32.SUCCEEDED(hresult)) {
        // ... proceed if success
    }

First, we allocate the necessary memory for a pointer to a win32 COMObject as required by CreateDXGIFactory1.

The CreateDXGIFactory1 function creates a factory that can be used to generate other DXGI objects. In our case, we're looking to create an IDXGIFactory object to be able to iterate the available graphics adapters with the EnumAdapters function.

Setting up dart functions to call these functions through FFI looks something like this:

// Load the DXGI library
final dxgi = DynamicLibrary.open('dxgi.dll');
final CreateDXGIFactory1 = 
         // lookup the native function symbol.
         // Be sure that the generic for the lookupFunction call matches the signature of the native function.
    dxgi.lookupFunction<win32.HRESULT Function(REFIID, Pointer<Pointer<Void>>),
        // Convert the native function into a compatible dart function.
        // 'CreateDXGIFactory1' matches the function name of the native function.
        int Function(REFIID, Pointer<Pointer<Void>>)>('CreateDXGIFactory1');

With the factory function call setup, we are able to enumerate the adapters:

Enumerating Adapters

final pAdapter = arena<Pointer<win32.COMObject>>();
dxgi.IDXGIFactory1Object factory = dxgi.IDXGIFactory1Object(pFactory.cast());
hresult = factory.EnumAdapters1(deviceIndex, pAdapter.cast());
if (win32.SUCCEEDED(hresult)) {
     // ... proceed if success
}

Again, we start by allocating the required memory buffer. Next, we create our IDXGIFactory1Object from the Void Pointer received from the original create factory call.

Where'd that come from? Here's where it gets a bit more complex: to call functions that are part of an object and class layout, we need to understand which memory location to point to: 

class IDXGIFactory1Object extends win32.IUnknown {
    IDXGIFactory1Object(super.ptr);
    int EnumAdapters1(int Adapter, Pointer<Pointer<Void>> ppAdapter) =>
        // Use the VTable layout to obtain a pointer to the native function
        (ptr.ref.vtable + 12)
                .cast<Pointer<NativeFunction<win32.HRESULT Function(Pointer, Uint32, Pointer<Pointer<Void>>)>>>()
                .value
                // Translate the native function to a dart function
                .asFunction<int Function(Pointer, int, Pointer<Pointer<Void>>)>()
            // Call the function
            (ptr.ref.lpVtbl, Adapter, ppAdapter);
    }

VTables and the like deserve a blog unto themselves, but for this case, I can simply say that the layout can be dumped by your compiler. In my case, I used the command:

cl /d1reportAllClassLayout <fileName>.cpp

For a clang class layout dump of:

//IDXGIFactory1::$vftable@:
// | &IDXGIFactory1_meta
// |  0
//  0 | &IUnknown::QueryInterface
//  1 | &IUnknown::AddRef
//  2 | &IUnknown::Release
//  3 | &IDXGIObject::SetPrivateData
//  4 | &IDXGIObject::SetPrivateDataInterface
//  5 | &IDXGIObject::GetPrivateData
//  6 | &IDXGIObject::GetParent
//  7 | &IDXGIFactory::EnumAdapters
//  8 | &IDXGIFactory::MakeWindowAssociation
//  9 | &IDXGIFactory::GetWindowAssociation
// 10 | &IDXGIFactory::CreateSwapChain
// 11 | &IDXGIFactory::CreateSoftwareAdapter
// 12 | &IDXGIFactory1::EnumAdapters1
// 13 | &IDXGIFactory1::IsCurrent

Notice how the EnumAdapters1 function has an offset number of '12', the same as used in the code above. The Dart FFI library handles offsets with memory size calculations, so you can use this number directly.

Now, starting from 0 and incrementing the device index until failure, we can retrieve information about the adapters.

Adapter Descriptions

dxgi.IDXGIAdapter1Object dxAdapter = dxgi.IDXGIAdapter1Object(pAdapter.cast());
final deviceDesc = arena<dxgi.DXGI_ADAPTER_DESC>();
hresult = dxAdapter.GetDesc(deviceDesc);
if (win32.SUCCEEDED(hresult)) {
   // ... proceed if success
}

What's new here? Now that we have a Void Pointer that's meant to be an IDXGIAdapter1, we create a new object from that pointer. Since the pattern of how this is accomplished has already been discussed, I will leave it out for brevity.

GPUInfo!

List<int> charCodes = [];
// Max 128 chars in Description
for (int i = 0; i < 128; i++) {
    int char = deviceDesc.ref.Description[i];
    // Null terminated string.
    if (char == win32.NULL) break;
    charCodes.add(char);
}
gpuInfo = GPUInfo(
    deviceIndex: deviceIndex,
    type: String.fromCharCodes(charCodes),
    dedicatedVRam: ByteInfo(value: deviceDesc.ref.DedicatedVideoMemory),
);

We made it! We have the data! 

There's one more important piece here to go over. How is DXGI_ADAPTER_DESC defined in our Dart code? This structure must match the expected memory layout as shown in the documentation:

base class DXGI_ADAPTER_DESC extends Struct {
    @Array<win32.WCHAR>(128)
    external Array<win32.WCHAR> Description;
    @Uint32()
    external int VendorId;
    @Uint32()
    external int DeviceId;
    @Uint32()
    external int SubSysId;
    @Uint32()
    external int Revision;
    @Uint64()
    external int DedicatedVideoMemory;
    @Uint64()
    external int DedicatedSystemMemory;
    @Uint64()
    external int SharedSystemMemory;
    external LUID AdapterLuid;
}
base class LUID extends Struct {


Conclusion

This project has been an incredible learning experience, providing deep insights into both Dart’s capabilities and the intricacies of Windows system programming. By combining Dart with FFI and leveraging dxgi.dll, I was able to unlock powerful functionalities to obtain device information, which in turn lets us provide the most well-optimized generative AI experience for our users within Pieces.

Written by

Written by

Rob Vrooman

Rob Vrooman

SHARE

SHARE

Title

Title

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.