Cogs and Levers A blog full of technical stuff

Making CIFS Shares available to Docker

Introduction

Mounting CIFS (SMB) shares in Linux can be a convenient way to access network resources as part of the local filesystem. In this guide, I’ll walk you through the steps for properly configuring a CIFS share in /etc/fstab on a Linux system. I’ll also show you how to ensure that network mounts are available before services like Docker start up.

Step 1: Modify /etc/fstab

To mount a CIFS share automatically at boot, we need to modify the /etc/fstab file. First, open it in a text editor:

sudo vim /etc/fstab

Now, add or modify the CIFS entry in the file. A typical CIFS entry looks like this:

# Example CIFS line in fstab
//server_address/share_name /local/mount/point cifs credentials=/path/to/credentials,file_mode=0755,dir_mode=0755,uid=1000,gid=1000,_netdev 0 0

Explanation:

  • //server_address/share_name: The remote server and share you want to mount (e.g., //192.168.1.100/shared).
  • /local/mount/point: The local directory where the share will be mounted.
  • cifs: The filesystem type for CIFS/SMB.
  • credentials=/path/to/credentials: Points to a file containing your username and password (this is optional, but recommended for security).
  • file_mode=0755,dir_mode=0755: Sets the file and directory permissions for the mounted share.
  • uid=1000,gid=1000: Specifies the user and group IDs that should own the files (replace 1000 with your user/group IDs).
  • _netdev: Ensures that the mount waits for network availability before mounting.
  • 0 0: The last two values are for dump and fsck; they can usually remain 0.

Step 2: Create a Credentials File

For better security, you can use a separate credentials file rather than hard-coding the username and password in /etc/fstab. To do this, create a file to store the username and password for the share:

sudo nano /path/to/credentials

Add the following lines to the file:

username=your_username
password=your_password
domain=your_domain   # (optional, if you're in a domain environment)

Make sure the credentials file is secure by setting appropriate permissions:

sudo chmod 600 /path/to/credentials

This ensures only the root user can read the file, which helps protect sensitive information.

Step 3: Test the Mount

After adding the CIFS line to /etc/fstab and configuring the credentials file, it’s time to test the mount. You can do this by running:

sudo mount -a

If everything is configured correctly, the CIFS share should mount automatically. If you encounter any issues, check the system logs for errors. Use one of these commands to inspect the logs:

# On Ubuntu or Debian-based systems
sudo tail /var/log/syslog

# On CentOS or RHEL-based systems
sudo tail /var/log/messages

Ensuring Mounts are Available Before Docker

If you’re running Docker on the same system and need to ensure that your CIFS mounts are available before Docker starts, you’ll want to modify Docker’s systemd service. Here’s how:

First, create a directory for Docker service overrides:

sudo mkdir -p /etc/systemd/system/docker.service.d

Next, create a custom override file:

sudo vim /etc/systemd/system/docker.service.d/override.conf

Add the following content:

[Unit]
After=remote-fs.target
Requires=remote-fs.target

This configuration ensures Docker waits until all remote filesystems (like CIFS) are mounted before starting.

Finally, reload the systemd configuration and restart Docker:

sudo systemctl daemon-reload
sudo systemctl enable docker
sudo systemctl restart docker

Now, Docker will wait for your CIFS mounts to be available before starting any containers that might rely on them.

By following these steps, you can ensure your CIFS shares are mounted reliably on boot and integrated seamlessly with other services like Docker. This is especially useful for network-based resources that are critical to your containers or other local services.

Double Buffering with the Windows GDI

Introduction

Flickering can be a common problem when drawing graphics in a Windows application. One effective way to prevent this is by using a technique called double buffering. In this article, we’ll walk through creating a simple Win32 application that uses double buffering to provide smooth and flicker-free rendering.

Getting Started

First, let’s create a basic Win32 window and set up the message loop.

#include <Windows.h>

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int running = 1;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {

    WNDCLASSEX wc = {
        sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
        WindowProc, NULL, NULL,
        hInstance,
        LoadIcon(hInstance, IDI_APPLICATION),
        LoadCursor(hInstance, IDC_ARROW),
        NULL, NULL, L"DoubleBufferClass", NULL
    };

    RegisterClassEx(&wc);

    HWND hWnd = CreateWindowEx(WS_EX_APPWINDOW, L"DoubleBufferClass", L"Double Buffer",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, hInstance, NULL);

    ShowWindow(hWnd, SW_SHOWDEFAULT);
    UpdateWindow(hWnd);

    MSG msg;

    while (running) {

        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

    }

    return (int)msg.lParam;
}

In this code, we define a WinMain function, which is the entry point for a Windows desktop application. We define a window class and register it with the system, then create the window using CreateWindowEx.

The message loop waits for input messages, like key presses or mouse movements, and dispatches them to the appropriate window procedure. We check for messages using PeekMessage so the loop remains responsive and can handle user input without blocking.

Creating the Buffer

Now, let’s modify the program to set up the back buffer for double buffering. We’ll do this by implementing the window procedure (WindowProc) and handling key messages like WM_CREATE, WM_SIZE, and WM_DESTROY.

