Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / UWP

CoreObjects/GoliahCore17: *.* for plain C

3.00/5 (3 votes)
3 Jan 2019CPOL6 min read 6.5K   53  
UWP, COM, C++/WinRT, OOP in plain C. What else?

Introduction

Today, I will tell you about what has become one of my craziest experiment:

Render the Canvas of previous issue in an UWP, Win2D Canvas Control.

The idea was simple:

  • Create an UWP app backed on C++/WinRT
  • Follow instruction to embed a Win2D Canvas in it
  • Include our Canvas code from the previous issue
  • Make a "C" projection of the needed COM interfaces
  • Extract pointers to native instances of that interfaces from C++/WinRT wrappers, to share with "C"
  • Make our Renderer wrapper around those interfaces
  • And finally, yield back control to "C" on any event

But not so simple to be accomplished!

Background

This is to be considered an "exploration beyond the boundaries" of concepts first introduced in "CoreObjects/GoliahCore17: Effective OOP for plain C" and an extension of its sequel "CoreObjects/GoliahCore17: Effective OOP for plain C || 2", so an overview of both is welcome. Component Object Model (COM) and C++ Basics can also help.

The Goal

The original plan was to create an UWP app working as a modern front-end derived by a "C" application, as if we would bring a modern Windows GUI to a legacy "C" application.

The legacy "C" application had to be based upon our sample Canvas example.

The new GUI had to be composed by a Win2D Canvas (see it on github) and some buttons to add and edit shapes.

The "C" code had to preserve is central role, handling events and driving actions.

The Route

The steps to reach our objective.

Create an UWP App Backed on C++/WinRT

First of all, we need to install the C+++/WinRT for Visual Studio 2017.

The actual release of VisualStudio installed on my PC is the following:

Microsoft Visual Studio Community 2017
Version 15.9.4
VisualStudio.15.Release/15.9.4+28307.222
Microsoft .NET Framework
Version 4.7.03056

To Install the C++/WinRT Extension

  1. Choose Extensions And Updates under the Tools Menu, to showup the Extensions And Updates dialog.
  2. Expand the installed node in the left pane.
  3. Verify if it is already installed in the list on the middle pane.
  4. To speedup the check, type winrt in the search box in the top of right panel.
  5. If it is not installed, expand the online node in the left pane.
  6. Type winrt in the search box in the top of right panel.
  7. Locate the box of C++/WinRT in the middle list.
  8. Click the download button.
  9. Follow the instruction up to VisualStudio restart.

To Create a UWP Project for C++/WinRT

  1. Choose Project under New entry of File Menu to show up the New Project dialog.
  2. In the left pane, under Installed, expand the Visual C++ node.
  3. Select Window Universal. The middle pane is filled with a list of templates.
  4. Select Blank App (C++/WInRT).
  5. Give a name to the project (and to the solution), choosing also the containing folder.
  6. Click OK.

The project is ready, with a form containing 1 button saying "Click Me", and all the code needed to handle the click.

You can now build the project and, after some minutes (i7 3.4Ghz 8MB SSD!!!), you can click on the Click Me button.

Installing the Win2D Package

To install Win2D package, I've followed the instructions in the Win2D documentation (here):

  1. Choose Manage NuGet Packages for Solution... under NuGet Package Manager of Tools menĂ¹.
  2. Type 'Win2D' into the 'Search Online' box, and hit Enter.
  3. Select the 'Win2D.uwp' package and click 'Install', then 'OK'.
  4. Accept the license agreement.
  5. Click 'Close'.

Embedding the Win2D Canvas Control

The documentation of Win2D mentioned above include only examples for C#, C++/CX, and VB.

Fortunately, the C++/WinRT approach is not so much different from the one of C++/CX, so the list below is an adapted version of the C++/CX example:

  1. Double click on MainPage.xaml in Solution Explorer to open the XAML editor.
  2. Add the Microsoft.Graphics.Canvas.UI.Xaml namespace next to the existing xmlns statements:
    XML
    xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
  3. Add a CanvasControl inside the existing Grid control:
    XML
    <StackPanel Orientation="Vertical" 
    HorizontalAlignment="Center" VerticalAlignment="Center">
        <canvas:CanvasControl x:Name="canvasControl" 
         Draw="canvasControl_Draw" ClearColor="CornflowerBlue" Width="600" 
         Height="300" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
    </StackPanel>
  4. Edit pch.h to add some includes:
    C++
    #include "winrt/Microsoft.Graphics.Canvas.UI.Xaml.h" 
  5. Edit the MainPage.h to add the following method on MainPage class:
    C++
    void canvasControl_Draw(Microsoft::Graphics::Canvas::UI::Xaml::CanvasControl const& sender,  
    Microsoft::Graphics::Canvas::UI::Xaml::CanvasDrawEventArgs const& args);
  6. Edit the MainPage.cpp to add namespaces at the top:
    C++
    using namespace Microsoft::Graphics::Canvas;
    using namespace Microsoft::Graphics::Canvas::UI::Xaml;
    using namespace Windows::UI; 
  7. Next add the method:
    C++
    void MainPage::canvasControl_Draw(CanvasControl const& sender, CanvasDrawEventArgs const& args)
    {
        args.DrawingSession().DrawEllipse(155, 115, 80, 30, Colors::Black(), 3);
        args.DrawingSession().DrawText(L"Hello, world!", 100, 100, Colors::Yellow());    
    }

