Skip to content

Conversation

@breyed
Copy link
Contributor

@breyed breyed commented Nov 2, 2025

  • Supports posting methods to the synchronization context.
  • Events by default are forwarded directly to the handler on the same thread. However, they can be configured to be posted (useful for events raised on a background thread or to avoid reentrant access).
  • When a task method completes, the status is posted on the main thread.
  • C# async/await uses the synchronization context by default, causing continuations to run on the main thread. .NET code that can run safely on a background thread can opt to do so with ConfigureAwait(false).

This PR eliminates background thread calls into FoxPro and C# continuations, avoiding race conditions and data tearing, and providing a deterministic runtime environment.

* Supports posting methods to the synchronization context.
* Events by default are forwarded directly to the handler on the same thread. However, they can be configured to be posted (useful for events raised on a background thread or to avoid reentrant access).
* When a task method completes, the status is posted on the main thread.
* C# async/await uses the synchronization context by default, causing continuations to run on the main thread. .NET code that can run safely on a background thread can opt to do so with `ConfigureAwait(false)`.

This commit eliminates background thread calls into FoxPro and C# continuations, avoiding race conditions and data tearing, and providing a deterministic runtime environment.
Copilot AI review requested due to automatic review settings November 2, 2025 04:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for synchronizing .NET method calls and events with the FoxPro main thread using a custom synchronization context. This enables posting .NET method invocations to the FoxPro message queue to avoid reentrancy issues in event handlers and handle callbacks from background threads.

Key changes:

  • Introduced FoxProSynchronizationContext to marshal .NET callbacks to the FoxPro main thread via Windows messages
  • Added PostMethod and PostStaticMethod functions to queue method invocations instead of executing them immediately
  • Refactored EventSubscriber to support posting events to the synchronization context and simplified the event subscription model

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
DotnetBridge/wwDotNetBridge.csproj Enabled latest C# language version to support modern syntax features
DotnetBridge/wwDotNetBridge.cs Added synchronization context setup, posting support for method invocations, and reorganized using statements
DotnetBridge/Utilities/FoxProSynchronizationContext.cs New class implementing SynchronizationContext for FoxPro thread marshaling
DotnetBridge/Utilities/EventSubscriber.cs Refactored event subscription to support synchronization context posting and simplified event handling
Distribution/wwDotnetBridge.PRG Added FoxPro-side synchronization context setup, new PostMethod/PostStaticMethod functions, and updated event subscription API
Comments suppressed due to low confidence (4)

DotnetBridge/Utilities/FoxProSynchronizationContext.cs:39

  • Poor error handling: empty catch block.
                try { postedEvent.handler(postedEvent.state); } catch { }

DotnetBridge/wwDotNetBridge.cs:770

  • Variable args may be null at this access as suggested by this null check.
                if (args[i] is ComValue)

DotnetBridge/wwDotNetBridge.cs:1038

  • Variable args may be null at this access as suggested by this null check.
                if (args[i] is ComValue)

DotnetBridge/Utilities/FoxProSynchronizationContext.cs:39

  • Generic catch clause.
                try { postedEvent.handler(postedEvent.state); } catch { }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

/// <summary>
/// Sets the current synchronization content to use the FoxPro main thread.
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'content' to 'context' in the documentation comment.

Suggested change
/// Sets the current synchronization content to use the FoxPro main thread.
/// Sets the current synchronization context to use the FoxPro main thread.

Copilot uses AI. Check for mistakes.
return _synchronizationContext.PostMessageId;
}

public void Dispatch() => _synchronizationContext.Dispatch();
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference exception. The _synchronizationContext field can be null if SetSynchronizationContext is never called, but Dispatch() dereferences it unconditionally. Add a null check or document that SetSynchronizationContext must be called first.

Suggested change
public void Dispatch() => _synchronizationContext.Dispatch();
public void Dispatch()
{
if (_synchronizationContext == null)
throw new InvalidOperationException("SynchronizationContext has not been set. Call SetSynchronizationContext() before Dispatch().");
_synchronizationContext.Dispatch();
}