HDC memDC = NULL, winDC = NULL;
HBITMAP memBitMap = NULL;
HBITMAP memOldMap = NULL;

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    switch (uMsg) {
        case WM_CLOSE:
            running = 0;
            break;

        case WM_ERASEBKGND:
            return 1;

        case WM_DESTROY: 
            DestroyBackBuffer(hWnd);
            PostQuitMessage(0);
            return 0;

        case WM_CREATE:
            RecreateBackBuffer(hWnd);
            break;

        case WM_SIZE:
            RecreateBackBuffer(hWnd);
            break;

        case WM_PAINT:

            PAINTSTRUCT ps;
            RECT r;

            GetClientRect(hWnd, &r);
            FillRect(memDC, &r, CreateSolidBrush(RGB(0, 255, 0)));

            HDC hdc = BeginPaint(hWnd, &ps);
            BitBlt(hdc, 0, 0, r.right - r.left, r.bottom - r.top, memDC, 0, 0, SRCCOPY);
            EndPaint(hWnd, &ps);

            break;
    }

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

The WindowProc function handles window events such as creating the back buffer (WM_CREATE), resizing it (WM_SIZE), and destroying it (WM_DESTROY). We also override WM_ERASEBKGND to prevent flickering by blocking the default background erase.

Next, in the WM_PAINT handler, we use BitBlt to copy the contents of the memory device context (memDC) to the window’s device context, effectively flipping the buffer and rendering the scene.

Drawing and Flipping

Now, we’ll define the RecreateBackBuffer and DestroyBackBuffer functions that manage the lifecycle of the buffer.

void DestroyBackBuffer(HWND hWnd) {

    if (memDC != NULL) {
        SelectObject(memDC, memOldMap);
        DeleteObject(memBitMap);
        DeleteDC(memDC);

        memDC = NULL;
        memOldMap = memBitMap = NULL;
    }

    if (winDC != NULL) {
        ReleaseDC(hWnd, winDC);
        winDC = NULL;
    }

}

void RecreateBackBuffer(HWND hWnd) {

    DestroyBackBuffer(hWnd);

    RECT client;

    GetClientRect(hWnd, &client);
    winDC = GetDC(hWnd);
    
    memDC = CreateCompatibleDC(winDC);
    memBitMap = CreateCompatibleBitmap(winDC, client.right - client.left, client.bottom - client.top);
    memOldMap = (HBITMAP)SelectObject(memDC, memBitMap);

}

The RecreateBackBuffer function creates a new off-screen bitmap whenever the window is resized or created. The bitmap is selected into the memory device context (memDC), which is used for all the off-screen drawing.

The DestroyBackBuffer function cleans up the memory device context, releasing the resources used by the back buffer when the window is destroyed or the buffer is resized.

Animation Loop

To animate, we need to redraw the back buffer continually. Instead of relying solely on WM_PAINT, we can create an animation loop that forces the screen to refresh at regular intervals.

A simple way to do this is to use SetTimer or a manual loop that invalidates the window periodically. Here’s how you could structure the loop:

while (running) {
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    } else {
        // Animation logic here
        InvalidateRect(hWnd, NULL, FALSE);
        Sleep(16); // Roughly 60 FPS
    }
}

This change redraws the window about 60 times per second, perfect for smooth animations.

Conclusion

Double buffering is a powerful technique that enhances the visual quality of graphical applications by eliminating flickering during rendering. By using an off-screen buffer to draw content before displaying it on the screen, we can ensure smooth transitions and animations. In this article, we walked through setting up a basic Win32 window, creating and managing the back buffer, and implementing a simple animation loop using double buffering.

With this foundation, you can now explore more complex drawing routines or incorporate this technique into larger projects for better performance and visual appeal.

Creating a Simple Ray Tracer in Haskell

Introduction

Ray tracing is a technique for generating an image by tracing the path of light as pixels in an image plane. It simulates how rays of light interact with objects in a scene to produce realistic lighting, reflections, and shadows.

In this post, we’ll walk through building a simple raytracer in Haskell. We will start with basic vector math, define shapes like spheres and cubes, and trace rays through the scene to generate an image. By the end, you’ll have a raytracer that can render reflections and different shapes.

What You’ll Learn:

  • Basics of raytracing and the math behind it
  • How to define math primitives in Haskell
  • How to trace rays against shapes (including spheres and cubes)
  • How to generate an image from the traced rays
  • … a little math

Some Math Primitives

To begin, we need to define some basic 3D vector math. This is essential for all calculations involved in ray tracing: adding vectors, calculating dot products, normalizing vectors, and more.

We’ll define a Vec3 data type to represent 3D vectors and functions for common vector operations.

-- Define a vector (x, y, z) and basic operations
data Vec3 = Vec3 { x :: Double, y :: Double, z :: Double }
deriving (Show, Eq)

-- Vector addition
add :: Vec3 -> Vec3 -> Vec3
add (Vec3 x1 y1 z1) (Vec3 x2 y2 z2) = Vec3 (x1 + x2) (y1 + y2) (z1 + z2)

-- Vector subtraction
sub :: Vec3 -> Vec3 -> Vec3
sub (Vec3 x1 y1 z1) (Vec3 x2 y2 z2) = Vec3 (x1 - x2) (y1 - y2) (z1 - z2)

-- Scalar multiplication
scale :: Double -> Vec3 -> Vec3
scale a (Vec3 x1 y1 z1) = Vec3 (a * x1) (a * y1) (a * z1)

