Wrapping C(++) APIs with Custom Managed Records
July 20, 2020 by Erik van Bilsen
July 20, 2020 by Erik van Bilsen
[SHOWTOGROUPS=4,20]
Delphi 10.4 introduced Custom Managed Records. In this post we show how you can use this new language feature to wrap third party C(++) APIs in an easy to use model. We also compare this method to some other ways you may have interfaced with C(++) in the past.
Ways to interface with C(++)
For this discussion we assume that the C(++) API you want to wrap follows some sort of create-use-destroy “object” model. Here, the term “object” can mean a C++ object or any form of structure as used in C. Many external APIs use this model. For example, the Windows API has functions like CreateWindow, ShowWindow and DestroyWindow. From here on, we will use the term C API even if the library is written in C++ (since we ultimately need to interface with flat C functions).
1. Using Classes
There are several ways you can wrap C APIs. Traditionally, these are wrapped in Delphi classes. The VCL is a prime example of this; most VCL classes are wrappers around handles in the Windows API.
A common design pattern here is to create the handle in the constructor of the wrapper and destroy it in the destructor. Other methods and properties can then use the handle:
Here, the routines that start with the “foo_” prefix are implemented in a third part library.
There can be some challenges with this method though. Suppose the C library keeps a list of Bar “objects” for each Foo object. We would wrap these using a TBar class in Delphi. An implementation could look like this:
Here the highlighted line is problematic. We retrieve a handle to a Bar object from the C API and we need to wrap this in a TBar class somehow. We could create a TBar instance on-the-fly as in this example. However, this instance needs to be destroyed at some point. In this example, the user of your wrapper class is responsible for this. This puts the burden of lifetime management on the user. And since it is not obvious that a method called GetItem creates a new instance, the user will probably not destroy it, leading to memory leaks.
This is a problem with all C APIs that return references to existing objects. There are several solutions for this issue. For example, the TFoo class could keep a list of TBar objects and keep these in sync with the C API. Then, the GetItem method would return an object in this list instead of creating a new one. And the TFoo class would make sure the objects get destroyed at the appropriate time. However, depending on the API it is not always easy or possible to keep this list in sync.
Another solution you may see is to use dictionaries that map C handles to their Delphi wrappers. In that case, the GetItem method would check if the dictionary already contains a Bar handle. If so, it would return its corresponding Delphi wrapper. Otherwise, it would create it and add it to the dictionary. However, keeping the dictionary up-to-date can also be problematic sometimes.
In addition, when using references to existing C objects, the Delphi wrapper does not own the handle, and should not destroy it in its destructor. This situation is often handled by using a flag called FOwnsHandle or something similar:
Here, the constructor is made private since it should only be used inside the unit that declares the TBar type.
2. Using Object Interfaces
Many of these issues can be avoided by using object interfaces instead of classes. This also simplifies lifetime management since you (and more importantly the users of your wrapper) don’t need to worry about destroying objects at the appropriate time.
The sample class model above could be converted to using object interfaces like this:
Here, the TFoo.GetItem method returns a newly created IBar interface (that doesn’t own the handle). Since this is an interface, it will be destroyed automatically when the user is done with it. No need for the TFoo class to maintain a list or dictionary of Bar instances (although it may still do so to avoid frequent allocations of TBar objects if that is an issue).
There are some disadvantages too though. Many wrapper methods are very small and only call the corresponding C API. However, since these methods are implemented in interfaces, you cannot inline them. Furthermore, since an interface is basically a virtual method table, all methods in an interface are virtual methods, resulting in an extra level of indirection when calling the method. This is not very cache-friendly since it requires accessing various (probably distant) locations in memory before making the call. When the wrapper function is called a lot, this can have an impact on performance.
Furthermore, as with classes, you are basically wrapping a (C) object inside another (Delphi) object, resulting in an additional dynamic memory allocation. If you create a lot of (temporary) objects this way, this can lead to additional memory fragmentation and performance loss.
Finally, using interfaces requires more code, since you have to write both an interface declaration, and then duplicate that interface declaration in a class. You also have to keep the interface and class declarations in sync. However, I consider this a minor disadvantage compared to all the benefits that interfaces have to offer.
[/SHOWTOGROUPS]
Delphi 10.4 introduced Custom Managed Records. In this post we show how you can use this new language feature to wrap third party C(++) APIs in an easy to use model. We also compare this method to some other ways you may have interfaced with C(++) in the past.
For this discussion we assume that the C(++) API you want to wrap follows some sort of create-use-destroy “object” model. Here, the term “object” can mean a C++ object or any form of structure as used in C. Many external APIs use this model. For example, the Windows API has functions like CreateWindow, ShowWindow and DestroyWindow. From here on, we will use the term C API even if the library is written in C++ (since we ultimately need to interface with flat C functions).
1. Using Classes
There are several ways you can wrap C APIs. Traditionally, these are wrapped in Delphi classes. The VCL is a prime example of this; most VCL classes are wrappers around handles in the Windows API.
A common design pattern here is to create the handle in the constructor of the wrapper and destroy it in the destructor. Other methods and properties can then use the handle:
Код:
type
TFoo = class
private
FHandle: THandle;
public
constructor Create;
destructor Destroy;
function Calc(const ALeft, ARight: Integer): Integer;
end;
constructor TFoo.Create;
begin
inherited;
FHandle := foo_create();
end;
destructor TFoo.Destroy;
begin
foo_destroy(FHandle);
inherited;
end;
function TFoo.Calc(const ALeft, ARight: Integer): Integer;
begin
Result := foo_calc(FHandle, ALeft, ARight);
end;
There can be some challenges with this method though. Suppose the C library keeps a list of Bar “objects” for each Foo object. We would wrap these using a TBar class in Delphi. An implementation could look like this:
Код:
type
TFoo = class
public
...
function GetItem(const AIndex: Integer): TBar;
end;
function TFoo.GetItem(const AIndex: Integer): TBar;
begin
var BarHandle := foo_get_item(FHandle, AIndex);
Result := TBar.Create(BarHandle); // ??
end;
This is a problem with all C APIs that return references to existing objects. There are several solutions for this issue. For example, the TFoo class could keep a list of TBar objects and keep these in sync with the C API. Then, the GetItem method would return an object in this list instead of creating a new one. And the TFoo class would make sure the objects get destroyed at the appropriate time. However, depending on the API it is not always easy or possible to keep this list in sync.
Another solution you may see is to use dictionaries that map C handles to their Delphi wrappers. In that case, the GetItem method would check if the dictionary already contains a Bar handle. If so, it would return its corresponding Delphi wrapper. Otherwise, it would create it and add it to the dictionary. However, keeping the dictionary up-to-date can also be problematic sometimes.
In addition, when using references to existing C objects, the Delphi wrapper does not own the handle, and should not destroy it in its destructor. This situation is often handled by using a flag called FOwnsHandle or something similar:
Код:
type
TBar = class
private
FHandle: THandle;
FOwnsHandle: Boolean;
private
constructor Create(const AHandle: THandle;
const AOwnsHandle: Boolean);
public
destructor Destroy; override;
end;
constructor TBar.Create(const AHandle: THandle;
const AOwnsHandle: Boolean);
begin
inherited Create;
FHandle := AHandle;
FOwnsHandle := AOwnsHandle;
end;
destructor TBar.Destroy;
begin
if (FOwnsHandle) then
bar_destroy(FHandle);
inherited;
end;
2. Using Object Interfaces
Many of these issues can be avoided by using object interfaces instead of classes. This also simplifies lifetime management since you (and more importantly the users of your wrapper) don’t need to worry about destroying objects at the appropriate time.
The sample class model above could be converted to using object interfaces like this:
Код:
type
IBar = interface
{ Various declarations... }
end;
type
IFoo = interface
function GetItem(const AIndex: Integer): IBar;
end;
type
TBar = class(TInterfacedObject, IBar)
{ Similar to TBar shown earlier }
end;
type
TFoo = class(TInterfacedObject, IFoo)
private
FHandle: THandle;
protected
{ IFoo }
function GetItem(const AIndex: Integer): IBar;
public
constructor Create;
destructor Destroy; override;
end;
constructor TFoo.Create;
begin
inherited;
FHandle := foo_create;
end;
destructor TFoo.Destroy;
begin
foo_destroy(FHandle);
inherited;
end;
function TFoo.GetItem(const AIndex: Integer): IBar;
begin
var BarHandle := foo_get_item(FHandle, AIndex);
Result := TBar.Create(BarHandle, False);
end;
There are some disadvantages too though. Many wrapper methods are very small and only call the corresponding C API. However, since these methods are implemented in interfaces, you cannot inline them. Furthermore, since an interface is basically a virtual method table, all methods in an interface are virtual methods, resulting in an extra level of indirection when calling the method. This is not very cache-friendly since it requires accessing various (probably distant) locations in memory before making the call. When the wrapper function is called a lot, this can have an impact on performance.
Furthermore, as with classes, you are basically wrapping a (C) object inside another (Delphi) object, resulting in an additional dynamic memory allocation. If you create a lot of (temporary) objects this way, this can lead to additional memory fragmentation and performance loss.
Finally, using interfaces requires more code, since you have to write both an interface declaration, and then duplicate that interface declaration in a class. You also have to keep the interface and class declarations in sync. However, I consider this a minor disadvantage compared to all the benefits that interfaces have to offer.
[/SHOWTOGROUPS]