Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
15aa08e
feat: add v3 of extension api with host callbacks
miljann995 May 27, 2026
ffc94f4
feat: remove extension name from parameters list
miljann995 May 27, 2026
effc829
Revert "feat: remove extension name from parameters list"
miljann995 May 27, 2026
4aa44e8
fix: adjust parameters
miljann995 May 28, 2026
d62a9de
chore: comments
miljann995 May 28, 2026
0adb869
feat: add test
miljann995 May 28, 2026
058e1a0
chore: comments
miljann995 May 28, 2026
6c2ba56
fix: add version check
miljann995 May 28, 2026
7060815
fix: set LogXEvent callback to nullptr if not provided by the host
miljann995 May 28, 2026
7329d03
fix: address nullptr concern
miljann995 May 28, 2026
8d50d54
fix: add version check
miljann995 May 28, 2026
80a7076
fix: address code review comments
miljann995 May 28, 2026
44b3f23
fix: address code review comments
miljann995 May 28, 2026
87b8636
fix: avoid race condition on concurrent cleanups
miljann995 May 28, 2026
dfc2d4d
fix: type mismatch
miljann995 May 28, 2026
01d9889
fix: address race condition concerns
miljann995 May 28, 2026
456aa78
fix: avoid dangling pointer of host callbacks
miljann995 May 28, 2026
011520d
feat: extract trace level into separate header file
miljann995 May 28, 2026
e726883
fix: extract trace level enum in a separate header file
miljann995 May 29, 2026
74d7a5b
fix: use caller-supplied extension name, if provided, or default exte…
miljann995 May 29, 2026
b5c7417
fix: forward compatibility on host callback versions
miljann995 May 29, 2026
2f9a8c9
fix: forward compatibility test
miljann995 May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions extension-host/include/sqlextensionhostcallbacks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//*********************************************************************
// Copyright (c) Microsoft Corporation.
//
// Host callbacks protocol between the host and extensions.
// Optional API surface introduced alongside v3 of the Extension API.
//
// @File: sqlextensionhostcallbacks.h
//
//*********************************************************************

#ifndef __SQLEXTENSIONHOSTCALLBACKS
#define __SQLEXTENSIONHOSTCALLBACKS

#include "sqlexternallanguage.h"
#include "sqlextensiontracelevel.h"

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

// Callback function provided by the host for logging XEvents from Extension.
//
typedef void (*PFunc_ExtensionLogXEvent)(
const SQLCHAR *ExtensionName,
SQLULEN ExtensionNameLength,
SQLGUID SessionId,
SQLUSMALLINT TaskId,
SQLUSMALLINT TraceLevel,
SQLINTEGER ErrorCode,
const SQLCHAR *Message,
SQLULEN MessageLength
);

// Host callbacks structure passed from host to Extension.
//
#define SQLEXTENSION_HOST_CALLBACKS_VERSION_1 1
#define SQLEXTENSION_HOST_CALLBACKS_MIN_SUPPORTED_VERSION SQLEXTENSION_HOST_CALLBACKS_VERSION_1

typedef struct _SQLEXTENSION_HOST_CALLBACKS
Comment thread
miljann995 marked this conversation as resolved.
{
// Highest SQLEXTENSION_HOST_CALLBACKS_VERSION_* the host populates.
// Extension must validate before reading version-gated fields.
//
SQLUSMALLINT Version;

// Explicit padding so SizeInBytes is naturally 4-byte aligned regardless
// of compiler packing settings. Must be zero.
//
SQLUSMALLINT Reserved0;

// sizeof(SQLEXTENSION_HOST_CALLBACKS) as the host saw it at build time.
// Extension must validate this is greater or equal the size of every field it intends
// to read. Lets a newer Extension safely run against an older host that supplied a smaller struct.
//
SQLUINTEGER SizeInBytes;

// Version 1 callbacks.
//
PFunc_ExtensionLogXEvent LogXEvent;

// Reserved for future expansion. Zero-initialized by the host. Extension must not read or call these.
//
void *Reserved1;
void *Reserved2;
} SQLEXTENSION_HOST_CALLBACKS;

// Optional API (v3+)
// Receives host callback functions from the host.
//
SQLEXTENSION_INTERFACE
SQLRETURN SetHostCallbacks(
SQLEXTENSION_HOST_CALLBACKS *Callbacks
);

#ifdef __cplusplus
} /* End of extern "C" { */
#endif/* __cplusplus */

#endif/* __SQLEXTENSIONHOSTCALLBACKS */
26 changes: 26 additions & 0 deletions extension-host/include/sqlextensiontracelevel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//*********************************************************************
// Copyright (c) Microsoft Corporation.
//
// ExtensionTraceLevel enum shared between the host and extensions.
//
// @File: sqlextensiontracelevel.h
//
//*********************************************************************

#ifndef __SQLEXTENSIONTRACELEVEL
#define __SQLEXTENSIONTRACELEVEL

