# ExeWatch SDK - DLL Version

**SDK Version: 0.22.1** | **ABI Version: 4**

Use the ExeWatch SDK from **any Delphi version** (Delphi 5 through Delphi 13 Florence) or any language that can call Windows DLLs.

## Supported languages and toolchains

The DLL exports an `extern "C"` + `__stdcall` + `wchar_t*` + `#pragma pack(1)` surface — the lowest-common-denominator Windows ABI. Verified working with:

- **Delphi 5 through 13 Florence** (via the bundled `ExeWatchSDKv1Imports.pas`)
- **Embarcadero C++Builder**
- **Microsoft Visual C++** (MSVC / `cl.exe`, VS 2022 Build Tools tested with `/W4` = zero warnings)
- **Python 3.7+** via `ctypes` (though if you're writing a real Python app, the native [ExeWatch Python SDK](https://exewatch.com/ui/docs#python-sdk) is much nicer — `pip install exewatch`)

Expected to work without modification in any Windows-FFI-capable runtime: **MinGW-w64 gcc/g++**, **Clang on Windows**, **Rust** (`extern "stdcall"`), **Go** (`syscall.NewLazyDLL`), **C#** (`DllImport` with `CallingConvention.StdCall`), **VBA** (`Declare Function`), **AutoIt** (`DllCall`), and friends.

## Runnable samples

Full samples live in the public [exewatchsamples](https://github.com/danieleteti/exewatchsamples) repository:

- **C++Builder** VCL → `CPPBuilderWithDLLSDK/`
- **Microsoft Visual C++** console → `MSVCWithDLLSDK/`
- **Delphi** VCL via `ExeWatchSDKv1Imports.pas` → `DelphiWithDLLSDK/`

Each sample is a self-contained project with realistic init/log/breadcrumbs/timing/metrics patterns — not just ABI smoke tests.

## Files

| File | Description |
|------|-------------|
| `ExeWatchSDKv1DLL.dll` | 32-bit DLL (Win32) |
| `ExeWatchSDKv1DLL_x64.dll` | 64-bit DLL (Win64) |
| `ExeWatchSDKv1Imports.pas` | Delphi import unit (Delphi 5+ compatible) |
| `ExeWatchSDKv1.h` | C/C++ header (MSVC, MinGW, Clang, C++Builder) — supports static + dynamic loading via `#define EW_DYNAMIC_LOAD` |
| `ExeWatchSDKv1.dynload.c` | Optional C/C++ runtime-loader implementation (`LoadLibrary` + `GetProcAddress`) |

## Quick Start

### 1. Copy files to your project

Copy the appropriate DLL and `ExeWatchSDKv1Imports.pas` to your project directory.

### 2. Add the import unit

```pascal
uses
  ExeWatchSDKv1Imports;
```

### 3. Initialize and use

**Delphi 2009+ (Unicode Delphi):**
```pascal
// Using convenience wrappers (recommended)
EWInitialize('ew_win_your_api_key', 'customer-123', '1.0.0');
EWInfo('Application started');
EWDebug('Loading config', 'startup');
ew_Shutdown;
```

**Delphi 5/6/7/2005/2006/2007 (pre-Unicode):**
```pascal
// Option A: Use convenience wrappers (handle conversion automatically)
EWInitialize('ew_win_your_api_key', 'customer-123', '1.0.0');
EWInfo('Application started');

// Option B: Direct ew_* calls with manual WideString cast
ew_Initialize(
  PWideChar(WideString('ew_win_your_api_key')),
  PWideChar(WideString('customer-123')),
  PWideChar(WideString('1.0.0'))
);
ew_Info(PWideChar(WideString('Application started')), nil);
```

### 4. Shutdown before exit

```pascal
ew_Shutdown;
```

## Loading Modes

### Static Linking (default)

The DLL must be present when your application starts. If missing, the app won't launch.

```pascal
uses
  ExeWatchSDKv1Imports;  // Links at load time
```

### Dynamic Loading

Define `EW_DYNAMIC_LOAD` **before** the unit is included. The DLL is loaded on demand, allowing graceful degradation.

```pascal
{$DEFINE EW_DYNAMIC_LOAD}
uses
  ExeWatchSDKv1Imports;

// In your code:
if EWLoadDLL then
begin
  EWInitialize('ew_win_your_api_key', 'customer-123');
  EWInfo('SDK loaded successfully');
end
else
  ShowMessage('ExeWatch DLL not found - monitoring disabled');

// On shutdown:
ew_Shutdown;
EWUnloadDLL;
```

You can also specify a custom DLL path:
```pascal
EWLoadDLL('C:\MyApp\Libs\ExeWatchSDKv1DLL.dll');
```

## Advanced Configuration

Use `ew_InitializeEx` for full control over SDK settings:

```pascal
var
  Config: TEWDLLConfig;
begin
  EWConfigInit(Config);  // Fill with defaults
  Config.ApiKey := PWideChar(WideString('ew_win_your_api_key'));
  Config.CustomerId := PWideChar(WideString('customer-123'));
  Config.AppVersion := PWideChar(WideString('2.0.0'));
  Config.BufferSize := 50;
  Config.FlushIntervalMs := 10000;  // 10 seconds
  Config.SampleRate := 0.5;         // 50% sampling
  ew_InitializeEx(@Config);
end;
```

## Error Handling

All `ew_*` functions return an integer error code:

| Code | Constant | Meaning |
|------|----------|---------|
| 0 | `EW_OK` | Success |
| -1 | `EW_ERR_NOT_INITIALIZED` | SDK not initialized |
| -2 | `EW_ERR_ALREADY_INITIALIZED` | Already initialized |
| -3 | `EW_ERR_INVALID_PARAM` | Nil or invalid parameter |
| -4 | `EW_ERR_EXCEPTION` | Internal exception |
| -5 | `EW_ERR_BUFFER_TOO_SMALL` | Output buffer too small |
| -6 | `EW_ERR_VERSION_MISMATCH` | DLL is **older** than the import unit / header requires (see *Versioning & Upgrades*) |

Get the last error message:
```pascal
var
  Msg: WideString;
begin
  Msg := EWGetLastErrorStr;
  // or manually:
  // ew_GetLastError(@Buf[0], Length(Buf));
end;
```

## Versioning & Upgrades

The DLL reports an integer **ABI version** via `ew_GetABIVersion` (and the
header/import unit declares the version it was built against in
`EW_IMPORT_ABI_VERSION`). The compatibility check is **forward-compatible**:

> A host app accepts any DLL whose ABI is **greater than or equal to** the
> version it was built against. It rejects (`EW_ERR_VERSION_MISMATCH`) only a
> DLL that is **older** than it requires.

This means you can **drop a newer DLL next to an existing app and it just
works** — no recompiling the host. It holds because every ABI bump is
**additive only**: new releases only *add* exports (existing ones keep their
signatures), and `TEWDLLConfig` only grows by appending fields. `ew_InitializeEx`
zero-extends an older/smaller `TEWDLLConfig`, so a config laid out for an earlier
version still initializes cleanly against a newer DLL (the new tail fields take
their defaults).

What you still need to keep in sync:

- **A newer app needs a DLL at least as new.** If you build against ABI *N*, ship
  a DLL whose ABI ≥ *N*. An older DLL is missing exports the app may call, so it
  is correctly rejected.
- **Apps already shipped against an older, strict import** (ABI ≤ 3, before this
  forward-compatible check existed) will still reject a newer DLL — that check is
  compiled into their binary and cannot be changed retroactively. Re-link such
  apps against the current import unit / header once; from then on, DLL-only
  upgrades are smooth.

**For maintainers of the DLL itself:** keep every ABI bump additive — only add
exports, never change an existing export's signature, and only append fields to
`TEWDLLConfig` (each new field must treat `0`/`nil` as its default so
zero-extension picks the right value). Bump both `EW_DLL_ABI_VERSION` and
`EW_IMPORT_ABI_VERSION` together. A genuinely breaking change would require a
different strategy than this single monotonic version.

## API Reference

### Logging

```pascal
ew_Log(Level, Msg, Tag, ExtraDataJson);  // Full control
ew_Debug(Msg, Tag);     // Shortcut for debug level
ew_Info(Msg, Tag);      // Shortcut for info level
ew_Warning(Msg, Tag);   // Shortcut for warning level
ew_Error(Msg, Tag);     // Shortcut for error level
ew_Fatal(Msg, Tag);     // Shortcut for fatal level

// Error with stack trace (for exception handlers)
ew_ErrorWithStackTrace(Msg, Tag, StackTrace, ExceptionClass);
```

`Tag` can be `nil` (defaults to `'main'`). `ExtraDataJson` is optional JSON: `'{"key":"value"}'`.

### Exception Handling with Stack Trace

> **Who captures the stack trace?** Automatic, native stack-trace capture is only available
> in the SDKs that can hook the host runtime directly: the **Delphi Native SDK**
> (`Exception.GetExceptionStackInfoProc`), the **.NET SDK** (`Exception.StackTrace` +
> `AppDomain.UnhandledException`), the **Python SDK** (`traceback`) and the **JavaScript SDK**
> (`Error.stack`). **The DLL SDK does NOT capture the stack automatically** — it runs as a
> library inside a foreign host (C++Builder, MSVC, .NET, Rust, …) and cannot hook that host's
> runtime. So with the **DLL SDK the stack trace must be obtained by the host application** and
> handed to ExeWatch via `ew_ErrorWithStackTrace`. (Note: a .NET app using the DLL is in the
> DLL-SDK case — it must pass `ex.ToString()` itself; this is different from the managed .NET SDK.)

The DLL cannot hook into the host application's RTL, so stack traces must be captured
by the host app and passed explicitly:

```pascal
// In Application.OnException or try/except:
procedure OnAppException(Sender: TObject; E: Exception);
var
  StackTrace: string;
begin
  // If using JCL:  StackTrace := JclDebug.GetExceptionStackInfoAsString(E);
  // If using madExcept: StackTrace := madExcept.GetCrashStackTrace;
  // Without a library: pass empty string (exception class + message still logged)
  StackTrace := '';
  EWErrorWithStackTrace(E.Message, 'exception', StackTrace, E.ClassName);
end;
```

The SDK won't attempt its own stack trace capture when one is provided.

> **Note — no VCL/FMX-hook warning in the DLL.** The native SDK warns a GUI EXE if no
> `ExeWatchSDKv1.VCL`/`.FMX` hook is installed. The DLL **does not** emit this warning
> (the advice is inapplicable — a non-Delphi host can't add Delphi units, and there's no
> exported hook-register API). With the DLL the host is responsible for wiring its own
> handler (e.g. `Application.OnException` in a Delphi/C++Builder host, or the host
> framework's unhandled-exception event) and calling `ew_ErrorWithStackTrace`.

### Breadcrumbs

```pascal
ew_AddBreadcrumb(EW_BT_CLICK, 'ui', 'Button clicked', nil);
ew_AddBreadcrumb(EW_BT_NAVIGATION, 'nav', 'Opened settings', nil);
ew_ClearBreadcrumbs;
```

### Timing / Profiling

```pascal
ew_StartTiming(PWideChar('db-query'), nil);
// ... do work ...
ew_EndTiming(PWideChar('db-query'), ElapsedMs);
// ElapsedMs now contains duration in milliseconds

// Stack-based (LIFO):
ew_StartTiming(PWideChar('operation'), nil);
ew_EndLastTiming(ElapsedMs);
```

### Nested timing traces (profiler-style tree)

A *trace* turns flat timings into a profiler-style tree. Between `ew_StartTrace`
and `ew_EndTrace`, every `ew_StartTiming`/`ew_EndTiming` pair on the same thread
becomes a child span; the dashboard reconstructs the tree and shows it as a
waterfall. Reusing the same timing id in a loop merges those siblings into one
node (count + avg/min/max). Synchronous / single-thread by design.

```pascal
var
  Buf: array[0..63] of WideChar;
  TraceId: WideString;
  ElapsedMs, Total: Double;
begin
  // ew_StartTrace fills a caller buffer with the 16-hex trace id (two-call
  // size pattern, like ew_GetCustomerId). Or use the EWStartTrace wrapper,
  // which returns the id as a string.
  ew_StartTrace(PWideChar('GenerateReport'), @Buf[0], Length(Buf));
  TraceId := WideString(PWideChar(@Buf[0]));

  ew_StartTiming(PWideChar('LoadCustomers'), PWideChar('db'));
  // ... work ...
  ew_EndTiming(PWideChar('LoadCustomers'), ElapsedMs);

  ew_EndTrace(Total);   // closes the root and ships the whole tree
end;
```

A nested `ew_StartTrace` while a trace is already open degrades to a child span
(returns the active trace id); `ew_EndTrace` force-closes any child still open.
Plain `ew_StartTiming` *without* a surrounding trace keeps working exactly as
before — the trace API is purely additive.

### Metrics

```pascal
ew_IncrementCounter(PWideChar('api_calls'), 1.0, nil);
ew_RecordGauge(PWideChar('memory_mb'), 256.5, nil);
```

### User Identity

```pascal
ew_SetUser(PWideChar('user-42'), PWideChar('john@example.com'), PWideChar('John'));
ew_ClearUser;
```

### Tags

```pascal
ew_SetTag(PWideChar('environment'), PWideChar('production'));
ew_RemoveTag(PWideChar('environment'));
ew_ClearTags;
```

### Customer ID

```pascal
ew_SetCustomerId(PWideChar('new-customer-456'));
```

### Device Info

```pascal
ew_SendDeviceInfo;  // Re-send hardware info
ew_SetCustomDeviceInfo(PWideChar('license'), PWideChar('enterprise'));
ew_SendCustomDeviceInfo;
```

### Callbacks

```pascal
procedure MyErrorHandler(ErrorMsg: PWideChar); stdcall;
begin
  WriteLn('ExeWatch error: ', WideString(ErrorMsg));
end;

// Register callback
ew_SetOnError(@MyErrorHandler);
```

**Important:** Callbacks may fire on a background thread (the shipper thread). If you need to update the UI from a callback, use `PostMessage`, `TThread.Synchronize`, or your framework's thread-safe mechanism.

## Feature Comparison

| Feature | Source SDK (.pas) | DLL Version |
|---------|:-----------------:|:-----------:|
| Logging (all levels) | Yes | Yes |
| Breadcrumbs | Yes | Yes |
| Timing/Profiling | Yes | Yes |
| Nested timing traces | Yes | Yes |
| Counters & Gauges | Yes | Yes |
| User Identity | Yes | Yes |
| Global Tags | Yes | Yes |
| Device Info | Yes | Yes |
| Custom Device Info | Yes | Yes |
| Customer ID | Yes | Yes |
| Callbacks | Yes | Yes |
| Format-style logging | Yes | No* |
| RegisterPeriodicGauge | Yes | No* |
| ErrorWithException | Yes | No* |
| ClientListener | Yes | No* |
| VCL/FMX exception hooks | Yes | No* |

\* These features require Delphi language constructs that cannot cross a DLL boundary safely. Use alternatives:
- **Format-style logging**: Format the string before calling `ew_Log`
- **PeriodicGauge**: Call `ew_RecordGauge` from a TTimer instead
- **ErrorWithException**: Extract `E.Message` and call `ew_Error`
- **VCL exception hook**: Install `Application.OnException` and call `ew_Error` from there

## Building from Source

Requires Delphi 12 Athens or later. The DLL project compiles the full SDK internally.

```bash
# 32-bit
dcc32 -NSSystem;System.Win;Winapi;System.Net;Data.Win -U<lib-path>/win32/release ExeWatchSDKv1DLL.dpr

# 64-bit
dcc64 -NSSystem;System.Win;Winapi;System.Net;Data.Win -U<lib-path>/win64/release ExeWatchSDKv1DLL.dpr
```