-- Dot product
dot :: Vec3 -> Vec3 -> Double
dot (Vec3 x1 y1 z1) (Vec3 x2 y2 z2) = x1 * x2 + y1 * y2 + z1 * z2

-- Normalize a vector
normalize :: Vec3 -> Vec3
normalize v = scale (1 / len v) v

-- Vector length
len :: Vec3 -> Double
len (Vec3 x1 y1 z1) = sqrt (x1 * x1 + y1 * y1 + z1 * z1)

-- Reflect a vector v around the normal n
reflect :: Vec3 -> Vec3 -> Vec3
reflect v n = sub v (scale (2 * dot v n) n)

Defining a Ray

The ray is the primary tool used to “trace” through the scene, checking for intersections with objects like spheres or cubes.

A ray is defined by its origin \(O\) and direction \(D\). The parametric equation of a ray is:

\[P(t) = O + t \cdot D\]

Where:

  • \(O\) is the origin
  • \(D\) is the direction of the ray
  • \(t\) is a parameter that defines different points along the ray
-- A Ray with an origin and direction
data Ray = Ray { origin :: Vec3, direction :: Vec3 }
    deriving (Show, Eq)

Shapes

To trace rays against objects in the scene, we need to define the concept of a Shape. In Haskell, we’ll use a typeclass to represent different types of shapes (such as spheres and cubes). The Shape typeclass will define methods for calculating ray intersections and normals at intersection points.

ExistentialQuantification and Why We Need It

In Haskell, lists must contain elements of the same type. Since we want a list of various shapes (e.g., spheres and cubes), we need a way to store different shapes in a homogeneous list. We achieve this by using existential quantification to wrap each shape into a common ShapeWrapper.

