C Programming — All Over Again

C Programming — All Over Again

April 01, 2024
Get tips and best practices from Develeap’s experts in your inbox

Intro

Writing code has always been a passion of mine. From the first time I wrote C# in high school to learning C in college, I am always left astonished. The sheer power, knowledge base, and deep understanding needed; it’s heaven and hell altogether.

A new hobby I’ve picked up on my embedded journey is working with Arduino. So, I’ve decided to get back on the horse and improve my C programming skills as well.

However, it’s been a couple of years since I last wrote C code, and I’ve learned a thing or two about being a better developer since I first entered the DevOps dimension. For example, I learned the importance of testing and why pipelines are crucial to avoid mistakes.

The Goal

The main purpose of this article is to create a GitHub repository with everything needed to program C effectively and efficiently. By simply forking the repo, to begin with, my repository would be set up and ready to go for any other C project I (or anyone else) plan to do in the future.

The Plan

The plan is to create a “template” repository, generically set up with Docker, docker-compose, and pipelines ready to go. Therefore, in any future C projects I’d like to work on, I’ll have everything set up and leverage the ability to code from anywhere. In this article, I’ll go through:

  1. Development environment
  2. Ceedling framework
  3. CI/CD Pipelines using Github Actions

Develop using Docker

I can’t even count my fingers about how many times I’ve been to a college lecture and suddenly realized I’ve made an error on my assignment, and my laptop is not with me. I borrowed a friend’s laptop, which is obviously running a different platform than mine and does not contain the tools I needed. In the future, I will use Docker to avoid any of this.

So, to develop C according to my flavor, I need a Linux distribution as a base, a GCC compiler, Unity for unit testing, GDB for debugging, and Valgrind for memory leaks.

Ceedling project owner does offer a docker image, however the tool and the image (official) are both out-of-date, and present many security risks. So I ended up building my own image, and publishing it over DockerHub.

How to use?

# This will start the container and run it interactively,
# with /project-demo directory binded into the running container at /project
# edit files withing the container or use your host's editor
docker compose up --detach; docker exec --interactive --tt
y c-dev /bin/bash

Ceedling Framework

Ceedling offers more than just a plain building system, it combines multiple tools together, leveraging the powers of Unity, CMock & CExeption.

Unity

Unity is an xUnit-style test framework for unit testing C. It is written completely in C and is portable, quick, simple, expressive, and extensible. Specially designed for unit testing embedded systems, it is also useful for other types of testing. Unity allows us to write unit test and assert the behaviour of programs.

CMock

CMock is a framework for generating mocks/stubs based on a header API. Support for CMock is integrated into Unity. CMake automatically generates the required test runner file and the mock files.

CExeption

CException is a project released by Throw The Switch. CException is designed to provide simple exception handling in C using the familiar try/catch/throw syntax.

CException is implemented in ANSI C and is highly portable. As long as your system supports the standard library calls setjmp and longjmp, you can use CException in your project. If you’re looking for an exception library for embedded systems, CException is for you.

How to use?

# Create a new project using ceedling.
ceedling new project zMynxx

The following folder structure will be created, along with the configuration to build it correctly →project.yml.


