Skip to content

PHP with CLR stuff

Imagine all the C#/.NET objects were available to the PHP code ... including the entire .NET runtime. Today we'll glance round .NET threading, Tasks, and CLR debugging.

System.Threading.Tasks.Task

A task is an object that encapsulates a delegate, its state and the state of the task itself. Its delegate is invoked on a "context". C# language lets you to do crazy things with tasks, scheduling them on various contexts, awaiting them asynchronously, and so on.

We won't be doing that in PHP code. We'll "only" create a Task and we'll run it on a background thread while the main thread will continue executing its code.

The Code

We're writing standard .php file (i.e. main.php) with the following valid PHP code:

<?php

use System\Threading\Tasks\Task;
use System\Threading\CancellationToken;
use System\Threading\CancellationTokenSource;

First, we introduce some class aliases, so we can then write just short names in our code. Notice, that there is no difference between PHP class or .NET class. It's all the same for the compiler.

function main() {

Put our code into a global function main (the name does not matter). The reason is that the compiler works better with local variables - their types can be inferred better and compiled code is much more efficient.

    $source = new CancellationTokenSource();
    $token = $source->Token;

Now create a CancellationTokenSource object - we'll use to cancel the background task, as we usually do in C# programs.

    $task = Task::Run(
        function () use ($token) { ... },
        $token
    );

Here the magic happens; the line above creates a Task object with a delegate that will be executed on a thread pool, and runs it.

Under the hood, this PHP anonymous function is compiled as a static CLR method with the following signature:

// static void anonymous@function(Closure? <closure>, PhpValue token)

PeachPie runtime creates a Closure object which wraps delegate to this method together with the use $token argument. Then the PeachPie runtime builds in-memory Action delegate which calls the created Closure. Finally, the Task::Run(Action, CancellationToken) static method get invoked.

Inside the anonymous function ({ ... }) running on a background thread we can do whatever we want to. For exmaple running some computation and periodically check the $token for cancellation.

        for ($i = 0; ; $i++) {

            if ($token->IsCancellationRequested) {
                break;
            }
            usleep(100_000);
        }

After the task is created, we continue execution on the main thread. In this sample, we schedule the CancellationTokenSource to cancel after 1 second, and wait synchronously for the background Task $task to finish.

    $source->CancelAfter(/*miliseconds:*/ 1000);
    $task->Wait();

Disclaimer

Some of the features necessary for this code to run - like passing a struct CancellationToken between closures, calling methods on value types, or better overload resolution - were implemented in pre-release version of PeachPie 1.2.0-r17766. You'll need to grab PeachPie from sources or get access to our private NuGet feed with all the SDKs, Runtime, and Compiler prepared for you.

Building Code

As for any other C#/.NET project to run, we need a project file and dotnet SDK.

Make sure you have dotnet (at leats 8.0): https://dotnet.microsoft.com/en-us/download

Add the following project file (i.e. system-threading-tasks.msbuildproj):

<Project Sdk="Peachpie.NET.Sdk/1.2.0-r17797">
  <PropertyGroup>
    <OutputType>exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <StartupObject>main.php</StartupObject>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="**/*.php" />
  </ItemGroup>
</Project>

The project file is a standard MSBuild project that specifies

  • Peachpie.NET.Sdk as its "base" project. It defines how to call the compiler, it embeds the compiler binaries themselves, and it defines default compilation options. dotnet will get it from NuGet feed.

  • <OutputType>exe</OutputType> tells the compiler we're building executable console app.

  • <TargetFramework>net8.0</TargetFramework> is our target ramework, in this case .NET 8.0.

  • <Compile Include="**/*.php" /> selects all .php files in project folder as source files. They will be compiled into the resulting .NET assembly.

Once you have the project file (.msbuildproj) and source file(s) (.php), run the build:

dotnet build

and/or run the program:

dotnet run

Project

The complete project can be found on https://github.com/iolevel/peachpie-samples/tree/patreon/system-threading-tasks

You can open it in Visual Studio Code, Visual Studio (at least 2022), or Rider. In those IDE's you can click on Run/Debug and even place breakpoints and debug the code in CLR style.