Manual Python Integration
The Source Generator library is a useful tool for creating the boilerplate code to invoke a Python function from a PythonEnvironment
instance and convert the types based on the type annotations in the Python function.
However, it is still possible to call Python code without the Source Generator, giving you full control over the integration. This approach requires writing the boilerplate code yourself but provides maximum flexibility.
When to Use Manual Integration
Consider manual integration when:
- You need fine-grained control over Python object handling
- Working with dynamic Python code that can't be statically analyzed
- Implementing custom type conversions not supported by the source generator
- Debugging complex interop scenarios
- Building custom abstractions over the CSnakes runtime
Importantly:
- Dynamic conversion will not be supported in Native AOT
- In most scenarios, manual integration will be slower
Basic Manual Integration Example
Here's an example of how you can call a Python function without the Source Generator. Consider this Python function in a module called test_basic
:
The C# code to call this function manually needs to:
- Convert the .NET types to
PyObject
instances and back - Use the
GIL.Acquire()
method to acquire the Global Interpreter Lock for all conversions and calls to Python - Use the
Import.ImportModule
method to import the module and store a reference once so that it can be used multiple times - Dispose the module when it is no longer needed
using CSnakes.Runtime;
using CSnakes.Runtime.Python;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
namespace CSnakes.Runtime;
public sealed class ExampleDirectIntegration : IDisposable
{
private readonly PyObject module;
private readonly ILogger<IPythonEnvironment> logger;
internal ExampleDirectIntegration(IPythonEnvironment env)
{
this.logger = env.Logger;
using (GIL.Acquire())
{
logger.LogInformation("Importing module {ModuleName}", "test_basic");
module = Import.ImportModule("test_basic");
}
}
public void Dispose()
{
logger.LogInformation("Disposing module");
module.Dispose();
}
public double TestIntFloat(long a, double b)
{
using (GIL.Acquire())
{
logger.LogInformation("Invoking Python function: {FunctionName}", "test_int_float");
using var __underlyingPythonFunc = this.module.GetAttr("test_int_float");
using PyObject a_pyObject = PyObject.From(a);
using PyObject b_pyObject = PyObject.From(b);
using var __result_pyObject = __underlyingPythonFunc.Call(a_pyObject, b_pyObject);
return __result_pyObject.As<double>();
}
}
}
Disabling the Source Generator
If you want to disable the Source Generator completely, you have two options:
Option 1: Remove AdditionalFiles
The Source Generator will only activate on files that are marked as AdditionalFiles
in the project file. Remove the AdditionalFiles
entry:
<!-- Remove or comment out this section -->
<!--
<ItemGroup>
<AdditionalFiles Include="*.py" />
</ItemGroup>
-->
Best Practices for Manual Integration
1. Proper Resource Disposal
// ✅ Good - Proper disposal
using (GIL.Acquire())
{
using var arg1 = PyObject.From(value1);
using var arg2 = PyObject.From(value2);
using var result = function.Call(arg1, arg2);
return result.As<string>();
}
// ❌ Bad - Memory leaks
var arg1 = PyObject.From(value1);
var arg2 = PyObject.From(value2);
var result = function.Call(arg1, arg2);
// Objects not disposed - will cause memory leaks!
2. Error Handling
try
{
using (GIL.Acquire())
{
// Python calls here
}
}
catch (PythonInvocationException ex)
{
// Handle Python-specific exceptions
logger.LogError(ex, "Python error: {PythonType}", ex.PythonExceptionType);
}
catch (Exception ex)
{
// Handle other exceptions
logger.LogError(ex, "Unexpected error in Python integration");
}
4. Caching
// Cache modules, attributes and other classes instead of calling `GetAttr` each time.
private readonly PyObject module;
public MyClass(IPythonEnvironment env)
{
using (GIL.Acquire())
{
module = Import.ImportModule("my_module");
}
}