zMynxx/
|-- project.yml
|-- src
`-- test
    `-- support
        `-- .gitkeep

3 directories, 2 files

Ceedling can also generate modules for us:

# Create a module (/src/demo_module.c, /src/demo_module.h, /test/test_demo_module.c) for us.
ceedling module:create[demo_module]

Run all test or create a release

# Run all test
ceedling test:all

# Test only the demo_module
ceedling test:demo_module

# Create a release - Make sure release is set to TRUE on your project.yml!
ceedling release

CI / CD Pipelines using GitHub Actions

Automation makes our lives easier, and helps us avoid mistakes and bugs. I’ll create a simple CI, CD pipeline using GitHub Actions.

CI:

#/.github/workflows/ci.yaml
---
name: CI

on:
  # Manually triggered testing
  workflow_dispatch:

  # Check every PR to main
  pull_request:
    branches:
      - main

jobs:
  docker-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build locally
        uses: docker/build-push-action@v5.1.0
        with:
          context: .
          file: Dockerfile.ubuntu
          platforms: linux/amd64
          push: false
          load: true
          tags: ${{ github.repo }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Run all Tests using Docker
        run: |
          docker run \
          --interactive \
          --rm \
          --volume ./project-demo:/project \
          ${{ github.repo }}:${{ github.sha }} \
          ceedling test:all

CD:

#/.github/workflows/cd.yaml
---
name: CD

on:
  # Publish on every approved PR
  pull_request:
    types:
      - closed
    branches:
      - main

permissions:
  id-token: write
  contents: write
  packages: write

jobs:
  check:
    name: check pr status
    runs-on: ubuntu-22.04
    steps:
      - name: check if PR is merged
        uses: zmynx/github-actions/.github/actions/git/check-merge@feature/gha

  cd:
    name: continuous-deployment
    needs: [check]
    runs-on: ubuntu-22.04
    outputs:
      new_tag: ${{ steps.semver.outputs.new_tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          fetch-depth: 0

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build locally
        uses: docker/build-push-action@v5.1.0
        with:
          context: .
          file: Dockerfile.ubuntu
          platforms: linux/amd64
          push: false
          load: true
          tags: ${{ github.repo }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Generate Release using Docker
        run: |
          docker run \
          --interactive \
          --rm \
          --volume ./project-demo:/project \
          ${{ github.repo }}:${{ github.sha }} \
          ceedling release

      - name: Semantic Versioning
        id: semverv2
        uses: zmynx/github-actions/.github/actions/git/semver-v2@feature/gha
        with:
          fallback: "0.1.0"
          prefix: "v"

      - name: Downcase PR body
        id: downcase
        shell: bash
        env:
          PR_BODY: ${{ github.event.pull_request.body }}
        run: echo "pr_body_lowercase=${PR_BODY,,}" >> $GITHUB_OUTPUT

      - name: Parse the body for any '#major' or '#minor' keywords
        id: new_tag
        shell: bash
        env:
          NEW_TAG: ${{ contains(steps.downcase.outputs.pr_body_lowercase, '#major') && steps.semverv2.outputs.major || contains(steps.downcase.outputs.pr_body_lowercase, '#minor') && steps.semverv2.outputs.minor || steps.semverv2.outputs.patch }}
        run: echo "new_tag=${NEW_TAG}" >> $GITHUB_OUTPUT

      - name: Publish GitHub Release
        uses: softprops/action-gh-release@v0.1.15
        with:
          tag_name: "${{ format('{0}{1}', 'v', steps.new_tag.outputs.new_tag) }}"
          files: |
            project-demo/build/artifacts/release/MyApp.out
            ceedling-0.32.0-d76db35.gem
            LICENSE

Bonus — Linting

Linting is the process of running a program that will analyse code for potential errors.

See lint on Wikipedia:

lint was the name originally given to a particular program that flagged some suspicious and non-portable constructs (likely to be bugs) in C language source code. The term is now applied generically to tools that flag suspicious usage in software written in any computer language.

We can take advantage of a tool named “Trunk”, to run linting checks as we wish:

#/.github/workflows/trunk-check.yaml
---
name: Pull Request Trunk Check
on: [pull_request]
concurrency:
  group: ${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

permissions: read-all

jobs:
  trunk_check:
    name: Trunk Check Runner
    runs-on: ubuntu-latest
    permissions:
      checks: write # For trunk to post annotations
      contents: read # For repo checkout

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Trunk Check
        uses: trunk-io/trunk-action@v1.1.10
Feel free to support and suggest improvements. Happy Coding!

Summary

Our Repository is all setup and ready for replications.

Update: In another article named “**Dockerfile — Kick it up a notch!”** I have taken this template to another level. If you wish to use it, click here and select the following:

Screenshot 2024-03-18 at 14.49.18.png

Read more about our advanced DevOps solutions

We’re Hiring!
Develeap is looking for talented DevOps engineers who want to make a difference in the world.