Open minesworld opened 3 weeks ago
Would be nicer if the generated class would be named NetConsoleClassEngineer
...
There could/should be an option to the decorator how the generated class is named.
At least that might be usefull for those who do not generate the C# code automatically but call a tool which does so.
Besides the missing GIL
usage it could be debated which parts might call the CAPI as "raw" (nint
), IntPtr
or PyObject
.
Maybe a layered approach gives the most options for all...
var asyncPythonFunc = pythonObject.GetAttr("asyncAiFunc");
var result = await PythonExecutor<int>.CallPythonAsync(asyncPythonFunc);
is wrong as PythonExecutor
uses static
functions that way.
To be able to pass the final wrapped python func to the interpreter in a way its compatible with the developers model, an instance of PythonExecutor
should be used.
That way PythonExecutor
could be subclassed so the CallPythonAsync
not only does the "right thing" but even different usage models are possible by using the "correct" instance.
Beste design case would be the need to override just the " FireAndForget
" method
Generators are the best option for calbacks, async functions (coroutines) are fancy wrappers around the generator system in Python.
Utilizing C#.NET's Task system you would execute a callback from a generator whenever it yielded a value in a thread:
from typing import Generator
def example_generator(length: int) -> Generator[str, int, bool]:
for i in range(length):
x = yield f"Item {i}"
if x:
yield f"Received {x}"
return True
var mod = Env.TestGenerators();
var generator = mod.ExampleGenerator(3);
var callback = new Action<string>(
// This is the callback that will be called from the generator
s => Assert.Equal(generator.Current, s)
);
// Wait for the generator to finish
var task = Task.Run(() =>
{
while (generator.MoveNext())
{
// Simulate a callback
callback(generator.Current);
// Optionally send a value back to the generator
generator.Send(10);
}
// Optionally return a value from the generator
Assert.True(generator.Return);
});
I'll submit this test to main to demonstrate it and make sure it's tracked for regressions.
The Pending calls API is really unreliable and only gets called in the interpreter loop (so not when embedded like CSnakes) and in certain scenarios.
pythonnet enables C# functions being called from Python. Very usefull, like redirecting stdout and stderr to whatever the developer likes...
Here a modified example from https://github.com/pythonnet/pythonnet/discussions/1794
Callbacks
Is something like that possible in CSnakes? Too bad I have no clue how to call into dotnet from Python and havn't analyzed yet how pythonnet does it. But as pythonnet does "crazy" stuff, like accessing CPython object internals it might not be the preferred way anyway.
With CSnakes magic something like this should be possible:
CSnakes SourceGenerator could generate the sources for a class like:
which could be use like:
This might be not thread safe, having "side effects" (here: changing a class definition while other python code might use it at the same time). But this should be no argument against it as developer how will use it that way should know what they are doing (documentation...) .
Those developers who want to have no thread safety issues etc. could use
PythonEngineerCallable<T>.CreateFor()
to create a callback which is then used as shown in the pythonnet code...As it is possible to give Python decorators parameters, the generated C# source could also:
InjectWrite(Action<PyObject, string, PyFunc> f);
where the last PyFunc argument is the original func . Would enable to create "hybrid" Python/C# classes where C# can inherit the method and call the "super" function... for those who want that. Maybe automatically generated this way if the decorator is given an "override" (or something else C# like) argument?To be able to prevent the dotnet runtime from catching outside variables etc. :
class MyNetConsoleEngineer : NetConsoleEngineer { string SomeValue { get; set; } }
using var engineer = new MyNetConsoleEngineer(pyCode, "NetConsole") { SomeValue = "?" };
engineer.InjectWrite( (e, self, message) => { var myNetConsoleEngineer = e as MyNetConsoleEngineer; Console.Write(message + myNetConsoleEngineer.SomeValue); });
class NetConsoleEngineer { NetConsoleEngineer(PyObject c, string attribute, object anything); void InjectWrite(Action<object, PyObject, string> f); }
using var engineer = new NetConsoleEngineer (pyCode, "NetConsole", "?");
engineer.InjectWrite( (value, self, message) => { Console.Write(message + value); });
class NetConsoleEngineer { NetConsoleEngineer(PyObject c, string attribute, object anything); void InjectWrite(Action<object, nint, nint>); }
List messages = new();
using var engineer = new NetConsoleEngineer (pyCode, "NetConsole", messages);
engineer.InjectWrite( (messages, self, message) => { CAPI.PyIncRef(message); ((List)messages).Append(message);
});
class PythonEngineer { static PyFunc GeneratePyFuncWrappingAsyncPyFunc(PyFunc F, PyFunc pyResultCallback , PyFunc pyIsCancelledCallback) { // return a generated python function which: // "takes" the C# callbacks to an awaitable queue and cancellation token, either by arguments // or being called using a given locals dict etc. ...
} }
class PythonExecutor
{
static async Task CallPythonAsync(PyFunc f, CancellationToken token=CancellationToken.None)
{
// should be a predefined as much as possible, like do not parse the function definition each time...
var pyIsCancelledCallback = PythonEngineerCallable<Func>.CreateFor(
"def f() -> bool:",
() => { return token.IsCancellationRequested(); // that would use automatic marshalling for the result } );
// // give the python interpreter the syncPyFunc to execute in a "fire and forget" manner //
// at this point we don;t need to hold any references to the python callbacks anymore // as they will be kept alive by python itself // // is that ensured by catching all exceptions? or will this leak if the python interpreter is brought down "somehow"? // in such case - are non leaking async calls possible anyway?
} }
import asyncio import random
def async asyncAiFunc() -> int: await asyncio.sleep(20)
how to access and call pyIsCancelledCallback() for code running in a loop etc. ??
for a "hard" termination of a python thread (if executed in one, see later) there might
be a CPython API call which injects signals or so
but something like that would be implemented on the C# side on a different level...
return random.choice([ 42, 23 ])
var asyncPythonFunc = pythonObject.GetAttr("asyncAiFunc"); var result = await PythonExecutor.CallPythonAsync(asyncPythonFunc);
// would be "usefull" if arguments to the Python func could be passed too...
var result = await pythonObject.GetAttr("asyncAiFunc").CalledAsync(); // where does the etc. go ?