Copilot uses AI. Check for mistakes.
internal object InvokeStaticMethod_Internal(string TypeName, string method, params object[] args)
{
if (PostInvokedMethods)
{
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference exception. When PostInvokedMethods is true, _synchronizationContext is accessed but may be null if SetSynchronizationContext was not called. Add a null check or validation.

Suggested change
{
{
if (_synchronizationContext == null)
throw new InvalidOperationException("SynchronizationContext is not set. Call SetSynchronizationContext before using PostInvokedMethods.");

Copilot uses AI. Check for mistakes.
Comment on lines +990 to +993
if (PostInvokedMethods)
{
PostMethod_Internal(instance, method, args);
return null;
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference exception in PostMethod_Internal. When PostInvokedMethods is true, the method calls _synchronizationContext.Post which will fail with a null reference if SetSynchronizationContext was not called.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
sealed class FoxProSynchronizationContext(int hwnd, object taskWaiter) : SynchronizationContext
{
private readonly IntPtr _hwnd = (IntPtr)hwnd;
private readonly ConcurrentQueue<(SendOrPostCallback handler, object? state)> _postQueue = [];
private readonly dynamic _taskWaiter = taskWaiter;
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _taskWaiter field is declared but never used in the class. Consider removing it or documenting its intended future use.

Suggested change
sealed class FoxProSynchronizationContext(int hwnd, object taskWaiter) : SynchronizationContext
{
private readonly IntPtr _hwnd = (IntPtr)hwnd;
private readonly ConcurrentQueue<(SendOrPostCallback handler, object? state)> _postQueue = [];
private readonly dynamic _taskWaiter = taskWaiter;
sealed class FoxProSynchronizationContext(int hwnd) : SynchronizationContext
{
private readonly IntPtr _hwnd = (IntPtr)hwnd;
private readonly ConcurrentQueue<(SendOrPostCallback handler, object? state)> _postQueue = [];

Copilot uses AI. Check for mistakes.
*** loHandler: An object with a method OnEvent(loEventName, loParams).
*** loHandler: An object with handler methods corresponding to each event to subscribe to.
*** lcPrefix: The initial part of the event handler function for each event. Defaults to "On".
*** llPost: When true, events are posted to the synchronization context. Useful for events raised on a background thread or to avoid reentrency.
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'reentrency' to 'reentrancy'.

Suggested change
*** llPost: When true, events are posted to the synchronization context. Useful for events raised on a background thread or to avoid reentrency.
*** llPost: When true, events are posted to the synchronization context. Useful for events raised on a background thread or to avoid reentrancy.

Copilot uses AI. Check for mistakes.
/// <summary>
/// Gets the ID of the Windows message posted when a task is ready to run.
/// </summary>
public int PostMessageId { get; private set; } = RegisterWindowMessage("FoxProSynchronizationContextDispatch");
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this call with a call to managed code if possible.

Copilot uses AI. Check for mistakes.
public override void Post(SendOrPostCallback d, object? state)
{
_postQueue.Enqueue((d, state));
PostMessage(_hwnd, PostMessageId, IntPtr.Zero, IntPtr.Zero);
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this call with a call to managed code if possible.

Copilot uses AI. Check for mistakes.

[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("user32.dll", CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true)]
private static extern int RegisterWindowMessage(string lpString);
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.

[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
[DllImport("user32.dll", SetLastError = true)]
private static extern bool PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minimise the use of unmanaged code.

Copilot uses AI. Check for mistakes.
@breyed breyed closed this Nov 2, 2025
@breyed breyed deleted the synchronization-contenxt branch November 2, 2025 04:34
@breyed
Copy link
Contributor Author

breyed commented Nov 2, 2025

Superseded by #42. (The branch name was spelled wrong, and Github closed the PR when it got renamed.}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant