Creating an opaque handle type

Opaque handles are used to refer to an object, but are themselves not an object. They are often implemented in terms of pointers to a type that user code knows nothing about or a wrapped integer and can only be used by passing the handle to library-provided functions.

How to do it…

We will execute the following steps to create an opaque handle type:

  1. Create a private struct with no data members and no methods in the interface to represent the opaque type. If you are implementing the library, you may use members and methods internally, but do not expose them to the public interface.
  2. Either use alias Impl* Handle; or create a public struct with only one data member—a pointer to the private struct or the handle ID if you use integers—to represent the handle. Mark this member private. The alias method works better when interfacing with C libraries that do not use a handle struct. Otherwise, the struct gives more control.
  3. Disable built-in functions on the public struct as desired to force the user to go through your library. You may disable the default constructor if it needs special initialization and/or postblit if you don't want the handle copied.
  4. Provide public functions in the module that operate upon the only handle in their interface. Whenever possible, make the handle the first argument to the function.
  5. Be certain that the same method, handle or pointer, is used in the library too. If the library is implemented with a naked pointer instead of a handle struct, also use the naked pointer at the usage point.

The code is as follows:

private struct OpaqueImpl{} // step 1
alias OpaqueImpl* OpaqueHandle; // step 2, pointer method
// or step 2, struct method
// struct OpaqueHandle{ private OpaqueImpl* item; }
// interface functions
OpaqueHandle createObject();
void freeObject(OpaqueHandleimpl);
void add(OpaqueHandleimpl, int);
void print(OpaqueHandleimpl);
void main() {
    // usage:
    auto handle = createObject();
    scope(exit) freeObject(handle);
    handle.add(10); // call using UFCS
    handle.print();
}

How it works…

Opaque handles are a technique to maximize binary compatibility. Since the user code doesn't need to know anything about the size or layout of the data, the library is free to change its implementation details at any time. It can add functions, add data, or remove data without breaking the client code. Opaque handles are also useful for interlanguage compatibility, since they only require an agreed method to pass basic data and call functions.

With D, we can use this technique directly and also build upon it, while maintaining similar syntax to other library writing techniques. There are four D features used here: alias, UFCS, an empty struct, and wrapper structs.

alias is a feature to give an entity an alternative name. It comes in two mostly equivalent forms: alias oldnamenewname; or alias newname = oldname;. Aliases can hide implementation details (the handle is a pointer, but this is not immediately visible at the usage point) and provide more specific names for your entities, but do not create a new type. This is why we defined the empty struct instead of simply alias void* OpaqueHandle;—the latter form would allow undesired implicit conversions since it does not define a unique type. This is similar to typedef in C++. D used to have typedef too, but it was deprecated in favor of the struct+alias combination to give better control over details. In Phobos, std.typecons.Typedef encapsulates this pattern in the standard library.

Next, we used an empty struct. On the implementation side, this struct would be populated with data; however, on the user interface side, it is empty to hide its details. In D, the implementation and interface may be separated logically using protection attributes or physically by writing two files, one with just declarations and the other with implementations (similar to header files in C or C++). The interface file may be automatically generated with dmd –H. Of course, the implementation may also be written in another language.

Note

The .di files, meaning D interface files, may be hand-written or automatically generated with dmd –H. As far as the compiler is concerned, there is no difference between the .d and .di files—you may write implementations in a .di file or just write interfaces in a .d file. Partially as a result of this, an implementation file cannot import its corresponding interface file. The compiler will think the module is attempting to import itself.

Your first thought when declaring an opaque struct might be to write it without a body: struct OpaqueImpl; instead of struct OpaqueImpl {}. We chose the latter here because then the compiler will believe it to have no members instead of being a forward declaration of a struct with an unknown number of members. The advantage of this is we will avoid compiler errors when we try to pass it to functions using method call syntax (UFCS). The compiler will confidently believe it has no members and let you call other functions through it without error. The downside is that a user could also declare OpaqueImpl when it has an empty body, but we are able to prohibit that by simply making the structure private.

Lastly, if we can use a wrapper struct instead of an alias, we have the advantage of being able to add or disable methods, making the handle automatically reference counted, destroyed, or not null, as we've already done with other types in this chapter. If you are writing a library, use a wrapper struct. There's little runtime cost and it has the most potential for growth and control. If you are accessing an existing library, however, be sure to match the method they used. While wrapper structs are almost identical to the wrapped type on a binary level, there are subtle differences in passing them in return values. If you wrap a type you do not control, you should also wrap the functions that use that type.

There's more…

We looked briefly at the preceding alias syntax. There are two forms; what are the differences between them? Both work the same way; the only difference is the alias old new syntax (inherited from C) has slightly more compatibility with the function pointer attributes and the alias new = old syntax also functions as shorthand for a template, which lets you easily do pass-through or transformations of an argument list. This can be seen in the following code:

// the following two lines are equivalent
alias TemplateArgList(T…) = T;
template TemplateArgList(T…) { alias TemplateArgList = T; }

This is not possible with the other syntax. On the other hand, the C syntax allows you to define names that include function linkage and other attributes:

// this line will not compile with the equals based syntax
alias extern(C) void function() CallbackType;

When interfacing with C, this syntax can help define function pointers with the right linkage (calling convention) and attributes like nothrow. These differences may disappear with time. Currently, outside of these two cases, the syntax you use comes down to personal preference.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.12.156