Munich-Quantum-Software-Stack / QDMI

Quantum Device Management Interface (QDMI)
https://munich-quantum-software-stack.github.io/QDMI/
Apache License 2.0
26 stars 0 forks source link

🎨 Prefixing Device Functions #73

Closed ystade closed 3 weeks ago

ystade commented 1 month ago

Prefixing the functions of a device would have the following advantages:

Overall, most changes should involve the driver. However, the device implementations also need to include the header in a way that supports those prefixes.

mnfarooqi commented 4 weeks ago

Would the device vendor need an ABC_QDMI_query... because it will have a different function signature than QDMI_query...? If so, it would be better to change the signature of QDMI_query... or introduce a new QDMI_query_xyz....

burgholzer commented 4 weeks ago

Would the device vendor need an ABC_QDMI_query... because it will have a different function signature than QDMI_query...? If so, it would be better to change the signature of QDMI_query... or introduce a new QDMI_query_xyz....

See https://github.com/Munich-Quantum-Software-Stack/QDMI/pull/86 for a proof-of-concept realization (still in flux/draft) ;-)

ystade commented 3 weeks ago

In the following, I add some lessons learned while experimenting with different options in #86.

Lessons Learned

Include header "shifted"

It is possible to substitute symbols in a header by previously defining substitution rules, such as

#define <old symbol> <new symbol>

In that way, a generic "substitution" header can be defined, see device_shifted.h.in that contains a placeholder for the prefix that can be replaced by CMake's configure_file(...). When this header is included in a device, the symbols are all name-shifted by the specified prefix.

Problem

The problem with that solution is, that the device_shifted.h.in header transitively includes qdmi/device.h with the substitutions applied. When all devices are linked statically together into one driver, the issue arises that the substitution rules for the first device that is compiled are applied since the header qdmi/device.h will only be included once due to the preprocessor directive #pragma once.

Use of objcopy

Another solution to rename the symbols in the compiled device libraries to rename the symbols after compilation in the object files using objcopy on Linux, and ld with the option -alias_list in Mac OS, see https://github.com/Munich-Quantum-Software-Stack/QDMI/blob/9b475de8fc5fe3dc13c57091cf10fcee22191a40/examples/device/unprefixed_c/CMakeLists.txt#L19 https://github.com/Munich-Quantum-Software-Stack/QDMI/blob/9b475de8fc5fe3dc13c57091cf10fcee22191a40/examples/device/unprefixed_c/CMakeLists.txt#L28

With this approach, no source code, neither in the header nor in the source code, must be touched for the device implementations. To this end, when statically linking all devices together into one library, the header that must be included is always the same and it suffices that this is included only once aligning with #pragma once.

Problem

While the function symbols can be renamed with objcopy or ld, respectively, that is not possible for types defined by the devices. Since every device must define their type for the Job, Site, and Operation this leads to multiple definitions for QDMI_Job, QDMI_Site. and QDMI_Operation. This issue cannot be resolved using objcopyor ld. The only way would be to define the previously mentioned types as void*. However, this could lead to type unsafe implementations.

Use Wrapper around device types

One way to avoid having the void* mentioned in the previous section could be to define a struct in the driver behind the QDMI_Job that contains a void pointer to the QDMI_Job of the respective device together with a pointer to the actual device. It would be the job of the driver then to cast the object behind the void pointer to the correct data-type for the respective device. This way, the driver would need to deal with more complexity but the client and driver could still safely use types for jobs, sites, and operations.

Problem

As pointed out in #83 , this approach causes problems in information returned from the device. For example, when querying the coupling map, the device needs to refer to the sites. The device has only access to its representation of a site and will return those to refer to certain sites. The client receiving this information (unfiltered—as the driver does (and should) not know what information is passed there) sees the objects of the site's type defined by the device. However, the client only knows the site's representation returned from the driver, which is the wrapper around the device's sites.

Potential Solution

As pointed out in the previous section, it is unavoidable for devices to have their own implementation of the jobs, sites, and operations, to define them under different names–and this must happen already on the source code level and cannot be achieved by applying objcopy afterward. To facilitate the different naming of the types (and function symbols), the qdmi header must be modified (prepending the prefix) before they are included in a device's implementation. To circumvent the problem described in the first section, the header files must be copied to follow the constraint that one header is only included once. Having this copy also allows the modification of this copy in place, which makes the substitution rules, as described in the first section, superfluous.

The devices can include their modified header that defines all types and symbols implemented in the device prepended with a custom prefix. The device must then use the prefix for every function and type. By this, also the (static) driver knows the signature (including the types of parameters) from the device's functions and can cast the arguments accordingly.

For an implementation of that check out #86 .