// Trace levels for events logged via the LogXEvent host callback.
// Lowest numeric value is the most severe, matching the Windows ETW
// TRACE_LEVEL_* convention.
//
enum ExtensionTraceLevel
{
Extension_Critical = 1,
Extension_Error = 2,
Extension_Warning = 3,
Extension_Information = 4,
Extension_Verbose = 5
};

#endif /* __SQLEXTENSIONTRACELEVEL */
2 changes: 1 addition & 1 deletion extension-host/include/sqlexternallanguage.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
extern "C" {
#endif /* __cplusplus */

Comment thread
miljann995 marked this conversation as resolved.
#define EXTERNAL_LANGUAGE_EXTENSION_API 2
#define EXTERNAL_LANGUAGE_EXTENSION_API 3
Comment thread
miljann995 marked this conversation as resolved.
Comment thread
miljann995 marked this conversation as resolved.

SQLEXTENSION_INTERFACE
SQLUSMALLINT GetInterfaceVersion();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <sql.h>
#include "sqlexternallanguage.h"
#include "sqlexternallibrary.h"
#include "sqlextensionhostcallbacks.h"
#include "DotnetEnvironment.h"

// Returns the string with the address of the first character and length
Expand Down Expand Up @@ -142,6 +143,17 @@ SQLEXTENSION_INTERFACE SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT
//
SQLEXTENSION_INTERFACE SQLRETURN Cleanup();

// Dotnet environment pointer
// Receives host callback function pointers from the host.
// Optional API, supported since v3 of the Extension API.
//
static DotnetEnvironment* g_dotnet_runtime = nullptr;
SQLEXTENSION_INTERFACE SQLRETURN SetHostCallbacks(
SQLEXTENSION_HOST_CALLBACKS *hostCallbacks);

// Dotnet environment pointer. Defined in nativecsharpextension.cpp.
//
extern DotnetEnvironment* g_dotnet_runtime;

// Host callbacks pointer provided by the host via SetHostCallbacks.
// Defined in nativecsharpextension.cpp.
//
Comment thread
miljann995 marked this conversation as resolved.
extern SQLEXTENSION_HOST_CALLBACKS* g_hostCallbacks;
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,105 @@ public static short GetOutputParam(
});
}

/// <summary>
/// Delegate type matching the host's LogXEvent callback signature.
/// </summary>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LogXEventCallbackDelegate(
byte *extensionName,
ulong extensionNameLength,
Guid sessionId,
ushort taskId,
ushort traceLevel,
int errorCode,
byte *message,
ulong messageLength);

/// <summary>
/// Managed representation of the SQLEXTENSION_HOST_CALLBACKS structure.
/// Must match the native struct layout exactly.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct SqlExtensionHostCallbacks
{
public ushort Version;
public IntPtr LogXEvent;
}

/// <summary>
/// Minimal SQLEXTENSION_HOST_CALLBACKS version this extension understands.
/// If host callbacks version is greater than this, extension will still parse and read
/// known host callbacks and ignore unknown fields, allowing forward compatibility.
/// </summary>
///
private const ushort MinSupportedHostCallbacksVersion = 1;

/// <summary>
/// This delegate declares the delegate type of SetHostCallbacks.
/// </summary>
public delegate short SetHostCallbacksDelegate(
SqlExtensionHostCallbacks *hostCallbacks);

/// <summary>
/// This method implements SetHostCallbacks API.
/// Receives a pointer to the host callbacks structure, reads the callback
/// function pointers during this call, and stores any needed managed
/// delegates so managed code can call back into the host.
/// </summary>
/// <param name="hostCallbacks">
/// Pointer to the SQLEXTENSION_HOST_CALLBACKS structure provided by the host.
/// </param>
/// <returns>
/// SQL_SUCCESS(0), SQL_ERROR(-1)
/// </returns>
public static short SetHostCallbacks(
SqlExtensionHostCallbacks *hostCallbacks)
{
Logging.Trace("CSharpExtension::SetHostCallbacks");
return ExceptionUtils.WrapError(() =>
Comment thread
miljann995 marked this conversation as resolved.
{
if (hostCallbacks == null)
{
throw new ArgumentNullException(nameof(hostCallbacks));
}

// Validate the struct version before reading any version-gated fields.
//
if (hostCallbacks->Version < MinSupportedHostCallbacksVersion)
{
Logging.Error(
"CSharpExtension::SetHostCallbacks: unsupported host callbacks version: " +
hostCallbacks->Version);

throw new NotSupportedException(
"Unsupported SQLEXTENSION_HOST_CALLBACKS version: " +
hostCallbacks->Version);
}

if (hostCallbacks->LogXEvent != IntPtr.Zero)
{
var logXEvent = Marshal.GetDelegateForFunctionPointer<LogXEventCallbackDelegate>(
hostCallbacks->LogXEvent);
Logging.SetLogXEventCallback(logXEvent);
Comment thread
miljann995 marked this conversation as resolved.
Comment thread
miljann995 marked this conversation as resolved.

Logging.LogXEvent(
extensionName: null,
Guid.Empty,
taskId: 0,
traceLevel: Logging.TraceLevel.Information,
errorCode: 0,
"CSharp extension loaded, host callbacks registered (version " + hostCallbacks->Version + ")");
}
Comment thread
miljann995 marked this conversation as resolved.
else
{
// Host opted out of XEvent logging.
// Clear any previously stored delegate.
//
Logging.SetLogXEventCallback(null);
}
});
}

