Steven Schveighoffer wrote:
On Thu, 24 Sep 2009 10:54:06 -0400, Jeremie Pelletier
<[email protected]> wrote:
Steven Schveighoffer wrote:
On Tue, 22 Sep 2009 22:02:59 -0400, Jeremie Pelletier
<[email protected]> wrote:
Yeah most of my display interfaces would make use of covariant
arguments, I use main abstract factory for the entire package, and
the objects it creates contain factory methods themselves. I plan to
have implementations for all of win32, gdk, x11, quartz, cairo,
pango, d2d, dwrite, gl, gl3 and finally d3d7 up to d3d11. Most of
the client code will therefore see only the interfaces in order to
maintain portability, and to allow different implementations to live
in the same executable (for example win32/gl/cairo/pango for up to
vista or win32/d3d/d2d/dwrite if on win7 and up).
Here is a watered down version of a few interfaces I use, which are
used by client code:
interface IDrawable {}
interface IWindow : IDrawable {} // onscreen drawable
interface ISurface : IDrawable {} // offscreen drawable
interface IDisplayContext {} // base of 2d-3d contextes
interface IRenderContext {} // 3d context
interface IWindowRenderContext {} // specialized onscreen 3d context
interface IRenderer {
IWindowRenderContext CreateRenderContext(IWindow);
ISurfaceRenderContext CreateRenderContext(ISurface);
}
And some of their current implementation, which are all used within
the package:
abstract class Win32Drawable : IDrawable {}
final class Win32Window : Win32Drawable, IWindow {}
final class Win32Surface : Win32Drawable, IWindow {}
final class GLRenderer : IRenderer {
GLWindowRenderContext CreateRenderContext(IWindow window) {
if(auto win32Window = cast(Win32Window)window)
return new GLWindowRenderContext(win32Window);
else throw new Error();
}
GLSurfaceRenderContext CreateRenderContext(ISurface surface) {
if(auto win32Surface = cast(Win32Surface)surface)
return new GLSurfaceRenderContext(win32Surface);
else throw new Error();
}
}
abstract class GLRenderContext : IRenderContext {}
final class GLWindowRenderContext : GLRenderContext,
IWindowRenderContext {
this(Win32Window) {}
}
final class GLSurfaceRenderContext : GLRenderContext,
ISurfaceRenderContext {
this(Win32Surface) {}
}
I have over a hundred of such methods doing dynamic casts across all
the different implementations like these twos in this package alone,
a display interface is quite a large beast.
Of course if you can suggest a better way of doing methods expecting
a specific implementation of an object, while still allowing client
code to call them with the interface pointer, I'd be glad to
implement it :)
Jeremie
There are some possible solutions. First, it looks like you are
using interfaces to abstract the platform, which seems more
appropriate for version statements. I once wrote an OS abstraction
library in C++, and in my fanatic attempt to avoid using the
preprocessor for anything, I made everything interfaces (pure
abstract classes). I think with D, the version statements are a much
better solution, and will reduce overhead quite a bit.
Second, Your IRenderer is the one responsible for creating a render
context, but it depends on being "hooked up" with the appropriate
IWindow or ISurface object. However, the IRenderer implementation's
methods are pretty much static (granted they might be trimmed down).
Why not move them into the IWindow and ISurface interfaces?
interface IWindow : IDrawable {
IWindowRenderContext CreateRenderContext();
}
interface ISurface : IDrawable {
ISurfaceRenderContext CreateRenderContext();
}
If you need an instance of IRenderer for some other reason not
shown, then consider using a hidden singleton of the correct
implementation.
-Steve
Because there will be multiple implementations living in the same
executable. We all know how microsoft likes to force new technology to
new windows versions. I want support for Direct2D, DirectWrite, and
all versions of Direct3D. Which requires different implementations for
7, vista, and xp. Then some people might have better performance with
GL, for graphic adapter vendor and driver issues, so I'm throwing such
an implementation in the mix. On unix you have a choice of different
windowing toolkits, be it Gnome, Qt, Xfce or directly using X11 but
losing specific features offered by the toolkits.
OK, my assumption was wrong, win32 is a toolkit identifier, not a
platform identifier :)
>
As for merging IRenderer with the drawables, this wouldn't fit my
design. I use what I call render contexts and paint contexts for 3d
and 2d drawing respectively, which are both built upon a common set of
display interfaces to get most of their shared concepts unified and
compatible with one another. I also have font layering and rendering
interfaces usable by the two. And given that many different render and
paint implementations can be used from the same drawable targets, it
wouldn't make sense.
It may well be that there is no better solution. In a statically-typed
system, it's sometimes the case that an object hierarchy is too complex
for compile-time detectable errors. I've ran into such cases before,
and for the most part, you just have to live with it, but it shouldn't
be *every* method that requires dynamic casts.
You may be able to cut back on a lot of your dynamic casts by finding
places where you are over-using interfaces for method detection instead
of just throwing errors for unimplemented functions, or allowing too
many possibilities to be passed in.
Well the interface arguments are part of the interface declarations in
order for the method to be visible to the client, so any class
implementing them must also use interface arguments even if they only
accept their own implementations. Only a subset of methods require
dynamic casts, mostly those binding objects of the same implementation
together from the public client interfaces. Once the objects are created
and bound there is barely any covariance involved.
My first design was using version statements but it was far from
flexible enough for my needs. The overhead is mostly needed to
allocate the interfaces, not to use them so the speed isn't affected.
Forget about the version thing, I misunderstood why you had win32
identifiers.
Then you get my I/O interface which roots at simple input/output
streams, then seekable streams, binary streams, file streams, async
streams, pipes, etc. And get different implementations for local
filesystem I/O, sockets, specialized file format abstractions, and
whatnot. So for example a method expecting an IInputStream does not
care what is implementing it, so long as it has a read method
implemented, be it reading data from a file, from a network
connection, from a packed file within an archive. These
implementations still need covariant parameters within themselves for
a few things.
A good example, which I happen to have some experience with.
The Tango lib used to define Input and Output streams independent of
Seekable streams (there was a Seek interface which was applied
separately to an input/output implementation class).
But what ends up happening is that it was unwieldly to use streams in
cases where seeking is used, because you have to dynamic-cast to the
seek interface to determine if seeking is available.
interface ISeekableInputStream : IInputStream, ISeekableStream {}
The solution we ended up using is that *all* streams defined the seek
function, even if they didn't support seeking, and if you called it on
such objects, they just throw an exception.
The compromise allows (in my opinion) a lot better code in things such
as filters and buffers, which may or may not be backed by seekable
streams, so may or may not need to implement seeking. The code can now
simply forward the calls to the underlying object.
All this aside, I still don't want covariance on parameters done
*automatically* by the compiler, it adds hidden cost for not-much gain,
and fosters designs that could have better alternatives. In my
experience, having to dynamic cast for *everything* indicates a redesign
is in order.
My I/O implementation barely uses dynamic casting since there is very
little object to object communication within a single implementation, I
mean, after implementing a stream interface you're pretty much done. The
display package quite a few interfaces to implement for every single
backend.
APIs such as DirectX already are only available through COM which only
works with interface pointers, it must also do dynamic cast to ensure I
don't make a custom ID3D10Texture2D implementation and try to use it.
Mozilla's entire platform (other than nspr) is built upon such
interfaces in IDL as a bridge between the possible C++ implementations
and their JavaScript bindings.
Covariant arguments is something you're gonna come across at some point
or another. Having language support for it does not make it the rule,
but a convenience for the exception.
Jeremie