{-# LANGUAGE ExistentialQuantification #-}

-- Shape typeclass
class Shape a where
    intersect :: Ray -> a -> Maybe Double
    normalAt :: a -> Vec3 -> Vec3
    getColor :: a -> Color
    getReflectivity :: a -> Double

-- A wrapper for any shape that implements the Shape typeclass
data ShapeWrapper = forall a. Shape a => ShapeWrapper a

-- Implement the Shape typeclass for ShapeWrapper
instance Shape ShapeWrapper where
    intersect ray (ShapeWrapper shape) = intersect ray shape
    normalAt (ShapeWrapper shape) = normalAt shape
    getColor (ShapeWrapper shape) = getColor shape
    getReflectivity (ShapeWrapper shape) = getReflectivity shape

Sphere

Sphere Equation

A sphere with center \(C = (c_x, c_y, c_z)\) and radius \(r\) satisfies the equation:

\[(x - c_x)^2 + (y - c_y)^2 + (z - c_z)^2 = r^2\]

In vector form:

\[\lVert P - C \rVert^2 = r^2\]

Where \(P\) is any point on the surface of the sphere, and \(\lVert P - C \rVert\) is the Euclidean distance between \(P\) and the center \(C\).

Substituting the Ray into the Sphere Equation

We substitute the ray equation into the sphere equation:

\[\lVert O + t \cdot D - C \rVert^2 = r^2\]

Expanding this gives:

\[(O + t \cdot D - C) \cdot (O + t \cdot D - C) = r^2\]

Let \(L = O - C\), the vector from the ray origin to the sphere center:

\[(L + t \cdot D) \cdot (L + t \cdot D) = r^2\]

Expanding further:

\[L \cdot L + 2t(L \cdot D) + t^2(D \cdot D) = r^2\]

This is a quadratic equation in \(t\):

\[t^2(D \cdot D) + 2t(L \cdot D) + (L \cdot L - r^2) = 0\]

Solving the Quadratic Equation

The equation can be solved using the quadratic formula:

\[t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\]

Where:

  • a is defined as: \(a = D \cdot D\)
  • b is defined as: \(b = 2(L \cdot D)\)
  • c is defined as: \(c = L \cdot L - r^2\)

The discriminant \(\Delta = b^2 - 4ac\) determines the number of intersections:

  • \(\Delta < 0\): no intersection
  • \(\Delta = 0\): tangent to the sphere
  • \(\Delta > 0\): two intersection points

Here’s how we define a Sphere as a Shape with a center, radius, color, and reflectivity.

-- A Sphere with a center, radius, color, and reflectivity
data Sphere = Sphere { center :: Vec3, radius :: Double, sphereColor :: Color, sphereReflectivity :: Double }
    deriving (Show, Eq)

instance Shape Sphere where
    intersect (Ray o d) (Sphere c r _ _) =
        let oc = sub o c
        a = dot d d
        b = 2.0 * dot oc d
        c' = dot oc oc - r * r
        discriminant = b * b - 4 * a * c'
        in if discriminant < 0
        then Nothing
        else Just ((-b - sqrt discriminant) / (2.0 * a))

    normalAt (Sphere c _ _ _) p = normalize (sub p c)
    getColor (Sphere _ _ color _) = color
    getReflectivity (Sphere _ _ _ reflectivity) = reflectivity

Cube Definition

For a cube, we typically use an axis-aligned bounding box (AABB), which means the cube’s faces are aligned with the coordinate axes. The problem of ray-cube intersection becomes checking where the ray crosses the planes of the box’s sides.

The cube can be defined by two points: the minimum corner \(\text{minCorner} = (x_{\text{min}}, y_{\text{min}}, z_{\text{min}})\) and the maximum corner \(\text{maxCorner} = (x_{\text{max}}, y_{\text{max}}, z_{\text{max}})\). The intersection algorithm involves calculating for each axis independently and then combining the results.

Cube Planes and Ray Intersections

For each axis (x, y, z), the cube has two planes: one at the minimum bound and one at the maximum bound. The idea is to calculate the intersections of the ray with each of these planes.

For the x-axis, for example, we compute the parameter \(t\) where the ray hits the two x-planes:

\[t_{\text{min}, x} = \frac{x_{\text{min}} - O_x}{D_x}\] \[t_{\text{max}, x} = \frac{x_{\text{max}} - O_x}{D_x}\]

We do the same for the y-axis and z-axis:

\[t_{\text{min}, y} = \frac{y_{\text{min}} - O_y}{D_y}\] \[t_{\text{max}, y} = \frac{y_{\text{max}} - O_y}{D_y}\] \[t_{\text{min}, z} = \frac{z_{\text{min}} - O_z}{D_z}\] \[t_{\text{max}, z} = \frac{z_{\text{max}} - O_z}{D_z}\]

Combining the Results

The idea is to calculate when the ray enters and exits the cube. The entry point is determined by the maximum of the \(t_{\text{min}}\) values across all axes (because the ray must enter the cube from the farthest plane), and the exit point is determined by the minimum of the \(t_{\text{max}}\) values across all axes (because the ray must exit at the nearest plane):

\[t_{\text{entry}} = \max(t_{\text{min}, x}, t_{\text{min}, y}, t_{\text{min}, z})\] \[t_{\text{exit}} = \min(t_{\text{max}, x}, t_{\text{max}, y}, t_{\text{max}, z})\]

If \(t_{\text{entry}} > t_{\text{exit}}\) or \(t_{\text{exit}} < 0\), the ray does not intersect the cube.

Final Cube Intersection Condition

To summarize, the cube-ray intersection works as follows:

  • Calculate \(t_{\text{min}}\) and \(t_{\text{max}}\) for each axis.
  • Compute the entry and exit points.
  • If the entry point occurs after the exit point (or both are behind the ray origin), there is no intersection.
-- A Cube defined by its minimum and maximum corners
data Cube = Cube { minCorner :: Vec3, maxCorner :: Vec3, cubeColor :: Color, cubeReflectivity :: Double }
deriving (Show, Eq)

instance Shape Cube where
    intersect (Ray o d) (Cube (Vec3 xmin ymin zmin) (Vec3 xmax ymax zmax) _ _) =
        let invD = Vec3 (1 / x d) (1 / y d) (1 / z d)
        t0 = (Vec3 xmin ymin zmin `sub` o) `mul` invD
        t1 = (Vec3 xmax ymax zmax `sub` o) `mul` invD
        tmin = maximum [minimum [x t0, x t1], minimum [y t0, y t1], minimum [z t0, z t1]]
        tmax = minimum [maximum [x t0, x t1], maximum [y t0, y t1], maximum [z t0, z t1]]
        in if tmax < tmin || tmax < 0 then Nothing else Just tmin

    normalAt (Cube (Vec3 xmin ymin zmin) (Vec3 xmax ymax zmax) _ _) p =
        let (Vec3 px py pz) = p
        in if abs (px - xmin) < 1e-4 then Vec3 (-1) 0 0
           else if abs (px - xmax) < 1e-4 then Vec3 1 0 0
           else if abs (py - ymin) < 1e-4 then Vec3 0 (-1) 0
           else if abs (py - ymax) < 1e-4 then Vec3 0 1 0
           else if abs (pz - zmin) < 1e-4 then Vec3 0 0 (-1)
           else Vec3 0 0 1

    getColor (Cube _ _ color _) = color

    getReflectivity (Cube _ _ _ reflectivity) = reflectivity

Tracing a Ray Against Scene Objects

Once we have rays and shapes, we can start tracing rays through the scene. The traceRay function checks each ray against all objects in the scene and calculates the color at the point where the ray intersects an object.

-- Maximum recursion depth for reflections
maxDepth :: Int
maxDepth = 5

-- Trace a ray in the scene, returning the color with reflections
traceRay :: [ShapeWrapper] -> Ray -> Int -> Color
traceRay shapes ray depth
    | depth >= maxDepth = Vec3 0 0 0  -- If we reach the max depth, return black (no more reflections)
    | otherwise = case closestIntersection of
        Nothing -> backgroundColor  -- No intersection, return background color
        Just (shape, t) -> let hitPoint = add (origin ray) (scale t (direction ray))
                               normal = normalAt shape hitPoint
                               reflectedRay = Ray hitPoint (reflect (direction ray) normal)
                               reflectionColor = traceRay shapes reflectedRay (depth + 1)
                               objectColor = getColor shape
                           in add (scale (1 - getReflectivity shape) objectColor)
                                  (scale (getReflectivity shape) reflectionColor)
    where
        intersections = [(shape, dist) | shape <- shapes, Just dist <- [intersect ray shape]]
        closestIntersection = if null intersections 
                              then Nothing 
                              else Just $ minimumBy (comparing snd) intersections
        backgroundColor = Vec3 0.5 0.7 1.0  -- Sky blue background

Putting It All Together

We can now render a scene by tracing rays for each pixel and writing the output to an image file in PPM format.

-- Create a ray from the camera to the pixel at (u, v)
getRay :: Double -> Double -> Ray
getRay u v = Ray (Vec3 0 0 0) (normalize (Vec3 u v (-1)))

-- Render the scene
render :: Int -> Int -> [ShapeWrapper] -> [[Color]]
render width height shapes =
    [[traceRay shapes (getRay (2 * (fromIntegral x / fromIntegral width) - 1)
                              (2 * (fromIntegral y / fromIntegral height) - 1)) 0
      | x <- [0..width-1]]
      | y <- [0..height-1]]

-- Convert a color to an integer pixel value (0-255)
toColorInt :: Color -> (Int, Int, Int)
toColorInt (Vec3 r g b) = (floor (255.99 * clamp r), floor (255.99 * clamp g), floor (255.99 * clamp b))
    where clamp x = max 0.0 (min 1.0 x)

-- Output the image in PPM format
writePPM :: FilePath -> [[Color]] -> IO ()
writePPM filename image = writeFile filename $ unlines $
    ["P3", show width ++ " " ++ show height, "255"] ++
    [unwords [show r, show g, show b] | row <- image, (r, g, b) <- map toColorInt row]
    where
        height = length image
        width = length (head image)

Examples

Here’s an example where we render two spheres and a cube:

main :: IO ()
main = do
    let width = 1024
    height = 768
    shapes = [ ShapeWrapper (Sphere (Vec3 (-1.0) 0 (-1)) 0.5 (Vec3 0.8 0.3 0.3) 0.5),  -- Red sphere
               ShapeWrapper (Sphere (Vec3 1 0 (-1)) 0.5 (Vec3 0.3 0.8 0.3) 0.5),       -- Green sphere
               ShapeWrapper (Cube (Vec3 (-0.5) (-0.5) (-2)) (Vec3 0.5 0.5 (-1.5)) (Vec3 0.8 0.8 0.0) 0.5)  -- Yellow cube
             ]
    image = render width height shapes
    writePPM "output.ppm" image

Simple Scene

Conclusion

In this post, we’ve built a simple raytracer in Haskell that supports basic shapes like spheres and cubes. You can extend this to add more complex features like shadows, lighting models, and textured surfaces. Happy ray tracing!

The full code is available here as a gist:

Unit testing your C code

Introduction

Writing reliable and maintainable code is a fundamental part of software development, and unit testing is one of the most effective ways to ensure your code works as expected. Unit tests help catch bugs early, ensure that changes to the codebase don’t introduce new issues, and serve as a form of documentation for your code’s expected behavior.

In this article, we’ll explore how to set up and use Google Test (also known as googletest), a popular C++ testing framework, to test your C and C++ code. We’ll walk through the installation process, demonstrate basic assertions, and then dive into testing a more complex feature—the variant data type.


Installation and Setup

Google Test makes it easy to write and run unit tests. It integrates well with build systems like CMake, making it straightforward to include in your project. Let’s go step-by-step through the process of installing and setting up Google Test in a CMake-based project.

Step 1: Add Google Test to Your Project

First, we need to download and include Google Test in the project. One of the easiest ways to do this is by adding Google Test as a subdirectory in your project’s source code. You can download the source directly from the Google Test GitHub repository.

Once you have the source, place it in a lib/ directory within your project. Your directory structure should look something like this:

my_project/
├── lib/
│   └── googletest-1.14.0/
├── src/
│   └── [source files...]
├── CMakeLists.txt

Step 2: Modify the CMake File

Now that you have Google Test in your project, let’s modify your CMakeLists.txt file to integrate it. Below is an example of a CMake configuration that sets up Google Test and links it to your test suite:

project(tests)

# Add Google Test as a subdirectory
add_subdirectory(lib/googletest-1.14.0)

# Include directories for Google Test and your source files
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
include_directories(../src)

# Add the executable that will run the tests
add_executable(Google_Tests_run
src/tests1.cpp
src/tests2.cpp
src/tests3.cpp)

# Link the Google Test library and any other necessary libraries
target_link_libraries(Google_Tests_run gtest gtest_main myproject)

This CMake setup includes Google Test in your project by adding it as a subdirectory, and it links your test suite to the gtest and gtest_main libraries. Now you’re ready to write and run unit tests!

Step 3: Build and Run the Tests

To compile the tests, simply run the following commands from the root of your project directory:

mkdir build
cd build
cmake ..
make

Once the build is complete, you can run your tests with:

./Google_Tests_run

This command will execute all the tests defined in your test files. Now that the environment is set up, let’s move on to writing some unit tests using Google Test.

Basic Assertions with Google Test

Before diving into testing our variant data type, let’s explore some of the basic assertions provided by Google Test. Assertions are used to check that a particular condition holds true during test execution. If the condition is false, the test fails.

Common Assertions

Here are some of the most commonly used assertions:

  • EXPECT_EQ(val1, val2): Checks that val1 is equal to val2.
  • EXPECT_NE(val1, val2): Checks that val1 is not equal to val2.
  • EXPECT_TRUE(condition): Checks that the condition is true.
  • EXPECT_FALSE(condition): Checks that the condition is false.
  • ASSERT_EQ(val1, val2): Like EXPECT_EQ, but if the assertion fails, it aborts the current function.

Let’s look at a simple example that tests basic operations:

#include "gtest/gtest.h"

TEST(SimpleTests, BasicAssertions) {
// Expect equality
EXPECT_EQ(1 + 1, 2);

    // Expect inequality
    EXPECT_NE(1 + 1, 3);
    
    // Expect true condition
    EXPECT_TRUE(4 > 2);
    
    // Expect false condition
    EXPECT_FALSE(2 > 4);
}

When you run this test, Google Test will evaluate each assertion and output the result. If any assertion fails, it will print a detailed message showing the expected and actual values.

Now that you’ve seen how to use basic assertions, let’s move on to testing a more complex feature: the variant data type.

Testing the Variant Data Type

In a previous post we explored creating our own variant data type. This piece of library code should provide us with some good examples on how to apply unit tests.

With the variant being able to hold multiple types (integers, floats, strings, etc.), we need to test that each type is correctly handled by the variant and behaves as expected.

Here’s an example test that checks if the ced_var_new_int8 function correctly creates an 8-bit integer variant:

#include "gtest/gtest.h"
#include "ced.h"

namespace {
TEST(VariantTests, ConstructInt8) {
ced_var_p var = ced_var_new_int8(1);

        EXPECT_EQ(var->__info.type, reflect_type_variant);
        EXPECT_EQ(var->type, ced_var_type_int8);
        EXPECT_EQ(var->data._int8, 1);
    
        ced_var_free(var);
    }
}

This test ensures that:

  1. The variant type is correctly set to ced_var_type_int8.
  2. The integer value stored in the variant is 1.

You can follow this pattern to test other data types supported by the variant, ensuring that each type is correctly initialized and behaves as expected.

In the next section, we’ll walk through more examples of testing different variant types and introduce more complex tests for arrays and type conversions.

More Tests!

Now that we’ve covered the basics of using Google Test, let’s look at some examples of how to apply these concepts to test our variant data type. We won’t go through every single test, but we’ll highlight a few that demonstrate different key behaviors—constructing basic types, handling arrays, and type conversion.

Constructing Basic Types

One of the simplest tests you can write is to verify that a variant can be properly constructed with a specific data type. This ensures that the ced_var_new_* functions correctly initialize the variant.

For example, here’s a test that checks if we can create an 8-bit integer variant:

TEST(VariantTests, ConstructInt8) {
ced_var_p var = ced_var_new_int8(1);

    EXPECT_EQ(var->__info.type, reflect_type_variant);  // Check the variant type
    EXPECT_EQ(var->type, ced_var_type_int8);            // Ensure it's an int8 variant
    EXPECT_EQ(var->data._int8, 1);                      // Check the stored value

    ced_var_free(var);  // Don't forget to free the variant!
}

This test checks the following:

  1. The variant’s type is correctly set to ced_var_type_int8.
  2. The data inside the variant is the expected integer value.
  3. The variant is freed properly at the end to avoid memory leaks.

Handling Arrays of Variants

Another important feature of the variant data type is its ability to hold arrays of other variants. Testing this involves creating an array, verifying its size, and ensuring each element in the array holds the correct value.

Here’s an example that constructs an array of variants and tests its contents:

TEST(VariantTests, ConstructArray) {
    ced_var_p arr[] = {
        ced_var_new_int8(10),
        ced_var_new_int16(500),
        ced_var_new_int32(100000),
        ced_var_new_int64(10000000000),
        ced_var_new_string("Howdy!")
    };

    ced_var_p var = ced_var_new_array(arr, 5);

    EXPECT_EQ(var->type, ced_var_type_array);  // Check it's an array
    EXPECT_EQ(var->size, 5);                   // Check the size of the array

    // Clean up memory for both the array and its contents
    for (int i = 0; i < 5; ++i) {
        ced_var_free(arr[i]);
    }
    ced_var_free(var);
}

In this test, we:

  1. Create an array of variants, each holding different types (integers and a string).
  2. Verify that the variant we created is indeed an array.
  3. Check that the size of the array is correct.
  4. Clean up the memory for each individual variant and the array as a whole.

Type Conversion and Safety

Variants allow us to convert between different types, but not all conversions are valid. We should ensure that the type conversion logic works correctly and fails gracefully when an invalid conversion is attempted.

Let’s look at a test that checks a valid conversion, and another that ensures a failed conversion returns NULL:

Successful Type Conversion

TEST(VariantTests, AsType) {
ced_var_p var = ced_var_new_int8(1);
ced_var_p new_var = ced_var_as_type(var, ced_var_type_int16);

    EXPECT_EQ(new_var->type, ced_var_type_int16);  // Ensure it's now int16
    EXPECT_EQ(new_var->data._int16, 1);            // Check the value after conversion

    ced_var_free(var);
    ced_var_free(new_var);
}

This test checks that:

  1. The original 8-bit integer is correctly converted into a 16-bit integer.
  2. The value remains unchanged after conversion.

Failed Type Conversion

TEST(VariantTests, AsTypeFail) {
ced_var_p var = ced_var_new_int64(1);
ced_var_p new_var = ced_var_as_type(var, ced_var_type_int8);

    EXPECT_EQ(new_var == NULL, true);  // Check that the conversion failed

    ced_var_free(var);
}

In this test:

  1. We attempt to convert a 64-bit integer into an 8-bit integer, which is not possible.
  2. The conversion returns NULL, indicating the failure, and we verify this with EXPECT_EQ.

These are just a few examples of the types of unit tests you can write for your variant data type. By covering basic type construction, handling arrays, and ensuring type conversion behaves as expected, we’ve demonstrated how to use Google Test to validate the functionality of complex C code.

Conclusion

Unit testing is a critical part of ensuring the reliability and correctness of your code. By integrating Google Test into your C/C++ projects, you can create a robust testing suite that not only catches bugs early but also provides confidence in the stability of your codebase.

With the ability to handle various types, arrays, and even type conversions, our variant data type is a powerful tool, and Google Test helps ensure it works exactly as intended. Whether you’re dealing with basic types or more complex features, writing clear, concise unit tests like the ones shown here will go a long way in maintaining high-quality code.

Make Your Own Variant Data Type

Introduction

In software development, we often encounter scenarios where we need to store or manipulate data of varying types—integers, strings, floating points, and more. Typically, each data type is handled separately, but what if you could encapsulate different types within a single structure? This is where a variant data type comes in.

A variant is a type-safe container that can hold any type of value, while keeping track of what type it currently holds. This makes variants incredibly useful in situations where your data structure needs to handle multiple data types dynamically, such as in scripting languages, serialization systems, or general-purpose containers.

In this article, we’ll walk through how to implement your own variant data type in C. We’ll start by defining the types that our variant can handle, move on to constructing the variant itself, and finish with operations like cloning, converting, and freeing variants. The goal is to provide you with a reusable component that can serve as a foundation for more complex systems, such as interpreters, data structures, or even custom languages.

Defining a Variant Data Type

The first step in implementing a variant data type is to define what types the variant can hold. In C, we can use an enum to list all the possible types we want to support. For our variant, we’ll handle everything from basic types like integers and floats to more complex types like strings and arrays.

We’ll also define a union within our variant structure. The union allows us to store different data types in the same memory space while ensuring that we only ever use one at a time, depending on the type of the variant.

Here’s the enum and the union for our variant type:

typedef enum {
ced_var_type_null = 0,
ced_var_type_int8 = 1,
ced_var_type_uint8 = 2,
ced_var_type_int16 = 3,
ced_var_type_uint16 = 4,
ced_var_type_int32 = 5,
ced_var_type_uint32 = 6,
ced_var_type_int64 = 7,
ced_var_type_uint64 = 8,
ced_var_type_bool = 9,
ced_var_type_float = 10,
ced_var_type_double = 11,
ced_var_type_string = 12,
ced_var_type_pointer = 13,
ced_var_type_array = 14,
ced_var_type_dict = 15,
} ced_var_type_t;

typedef struct ced_var_t {
ced_var_type_t type;
size_t size;

    union {
        int8_t _int8;
        uint8_t _uint8;
        int16_t _int16;
        uint16_t _uint16;
        int32_t _int32;
        uint32_t _uint32;
        int64_t _int64;
        uint64_t _uint64;
        int _bool;
        float _float;
        double _double;
        char* _string;
        void* _pointer;
        struct ced_var_t **_array;
    } data;
} ced_var_t, *ced_var_p;

Type Enumeration

The enum defines constants for each supported type, allowing us to track the type of data that the variant currently holds. By assigning each type a unique value, we can ensure that the variant correctly interprets the data in its union.

For example:

  • ced_var_type_int8 corresponds to an 8-bit signed integer.
  • ced_var_type_string corresponds to a string pointer.

These constants will be key when handling conversions or operations that depend on the data type.

Union for Data Storage

At the heart of the variant structure is a union. The union allows us to store multiple data types in the same memory space, but only one at a time. By combining this union with the type field from the enum, we always know which type the variant currently holds.

Here’s what the union includes:

  • Integer types like int8_t, int16_t, and so on.
  • Floating-point types like float and double.
  • Complex types like char* for strings and void* for pointers.
  • Arrays of variants (for holding lists or other complex data).

The union ensures that the variant is memory-efficient, as only one of these types will occupy the memory at any given time.

Memory and Size Tracking

The size field allows us to track the size of the data that the variant is holding. This is especially important for types like strings or arrays, where the size of the content can vary.

For basic types like int32_t, the size is fixed and known in advance, but for strings or arrays, this field gives us the ability to manage memory dynamically. As we handle more complex data types, this size tracking becomes crucial to avoid memory leaks and ensure proper memory management.

Usage

Now that we’ve defined the variant data type, let’s look at how to create and manage these variants. This section will walk through constructing and tearing down a variant to ensure proper memory management and usage.

Construction

Creating a variant is straightforward. We provide helper functions that allow us to construct variants for different types. These functions allocate memory for the variant and initialize it with the appropriate data.

For example, here’s how you would create a variant that holds an 8-bit integer:

ced_var_p my_int8_var = ced_var_new_int8(42);

This function creates a variant with the type ced_var_type_int8, sets its value to 42, and returns a pointer to the new variant. Similarly, we can construct variants for other types like strings, booleans, and floating points:

ced_var_p my_string_var = ced_var_new_string("Hello, Variant!");
ced_var_p my_bool_var = ced_var_new_bool(1);
ced_var_p my_float_var = ced_var_new_float(3.14f);

Each of these functions ensures that the correct type is assigned and memory is allocated to store the value.

Creating Arrays

You can also create more complex variants, such as arrays of variants. The ced_var_new_array function allows you to pass an array of variants and the number of elements, constructing a variant that holds an array:

ced_var_p array_items[3];
array_items[0] = ced_var_new_int32(10);
array_items[1] = ced_var_new_string("Array Element");
array_items[2] = ced_var_new_bool(0);

ced_var_p my_array_var = ced_var_new_array(array_items, 3);

In this example, the array variant will hold three different elements: an integer, a string, and a boolean.

Tear Down

As with any dynamically allocated memory in C, it’s important to free the memory when you’re done using a variant. Failing to do so will result in memory leaks. Each variant, whether it’s a basic type or an array, must be freed using the ced_var_free function:

ced_var_free(my_int8_var);
ced_var_free(my_string_var);
ced_var_free(my_bool_var);
ced_var_free(my_float_var);

When dealing with arrays or more complex structures like dictionaries, ced_var_free will recursively free all elements within the array or dictionary, ensuring that all memory is properly cleaned up:

ced_var_free(my_array_var);

In this case, the function will free each element within the array before freeing the array itself.

Important Notes on Memory Management

  • Strings: Strings are dynamically allocated when a variant is created, so make sure to free the variant holding the string when you’re done with it.
  • Arrays: Arrays of variants can grow large, and freeing them requires freeing each individual variant inside the array. The ced_var_free function handles this for you, but it’s good practice to be aware of the potential overhead.

By ensuring that every variant is constructed properly and freed once it’s no longer needed, you can manage dynamic types safely and efficiently in your applications.

Back to the Real World

Now that we’ve built our variant data type and explored how to construct and tear it down, let’s bring it into a real-world scenario. A variant data type is most useful when you need to handle dynamic types interchangeably without knowing in advance what type of data you’re working with. Let’s see how we can use variants in practical applications and seamlessly interchange them with native C data types.

Working with Native Data Types

One key feature of our variant type is that it allows us to work with various data types dynamically and convert between them when needed. Let’s take a look at some common examples of interchanging variant types with native C data types.

Example 1: Converting Variants to Native Types

Suppose you have a variant containing an integer, and you want to use this integer in a C function that expects a native int32_t. Using the ced_var_as_int32 function, we can safely convert the variant to its corresponding native type:

ced_var_p my_variant = ced_var_new_int32(100);

int32_t native_int = ced_var_as_int32(my_variant)->data._int32;
printf("Native int value: %d\n", native_int);

In this case, the variant holds a 32-bit integer. We retrieve it using ced_var_as_int32 and extract the native integer value from the data field. Now, we can use it as we would any regular int32_t.

Example 2: Converting Between Types

Sometimes, you might want to convert from one type to another. For example, you have a floating-point value stored in a variant, and you need to convert it to an integer for use in some part of your application:

ced_var_p my_float_variant = ced_var_new_float(3.14159f);

// Convert the variant to an int32
ced_var_p int_variant = ced_var_as_int32(my_float_variant);

// Extract the integer value
int32_t native_int = int_variant->data._int32;
printf("Converted int value: %d\n", native_int);

Here, the ced_var_as_int32 function attempts to convert the float to an integer. This example illustrates how variants make dynamic type handling seamless, allowing you to move between types without much friction.

Example 3: Working with Complex Types

Beyond simple data types, our variant can handle more complex types like strings and arrays. Suppose we want to extract a string from a variant and use it as a native C string:

ced_var_p my_string_variant = ced_var_new_string("Hello, Variants!");

// Extract the string from the variant
const char* native_string = ced_var_as_string(my_string_variant)->data._string;
printf("Native string value: %s\n", native_string);

In this case, ced_var_as_string gives us the native C string pointer, which can then be passed around and used in the same way as any other char* in C.

Example 4: Handling Arrays

Finally, let’s demonstrate handling an array of mixed types. We can create a variant array, add different data types to it, and extract the native values from each element:

ced_var_p array_items[2];
array_items[0] = ced_var_new_int32(42);
array_items[1] = ced_var_new_string("Variant in an Array");

ced_var_p my_array_variant = ced_var_new_array(array_items, 2);

// Extract and print the integer from the first element
int32_t array_int = ced_var_as_int32(ced_var_array_get(my_array_variant, 0))->data._int32;
printf("Array int value: %d\n", array_int);

// Extract and print the string from the second element
const char* array_string = ced_var_as_string(ced_var_array_get(my_array_variant, 1))->data._string;
printf("Array string value: %s\n", array_string);

In this example, we see how a variant array can hold multiple types, and we extract and use each native value as needed.

Conclusion

With our variant data type, we’ve created a powerful tool that allows us to work dynamically with multiple data types in C, interchanging them seamlessly. Whether you’re working with integers, floating points, strings, or even arrays, the variant provides a flexible and type-safe way to manage data without requiring explicit type knowledge at compile-time.

This flexibility can be especially useful in systems where data types are not known in advance, such as scripting engines, serialization systems, or general-purpose data structures. By interchanging variants with native data types, we unlock a wide range of possibilities for dynamic and flexible programming in C.

A full implementation of this variant data type can be found in my ced library up on GitHub.