Build and test the results.

Making Win2D COM Interfaces Suitable from Plain C

  1. First of all, I've realized that for this example, I had the needs of only one interface, the IWin2dDrawingSession.
  2. Next, I've found the basic COM interface in a generate header ".\Generated Files\winrt\impl\Microsoft.Graphics.Canvas.0.h":
    C++
    template <> struct abi<Microsoft::Graphics::Canvas::ICanvasDrawingSession>
    { struct type : IInspectable
    {
        {...}
    }
  3. I've manually converted this C++ struct in its "C" counterpart:
    C++
    struct IWin2dDrawingSessionProtocol {
      {...}
    }
  4. To do so, I've ported to "C" a constellation of support structure needed by the huge interface.
  5. Finally, I've created a Win2dDrawingSession the more conforming possible to the CoreObjects principles.
    Here is an example of the results in an invocation:
    C++
    CoreResultCode Win2dRenderer_Renderer_renderRectangle
    (CoreTarget target, struct CanvasRectangle* entity) {
       struct Win2dRenderer * self = (struct Win2dRenderer *)
       CoreObject_getSelf(Win2dRendererType->Class, target);
       self->Session->invoke->Win2dDrawingSession->
       DrawRectangleAtCoordsWithColorAndStrokeWidth(self->Session
           , entity->Location->X-(entity->Extent->Width/2), 
             entity->Location->Y-(entity->Extent->Height/2)
            , entity->Extent->Width, entity->Extent->Height
             , ColorModelARGB32_asWindow_UI_Color(entity->Attributes->StrokeColor)
              , entity->Attributes->StrokeWidth);
     return CoreResultCode_Success;
    } 

The "C" Application

The "C" application is an adaptation of our Canvas sample in the previous issue.

  1. I've included in the project folder and renamed as GoliahCore17_CoreObjects_Demo2b_Canvas.c a copy of the sample from "CoreObjects/GoliahCore17: Effective OOP for plain C || 2"
  2. Created the Win2dRender backed on Win2dDrawingSession (see attached code)
  3. Created the App_main function:
    C++
    enum App_main_Stage {
     App_main_Stage_Load
     , App_main_Stage_Exit
     , App_main_Stage_Draw
     , App_main_Stage_Click
    };
    CoreResultCode App_main(CoreTarget target, CoreWord16 stage, ...) {
     CoreArgumentList argList[1];
     if(target == 0) goto __abort;
     
     switch(stage) {
      case App_main_Stage_Load:
      {
       struct CanvasEntity* ent;
       struct Canvas* canvas = Canvas_build(0);
       ent = (struct CanvasEntity*)CanvasRectangle_build
             (0, 155, 115, 80, 30, 14, ColorModelARGB32_make(100, 0, 255, 0));
       canvas->invoke->Entities->insert(canvas, ent);
       ent = (struct CanvasEntity*)CanvasCircle_build
             (0, 155, 115, 80, ColorModelARGB32_make(100, 255, 0, 0));
       canvas->invoke->Entities->insert(canvas, ent);
       (*(struct Canvas**)target)=canvas;
       break;
      }
      case App_main_Stage_Exit:
      {
       CoreObject_free(target);
       break;
      }
      case App_main_Stage_Draw:
      {
       CoreByte rendererRegion[sizeof(struct Win2dRenderer)];
       CoreArgumentList_init(argList, stage);
       struct CanvasRenderer* renderer = (struct CanvasRenderer*)Win2dRenderer_build
        (rendererRegion, (struct Win2dDrawingSession*)CoreArgumentList_take(argList,CoreTarget));
       CoreArgumentList_done(argList);
       struct Canvas* canvas = (struct Canvas*) target;
       canvas->invoke->Canvas->render(canvas, renderer);
       break;
      }
      case App_main_Stage_Click:
      {
       struct Canvas* canvas = (struct Canvas*) target;
       struct CanvasEntityIterator* it=canvas->invoke->Entities->getIterator(canvas);
       struct CanvasEntityLocated* ent;
       for(ent = 0; it->invoke->next(it, &ent););
       GeometricsLocation2D loc = {150, 150};
       ColorModelARGB32 color = ColorModelARGB32_make(255,255,0,0);
       if(ent) {
        loc.X = (ent->Location->X + 30); if(loc.X > 600) loc.X = 0;
        loc.Y = (ent->Location->Y-30); if(loc.Y < 0) loc.Y = 300;
        color = ColorModelARGB32_make(255, ent->Attributes->StrokeColor.Red - 10, 0, 0);
       }
       ent = CanvasRectangle_build(0, loc.X, loc.Y, 100, 50, 0, color);
       canvas->invoke->Entities->insert(canvas, ent);
       break;
      }
     }
     return CoreResultCode_Success;
    __abort:
     return CoreResultCode_Fail;
    }

A Bit of Glue into the C++ Side

To join all together, I've inserted App_main calls in MainPage class keypoints.

  1. Adding the magic "getter" of the needed COM interfaces:
    C++
    template<typename Type> struct CoreObject_ComInstance { using type = IUnknown; };
    template<typename Type> static CoreTarget CoreObjects_getComInstance(Type const&target) {
      // Rearrangment of private:
      //  #define WINRT_SHIM(Type) (*(abi_t<Type>**)&static_cast<Type const&>
      //  (static_cast<D const&>(*this)))
      using IType = typename CoreObject_ComInstance<CanvasDrawingSession>::type;
      return *(winrt::impl::abi_t<ICanvasDrawingSession>**)
             &static_cast<ICanvasDrawingSession const&>(target);
    }
  2. Adding Canvas data Member:
    C++
    struct MainPage : MainPageT<MainPage>
    {
        {...}
        CoreTarget Canvas;
        {...}
    };
  3. Extern declarations at the top of MainPage.cpp:
    C++
    extern "C" {
     typedef uint16_t CoreWord16;
     typedef const void* CoreTarget;
     typedef uint32_t CoreResultCode;
     enum App_main_Stage {
      App_main_Stage_Load
      , App_main_Stage_Exit
      , App_main_Stage_Draw
      , App_main_Stage_Click
     };
     CoreResultCode App_main(CoreTarget target, CoreWord16 stage, ...);
    }
  4. Constructor:
    C++
    MainPage::MainPage()
    {
        InitializeComponent();
        App_main(&Canvas,App_main_Stage_Load);
    }
  5. Destructor:
    C++
    MainPage::~MainPage() {
        App_main(Canvas,App_main_Stage_Exit);
    }
  6. Win2D Canvas Draw Event:
    C++
    void MainPage::buttonControl_Click(IInspectable const&, RoutedEventArgs const&)
    {
        App_main(Canvas,App_main_Stage_Click);
        canvasControl().Invalidate();  
    }
  7. Button Click Event:
    C++
    void MainPage::canvasControl_Draw(CanvasControl const& sender, CanvasDrawEventArgs const& args)
    {
        App_main(Canvas, App_main_Stage_Draw, CoreObjects_getComInstance(args.DrawingSession()));
    }

The Results

The final app is not exactly what I've expected (due... see the next paragraph), but for now sufficient for the purpose of this article. There are the Canvas control and a button to add a shape.

That's all folks!

Jams

I have to mention that C++/WINRT is still at a preliminary stage, as told by itself:

XamlCompiler warning WMC1502: CppWinRT support is currently in preview and may change in future updates.

A part of that, I had to experiment a mess of disappointing issues...

  • Over 2 minutes for a complete rebuild, 2 of that to run code generators and PCH
  • Impossible to define a separate PCH for the C code
  • XAML Designer quite unusable due unknown exceptions frequently thrown
  • Source code associated with XAML not correctly generated nor kept in sync
  • IntelliSense lazy, incorrect and very very crazy
  • VisualStudio lagging, keeping for dozens of seconds one processor core of my i7 at 100% of duty
  • Not to mention the leak of a method to get the wrapped COM interface from a C++/WinRT class
  • etc.

How to Use the Source Code

The source code attached is an agglomerate of code fragments pasted together in a unique C file, to instantly bring the focus on the subject. The package includes also the VisualStudio2017 Community Edition project and solution under which it was created. Feel free to mess with it and enjoy.

Note: Before you build, you need to restore nuget packages.

To Be Continued...

The vastness of the topic can't be fitted in a couple of articles, this is only a brief overview. If it were to receive some kind of interest, I will return to the topic to deal with some other aspects of the OOP using CoreObjects/ColiahCore17, from common PC to embedded architectures with very few resources.

History

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)