|
| 1 | +--- |
| 2 | +alwaysApply: true |
| 3 | +description: PIMPL (Pointer to Implementation) pattern for platform-specific code |
| 4 | +--- |
| 5 | + |
| 6 | +# PIMPL Pattern Rules |
| 7 | + |
| 8 | +The nativeapi library uses the PIMPL (Pointer to Implementation) idiom extensively to hide platform-specific implementation details from the public API. This provides binary compatibility, reduces compilation dependencies, and enables clean platform abstraction. |
| 9 | + |
| 10 | +## What is PIMPL? |
| 11 | + |
| 12 | +PIMPL separates a class's interface from its implementation by: |
| 13 | +1. Declaring a private nested `Impl` class in the header |
| 14 | +2. Storing only a pointer to the implementation |
| 15 | +3. Implementing platform-specific logic in the source files |
| 16 | + |
| 17 | +## Pattern Structure |
| 18 | + |
| 19 | +### Header File Pattern ([window.h](mdc:src/window.h)) |
| 20 | + |
| 21 | +```cpp |
| 22 | +#pragma once |
| 23 | +#include <memory> |
| 24 | + |
| 25 | +namespace nativeapi { |
| 26 | + |
| 27 | +class Window { |
| 28 | +public: |
| 29 | + Window(); |
| 30 | + Window(void* native_window); |
| 31 | + virtual ~Window(); |
| 32 | + |
| 33 | + // Public interface methods |
| 34 | + void Show(); |
| 35 | + void Hide(); |
| 36 | + bool IsVisible() const; |
| 37 | + |
| 38 | +private: |
| 39 | + // Forward declaration only - no definition |
| 40 | + class Impl; |
| 41 | + |
| 42 | + // Pointer to implementation |
| 43 | + std::unique_ptr<Impl> pimpl_; |
| 44 | +}; |
| 45 | + |
| 46 | +} // namespace nativeapi |
| 47 | +``` |
| 48 | + |
| 49 | +**Key Points:** |
| 50 | +- Forward declare `Impl` class - don't define it in header |
| 51 | +- Use `std::unique_ptr<Impl>` for automatic cleanup |
| 52 | +- No platform-specific includes in header |
| 53 | +- No platform-specific types in public interface |
| 54 | + |
| 55 | +### Platform-Specific Implementation Files |
| 56 | + |
| 57 | +Each platform provides its own `Impl` definition: |
| 58 | + |
| 59 | +#### Windows Implementation ([platform/windows/window_windows.cpp](mdc:src/platform/windows)) |
| 60 | + |
| 61 | +```cpp |
| 62 | +#include <windows.h> // Platform includes only in .cpp |
| 63 | +#include "../../window.h" |
| 64 | + |
| 65 | +namespace nativeapi { |
| 66 | + |
| 67 | +// Define Impl class with platform-specific members |
| 68 | +class Window::Impl { |
| 69 | +public: |
| 70 | + Impl(HWND hwnd) : hwnd_(hwnd) {} |
| 71 | + |
| 72 | + HWND hwnd_; // Windows-specific handle |
| 73 | + // Other Windows-specific state... |
| 74 | +}; |
| 75 | + |
| 76 | +Window::Window() : pimpl_(std::make_unique<Impl>(nullptr)) {} |
| 77 | + |
| 78 | +Window::Window(void* window) |
| 79 | + : pimpl_(std::make_unique<Impl>(static_cast<HWND>(window))) {} |
| 80 | + |
| 81 | +Window::~Window() = default; // unique_ptr handles cleanup |
| 82 | + |
| 83 | +void Window::Show() { |
| 84 | + if (pimpl_->hwnd_) { |
| 85 | + ShowWindow(pimpl_->hwnd_, SW_SHOW); |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +} // namespace nativeapi |
| 90 | +``` |
| 91 | + |
| 92 | +#### macOS Implementation ([platform/macos/window_macos.mm](mdc:src/platform/macos)) |
| 93 | + |
| 94 | +```objc |
| 95 | +#import <Cocoa/Cocoa.h> // Platform includes only in .mm |
| 96 | +#include "../../window.h" |
| 97 | + |
| 98 | +namespace nativeapi { |
| 99 | + |
| 100 | +// Define Impl class with macOS-specific members |
| 101 | +class Window::Impl { |
| 102 | +public: |
| 103 | + Impl(NSWindow* window) : window_(window) {} |
| 104 | + |
| 105 | + NSWindow* window_; // macOS-specific handle |
| 106 | + // Other macOS-specific state... |
| 107 | +}; |
| 108 | + |
| 109 | +Window::Window() : pimpl_(std::make_unique<Impl>(nil)) {} |
| 110 | + |
| 111 | +Window::Window(void* window) |
| 112 | + : pimpl_(std::make_unique<Impl>(static_cast<NSWindow*>(window))) {} |
| 113 | + |
| 114 | +Window::~Window() = default; |
| 115 | + |
| 116 | +void Window::Show() { |
| 117 | + if (pimpl_->window_) { |
| 118 | + [pimpl_->window_ makeKeyAndOrderFront:nil]; |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +} // namespace nativeapi |
| 123 | +``` |
| 124 | + |
| 125 | +#### Linux Implementation ([platform/linux/window_linux.cpp](mdc:src/platform/linux)) |
| 126 | + |
| 127 | +```cpp |
| 128 | +#include <gtk/gtk.h> // Platform includes only in .cpp |
| 129 | +#include "../../window.h" |
| 130 | + |
| 131 | +namespace nativeapi { |
| 132 | + |
| 133 | +// Define Impl class with GTK-specific members |
| 134 | +class Window::Impl { |
| 135 | +public: |
| 136 | + Impl(GtkWidget* window) : window_(window) {} |
| 137 | + |
| 138 | + GtkWidget* window_; // GTK-specific handle |
| 139 | + // Other GTK-specific state... |
| 140 | +}; |
| 141 | + |
| 142 | +Window::Window() : pimpl_(std::make_unique<Impl>(nullptr)) {} |
| 143 | + |
| 144 | +Window::Window(void* window) |
| 145 | + : pimpl_(std::make_unique<Impl>(static_cast<GtkWidget*>(window))) {} |
| 146 | + |
| 147 | +Window::~Window() = default; |
| 148 | + |
| 149 | +void Window::Show() { |
| 150 | + if (pimpl_->window_) { |
| 151 | + gtk_widget_show(pimpl_->window_); |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +} // namespace nativeapi |
| 156 | +``` |
| 157 | + |
| 158 | +## Implementation Guidelines |
| 159 | + |
| 160 | +### 1. Creating a New PIMPL Class |
| 161 | + |
| 162 | +When adding a new cross-platform class: |
| 163 | + |
| 164 | +```cpp |
| 165 | +// my_class.h |
| 166 | +#pragma once |
| 167 | +#include <memory> |
| 168 | + |
| 169 | +namespace nativeapi { |
| 170 | + |
| 171 | +class MyClass { |
| 172 | +public: |
| 173 | + MyClass(); |
| 174 | + virtual ~MyClass(); |
| 175 | + |
| 176 | + void DoSomething(); |
| 177 | + |
| 178 | +private: |
| 179 | + class Impl; |
| 180 | + std::unique_ptr<Impl> pimpl_; |
| 181 | +}; |
| 182 | + |
| 183 | +} // namespace nativeapi |
| 184 | +``` |
| 185 | + |
| 186 | +Then create implementations for each platform: |
| 187 | +- `src/platform/windows/my_class_windows.cpp` |
| 188 | +- `src/platform/macos/my_class_macos.mm` |
| 189 | +- `src/platform/linux/my_class_linux.cpp` |
| 190 | + |
| 191 | +### 2. Accessing Platform State |
| 192 | + |
| 193 | +Always access platform-specific members through `pimpl_`: |
| 194 | + |
| 195 | +```cpp |
| 196 | +// Good |
| 197 | +void Window::SetTitle(const std::string& title) { |
| 198 | + if (pimpl_->hwnd_) { // Access through pimpl_ |
| 199 | + SetWindowTextW(pimpl_->hwnd_, ...); |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +// Bad - won't compile, hwnd_ not in public interface |
| 204 | +void Window::SetTitle(const std::string& title) { |
| 205 | + if (hwnd_) { // Error: no member named 'hwnd_' |
| 206 | + ... |
| 207 | + } |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +### 3. Constructor/Destructor Pattern |
| 212 | + |
| 213 | +Follow this pattern for all PIMPL classes: |
| 214 | + |
| 215 | +```cpp |
| 216 | +// Header |
| 217 | +class MyClass { |
| 218 | +public: |
| 219 | + MyClass(); |
| 220 | + virtual ~MyClass(); // Virtual if used as base class |
| 221 | + |
| 222 | + // Copy/move operations - handle appropriately |
| 223 | + MyClass(const MyClass&) = delete; |
| 224 | + MyClass& operator=(const MyClass&) = delete; |
| 225 | + |
| 226 | +private: |
| 227 | + class Impl; |
| 228 | + std::unique_ptr<Impl> pimpl_; |
| 229 | +}; |
| 230 | + |
| 231 | +// Implementation |
| 232 | +MyClass::MyClass() : pimpl_(std::make_unique<Impl>()) {} |
| 233 | +MyClass::~MyClass() = default; // unique_ptr handles cleanup |
| 234 | +``` |
| 235 | + |
| 236 | +### 4. Manager Classes with PIMPL |
| 237 | + |
| 238 | +Singleton managers also use PIMPL ([window_manager.h](mdc:src/window_manager.h)): |
| 239 | + |
| 240 | +```cpp |
| 241 | +class WindowManager : public EventEmitter<WindowEvent> { |
| 242 | +public: |
| 243 | + static WindowManager& GetInstance(); |
| 244 | + virtual ~WindowManager(); |
| 245 | + |
| 246 | + std::shared_ptr<Window> Create(const WindowOptions& options); |
| 247 | + |
| 248 | +private: |
| 249 | + WindowManager(); // Private constructor |
| 250 | + |
| 251 | + class Impl; |
| 252 | + std::unique_ptr<Impl> pimpl_; |
| 253 | + |
| 254 | + // Public data that doesn't vary by platform |
| 255 | + std::unordered_map<WindowID, std::shared_ptr<Window>> windows_; |
| 256 | +}; |
| 257 | +``` |
| 258 | + |
| 259 | +Platform setup and cleanup methods: |
| 260 | + |
| 261 | +```cpp |
| 262 | +// Header |
| 263 | +private: |
| 264 | + void SetupEventMonitoring(); |
| 265 | + void CleanupEventMonitoring(); |
| 266 | + |
| 267 | +// Implementation forwards to pimpl_ |
| 268 | +WindowManager::WindowManager() : pimpl_(std::make_unique<Impl>(this)) { |
| 269 | + SetupEventMonitoring(); |
| 270 | +} |
| 271 | + |
| 272 | +void WindowManager::SetupEventMonitoring() { |
| 273 | + pimpl_->SetupEventMonitoring(); |
| 274 | +} |
| 275 | +``` |
| 276 | + |
| 277 | +## Common Patterns |
| 278 | + |
| 279 | +### Pattern 1: Wrapping Native Objects |
| 280 | + |
| 281 | +Constructors that accept native handles ([window.h](mdc:src/window.h)): |
| 282 | + |
| 283 | +```cpp |
| 284 | +// Header |
| 285 | +class Window { |
| 286 | +public: |
| 287 | + Window(); |
| 288 | + Window(void* native_window); // Wrap existing native window |
| 289 | +}; |
| 290 | + |
| 291 | +// Windows implementation |
| 292 | +Window::Window(void* window) |
| 293 | + : pimpl_(std::make_unique<Impl>(static_cast<HWND>(window))) {} |
| 294 | + |
| 295 | +// macOS implementation |
| 296 | +Window::Window(void* window) |
| 297 | + : pimpl_(std::make_unique<Impl>(static_cast<NSWindow*>(window))) {} |
| 298 | +``` |
| 299 | + |
| 300 | +### Pattern 2: Checking for Null Native Handles |
| 301 | + |
| 302 | +Always validate before using platform handles: |
| 303 | + |
| 304 | +```cpp |
| 305 | +void Window::Show() { |
| 306 | + if (!pimpl_->hwnd_) return; // Or pimpl_->window_, pimpl_->gtk_window_ |
| 307 | + |
| 308 | + // Safe to use handle |
| 309 | + ShowWindow(pimpl_->hwnd_, SW_SHOW); |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +### Pattern 3: Returning Platform Handles |
| 314 | + |
| 315 | +Use `NativeObjectProvider` base class ([native_object_provider.h](mdc:src/foundation/native_object_provider.h)): |
| 316 | + |
| 317 | +```cpp |
| 318 | +// Header |
| 319 | +class Window : public NativeObjectProvider { |
| 320 | +protected: |
| 321 | + void* GetNativeObjectInternal() const override; |
| 322 | +}; |
| 323 | + |
| 324 | +// Windows implementation |
| 325 | +void* Window::GetNativeObjectInternal() const { |
| 326 | + return static_cast<void*>(pimpl_->hwnd_); |
| 327 | +} |
| 328 | + |
| 329 | +// macOS implementation |
| 330 | +void* Window::GetNativeObjectInternal() const { |
| 331 | + return static_cast<void*>(pimpl_->window_); |
| 332 | +} |
| 333 | +``` |
| 334 | + |
| 335 | +## Benefits of PIMPL |
| 336 | + |
| 337 | +1. **Binary Compatibility** - Implementation changes don't affect public API |
| 338 | +2. **Fast Compilation** - Platform headers not included in public headers |
| 339 | +3. **Clean Separation** - Platform code completely separated |
| 340 | +4. **Type Safety** - Compiler ensures correct platform build |
| 341 | +5. **No Leaks** - `unique_ptr` handles cleanup automatically |
| 342 | + |
| 343 | +## Best Practices |
| 344 | + |
| 345 | +1. **Always use `std::unique_ptr<Impl>`** - Never raw pointers |
| 346 | +2. **Define destructor in .cpp file** - Even if `= default`, needed for unique_ptr of incomplete type |
| 347 | +3. **Keep public headers clean** - No platform includes, no platform types |
| 348 | +4. **Validate handles** - Check for null before using native handles |
| 349 | +5. **Use forward declarations** - Minimize includes in headers |
| 350 | +6. **Follow naming** - Always name inner class `Impl`, always name member `pimpl_` |
| 351 | +7. **Consider copy/move** - Usually delete copy, sometimes allow move |
| 352 | +8. **Document constructors** - Especially those taking native handles |
| 353 | + |
| 354 | +## Related Patterns |
| 355 | + |
| 356 | +- See [Native Object Provider Rules](mdc:.cursor/rules/native-object-provider.mdc) for exposing native handles |
| 357 | +- See [Platform Implementation Rules](mdc:.cursor/rules/platform-implementation.mdc) for platform-specific code organization |
| 358 | +- See [Project Architecture Rules](mdc:.cursor/rules/project-architecture.mdc) for overall structure |
0 commit comments