Creating a Window with GLFW

Now that we’ve set up Metal on our machine, we need a way to display our pixel magic on the screen, but before we do that, we need a window.

Technically, we could use platform-specific code to open a window, create a rendering context, attach it to the window, and handle all the input events — like resizing, moving the window, and managing edge cases.
However, that approach is both complex to set up and beyond the scope of this tutorial series.

That’s exactly why we’ll use a powerful cross-platform library that takes care of all that for us — including window creation, input handling, and even support for OpenGL and Vulkan contexts, if you’re interested in those as well.

GLFW

GLFW is a lightweight, open-source library designed to create windows, manage OpenGL/Vulkan/Metal contexts, and handle user input — like keyboard, mouse, and gamepad events — in a platform-independent way.
It’s widely used in graphics applications and game development for its simplicity and minimal overhead.

GLFW can be linked into your project either statically or dynamically.

When you link it statically, the entire GLFW library is compiled directly into your application binary - this makes deployment easier since there’s no need to ship external dynamic libraries alongside your executable.
It also reduces the risk of runtime errors due to missing or incompatible library files on the user’s system.

Static linking on the other hand increases the size of your binary and requires you to recompile your application if you update GLFW.
In addition to this of course, you must include the GLFW license text when distributing your application.

Dynamic linking means your application loads the GLFW library at runtime from a separate file, such as a .dll on Windows, .dylib on macOS, or .so on Linux.

This keeps your binary smaller and allows you to update or replace the GLFW library without recompiling your entire project.
The downside is that you must ensure the dynamic library is correctly installed and accessible on the target system, which can complicate deployment and lead to runtime errors if the library is missing or incompatible.

Each method has trade-offs, and the choice depends on your project’s needs: static linking for simplicity and portability, dynamic linking for modularity and easier updates.

If you’re curious, you can take look at this, explaining things in a bit more detail.

To keep things simple and self-contained, we’ll be using GLFW’s static library for this tutorial.

The first thing we’ll be doing is downloading the library from the official website, specifically the precompiled binaries.
If you’re on a Mac with Apple Silicon (M1, M2, M3, or M4), don’t worry — the macOS x64 package also includes arm64 libraries, so you’re covered.

Once downloaded go ahead and move the glfw folder in the root folder of your project.

The only parts we really care about are the include folder, which contains the GLFW header files, and the lib directory corresponding to your operating system and architecture — for example, on Apple Silicon, that would be the lib-arm64 folder.

Now let’s set it up:

  • Go to Build SettingsHeader Search Paths, click +, and add the path to the GLFW include folder.
  • Then go to Library Search Paths and add the path to the appropriate lib folder.
  • Under Build PhasesLink Binary With Libraries, click +, choose Add Other..., and select the .a static library file from the correct lib folder for your architecture.

Congratulations! Now GLFW is successfully integrated into your project.

In these tutorials you’ll find both .mm and .cpp files - Why? The .mm extension tells the compiler we’re using Objective-C++, which allows us to seamlessly mix Objective-C and C++ code — something that’s essential when working with Cocoa (Apple’s native windowing system) and Metal together.

Let’s have a look at how we can go ahead and create a window now:

main.mm

#pragma once

#pragma once is a preprocessor directive that ensures a header file is included only once during compilation, no matter how many times it’s referenced.

This prevents multiple definition errors and redundant code processing, which can happen when the same header is included in several files, directly or indirectly.

It’s a modern alternative to traditional include guards like:

#ifndef MY_HEADER_H
#define MY_HEADER_H
// header contents here
#endif

Unlike include guards, #pragma once is shorter, cleaner, and less error-prone — though it’s not officially part of the C++ standard.
That said, it’s widely supported by all major compilers, including GCC, Clang, and MSVC, making it safe to use in nearly all modern projects.

However, if you’re working on a highly portable, cross-platform codebase where maximum compiler compatibility is critical, keep in mind that #pragma once may not be recognized by very old or obscure compilers.

#define GLFW_INCLUDE_NONE
#import <GLFW/glfw3.h>

This tells GLFW not to include any OpenGL or Vulkan headers for us when we include glfw3.h.

const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

Up next, we define two separate variables, to easily set our width and height, SCR_WIDTH and SCR_HEIGHT - note that we are using unsigned int - this is a common convention, since the window size can’t be negative.

On top of this, many graphics APIs often expect uint32_t or unsigned int for things like viewport size, framebuffer dimensions etc.. , so this is just another way to stay even more consistent with the code we write.

void glfwErrorCallback(int error, const char* description)
{
    std::cerr << "[GLFW ERROR] (" << error << "): " << description << std::endl;
}

This function defines a GLFW error callback — a handler GLFW calls whenever it encounters an error.

It takes two parameters: an int error code and a descriptive const char* description.
Inside, it prints the error code and description to the standard error stream (std::cerr) with a clear “[GLFW ERROR]” prefix. This helps you catch and log GLFW-specific errors in real time, making debugging much, much easier.

To use it, you register this function with GLFW via:

glfwSetErrorCallback(glfwErrorCallback);

But we’ll do this right after we initialize glfw correctly.

// Initialize GLFW library
if(!glfwInit())
{
    std::cerr << "[ERROR] Failed to initialize GLFW." << std::endl;
    glfwTerminate();          // Cleanup GLFW before exit
    std::exit(EXIT_FAILURE);  // Exit program with failure code
}

// Set the error callback function to handle GLFW errors
glfwSetErrorCallback(glfwErrorCallback);

// Tell GLFW not to create an OpenGL context (for Vulkan, Metal, etc.)
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

// Create a GLFW window with given width, height and title
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "My Window", nullptr, nullptr);

// Check if window creation failed
if (!window)
{
    std::cerr << "[ERROR] Failed to create GLFW window." << std::endl;
    glfwTerminate();          // Cleanup GLFW before exit
    std::exit(EXIT_FAILURE);  // Exit program with failure code
}

// Get actual framebuffer size (important for high-DPI displays)
int width, height;
glfwGetFramebufferSize(window, &width, &height);

So first things first, we need to call glfwInit() to initialize GLFW itself - if this fails, we log an error, clean up by calling glfwTerminate() and exit the program with failure;
right after this we set the GLFW error glfwErrorCallback to catch and log detailed GLFW errors automatically. Then, it calls

It configures GLFW with glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API), telling GLFW not to create an OpenGL context since we’ll be using Metal.

Next, we create the GLFW window with our specified width, height, and title.
I’ve also added glfwGetFramebufferSize to make it so the width and height of the framebuffer are the same all across our application - whether it’d be the window’s resolution, the metal layer size or the framebuffer’s size - this way everything is the same.

/ Main event loop: run until the window is closed
while (!glfwWindowShouldClose(window))
{
    glfwPollEvents(); // Process pending window events
    // clear the screen
    // render
}

This is our main loop.
It’s where our rendering logic will go, once we’ve finished setting up all of our data and we are ready to render it onto the screen.

// Cleanup and close the window
glfwDestroyWindow(window);
glfwTerminate();  // Terminate GLFW

return 0;
}

We then finish by exiting the main loop (currently stopped when we hit the window’s “x” button), destroy the GlfwWindow and terminate GLFW alltoghether.

Build And Run

Build (Cmd+B) and Run (Cmd+R) the project. If everything is set up correctly, you should see a window pop up ready to display some nice graphics!

Window

Download The Project Files

If you encounter issues with the code or simply want to test with the correct setup, you can download the latest project files below:

Download Project Files


Copyright © 2025 Gabriele Vierti. Distributed by an MIT license.