/// <summary>
/// This delegate declares the delegate type of CleanupSession.
/// </summary>
Expand Down
117 changes: 117 additions & 0 deletions language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ class Logging
/// </summary>
private const int StdErr = 2;

/// <summary>
/// Host-provided LogXEvent callback, set via SetHostCallbacks.
/// Marked volatile so concurrent readers in LogXEvent always
/// observe the latest write from SetLogXEventCallback.
/// </summary>
private static volatile CSharpExtension.LogXEventCallbackDelegate _logXEventCallback;

/// <summary>
/// Static constructor to initialize the custom text writers for stdout and stderr.
/// This ensures that any Console.WriteLine or Console.Error.WriteLine calls
Expand Down Expand Up @@ -74,6 +81,116 @@ public static void Error(string message)
}
}

/// <summary>
/// Stores the host-provided LogXEvent callback for later use.
/// Called from CSharpExtension.SetHostCallbacks.
/// </summary>
/// <param name="callback">The delegate wrapping the host's LogXEvent function pointer.</param>
public static void SetLogXEventCallback(CSharpExtension.LogXEventCallbackDelegate callback)
{
_logXEventCallback = callback;
}

/// <summary>
/// Returns true if the host has provided a LogXEvent callback.
/// </summary>
public static bool HasLogXEventCallback => _logXEventCallback != null;

/// <summary>
/// Trace levels for events logged via the host's LogXEvent callback.
/// Lowest numeric value is the most severe, matching Windows ETW TRACE_LEVEL_* convention.
Comment thread
SicongLiu2000 marked this conversation as resolved.
/// </summary>
public enum TraceLevel : ushort
{
Critical = 1,
Error = 2,
Warning = 3,
Comment thread
SicongLiu2000 marked this conversation as resolved.
Information = 4,
Verbose = 5,
}

/// <summary>
/// Default name of the Extension to be used for XEvent logging when
/// the caller does not supply one.
/// </summary>
private const string DefaultExtensionName = "CSharp";

/// <summary>
/// Logs a message through the host's XEvent infrastructure.
/// If no host callback is registered, this is a no-op.
/// </summary>
/// <param name="extensionName">Extension name.</param>
/// <param name="sessionId">Session GUID.</param>
/// <param name="taskId">Task identifier.</param>
/// <param name="traceLevel">Trace severity.</param>
/// <param name="errorCode">Error code for non-informational logs.</param>
/// <param name="message">The message to log.</param>
public static unsafe void LogXEvent(
string extensionName,
Guid sessionId,
ushort taskId,
TraceLevel traceLevel,
int errorCode,
string message)
{
// Snapshot the callback once so a concurrent cleanup between the null-check and the invocation
// cannot turn this into a NullReferenceException.
//
CSharpExtension.LogXEventCallbackDelegate callback = _logXEventCallback;
if (callback == null)
{
return;
}

// Ensure message is not null to avoid issues during UTF-8 encoding.
string safeMessage = message ?? string.Empty;

// Convert the message to a UTF-8 byte array for native interop.
byte[] utf8MessageBytes = Encoding.UTF8.GetBytes(safeMessage);

// Capture the real byte length.
ulong messageLen = (ulong)utf8MessageBytes.Length;

// As `fixed` on a zero-length array yields a null pointer,
// validate that the byte array is not empty.
//
if (utf8MessageBytes.Length == 0)
{
utf8MessageBytes = new byte[] { 0 };
}

// Use the caller-supplied Extension name when non-empty,
// otherwise fall back to the default value.
//
string safeExtensionName = string.IsNullOrEmpty(extensionName)
? DefaultExtensionName
: extensionName;
byte[] utf8ExtNameBytes = Encoding.UTF8.GetBytes(safeExtensionName);
ulong extNameLen = (ulong)utf8ExtNameBytes.Length;

// Call the host's LogXEvent callback with the prepared parameters.
try
{
fixed (byte* pExtName = utf8ExtNameBytes)
fixed (byte* pMessage = utf8MessageBytes)
{
callback(
pExtName,
extNameLen,
sessionId,
taskId,
(ushort)traceLevel,
errorCode,
pMessage,
messageLen);
}
}
catch (Exception ex)
{
Debug.WriteLine($"LogXEvent host callback threw: {ex}");
}
Comment thread
SicongLiu2000 marked this conversation as resolved.
Comment thread
miljann995 marked this conversation as resolved.
}

/// <summary>
/// Custom TextWriter that writes directly to the standard output/error streams
/// using native system calls. This is required because the standard Console
Expand Down
Loading