Skip to content

Commit

Permalink
wraps via a , to facilitate code generation on top of a C API allowi…
Browse files Browse the repository at this point in the history
…ng to be passed in.
  • Loading branch information
jmp75 committed Jan 25, 2023
1 parent d224e28 commit 56c0aa0
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 24 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

<!--next-version-placeholder-->

## v1.2.0 (2023-01-25)

`wrap_as_pointer_handle` wraps `None` via a `GenericWrapper`, to facilitate code generation on top of a C API allowing `nullptr` to be passed in.

## v1.1.1 (2022-08-19)

Minor changes that may not have been required to build the [conda package](https://github.com/conda-forge/refcount-feedstock/pull/2)
Expand Down
49 changes: 49 additions & 0 deletions docs/tech_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,55 @@

Note to self: as of Jan 2019 also using github_jm\didactique\doc\know_how.md to log the exploratory and release processes around `refcount`

## Poetry

2022-12

Having to update dependent package versions [GitPython vulnerable to Remote Code Execution due to improper user input validation](https://github.com/csiro-hydroinformatics/pyrefcount/security/dependabot/4). It is probably not a burning issue given this should be only a dev-time dependency and certainly not runtime (otherwise poetry is crap)

```sh
cd ~/src/pyrefcount
poetry --version # in a conda env for poetry...
```

`ModuleNotFoundError: No module named 'importlib_metadata'`. Right...

```sh
cd ~/bin
curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj micromamba
micromamba shell init -s bash -p ~/micromamba
```

```sh
source ~/.bashrc
alias mm=micromamba
mm create -n poetry python=3.9
mm activate poetry
mm list
```

`pip install poetry`

```
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
plantuml-markdown 3.6.3 requires Markdown, which is not installed.
```

Huh??

`pip install Markdown` seems to fix it. Odd.

poetry --version returns 1.3.1 which seems to be what is available from conda-forge anyway. So:

```
mm deactivate
mm env remove -n poetry
mm create -n poetry python=3.9 poetry=1.3.1
mm activate poetry
mm list
```

## Release steps

* all UT pass
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "refcount"
version = "1.1.1"
version = "1.2.0"
description = "Python classes for reference counting"
authors = ["J-M <[email protected]>"]
license = "LICENSE.txt"
Expand Down
2 changes: 1 addition & 1 deletion refcount/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
Doing it this way provides for access in setup.py and via __version__
"""
__version__ = "1.1.1"
__version__ = "1.2.0"
62 changes: 42 additions & 20 deletions refcount/interop.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,51 @@
class CffiNativeHandle(NativeHandle):
"""Reference counting wrapper class for CFFI pointers
This class is originally inspired from a class with a similar purpose in C#. See https://github.com/rdotnet/dynamic-interop-dll
Say you have a C API as follows:
* `void* create_some_object();`
* `dispose_of_some_object(void* obj);`
and accessing it using Python and [CFFI](https://cffi.readthedocs.io).
Users would use the `calllib` function:
```python
from cffi import FFI
ffi = FFI()
# cdef() expects a single string declaring the C types, functions and
# globals needed to use the shared object. It must be in valid C syntax.
ffi.cdef('''
void* create_some_object();
dispose_of_some_object(void* obj);
''')
mydll_so = ffi.dlopen('/path/to/mydll.so')
cffi_void_ptr = mydll_so.create_some_object()
```
at some point when done you need to dispose of it to clear native memory:
```python
mydll_so.dispose_of_some_object(cffi_void_ptr)
```
In practice in real systems one quickly ends up with cases
where it is unclear when to dispose of the object.
If you call the `dispose_of_some_object` function more
than once, or too soon, you quickly crash the program, or possibly worse outcomes with numeric non-sense.
`CffiNativeHandle` is designed to alleviate this headache by
using native reference counting of `handle` classes to reliably dispose of objects.
Attributes:
_handle (object): The handle (e.g. cffi pointer) to the native resource.
_type_id (Optional[str]): An optional identifier for the type of underlying resource. This can be used to usefully maintain type information about the pointer/handle across an otherwise opaque C API. See package documentation.
_finalizing (bool): a flag telling whether this object is in its deletion phase. This has a use in some advanced cases with reverse callback, possibly not relevant in Python.
"""

# Say you have a C API as follows:
# * `void* create_some_object();`
# * `dispose_of_some_object(void* obj);`
# and accessing it using Python and CFFI. Users would use the `calllib` function:
# aLibPointer = callib('mylib', 'create_some_object');
# but at some point when done need to dispose of it:
# callib('mylib', 'dispose_of_some_object', aLibPointer);
# In practice in real systems one quickly ends up with cases
# where it is unclear when to dispose of the object.
# If you call the `dispose_of_some_object` function more
# than once of too soon, you could easily crash the program.
# CffiNativeHandle is designed to alleviate this headache by
# using Matlab native reference counting of `handle` classes to reliably dispose of objects.

# This class is originally inspired from a class with a similar purpose in C#. See https://github.com/rdotnet/dynamic-interop-dll

def __init__(self, handle: "CffiData", type_id: str = None, prior_ref_count: int = 0):

def __init__(self, handle: "CffiData", type_id: Optional[str] = None, prior_ref_count: int = 0):
"""Initialize a reference counter for a resource handle, with an initial reference count.
Args:
Expand Down Expand Up @@ -195,7 +217,7 @@ def __init__(
self,
handle: "CffiData",
release_native: Optional[Callable[["CffiData"], None]],
type_id: str = None,
type_id: Optional[str] = None,
prior_ref_count: int = 0,
):
"""New reference counter for a CFFI resource handle.
Expand Down Expand Up @@ -381,7 +403,7 @@ def ptr(self) -> Any:

def wrap_as_pointer_handle(
obj_wrapper: Any, stringent: bool = False
) -> Union[CffiNativeHandle, OwningCffiNativeHandle, GenericWrapper, None]:
) -> Union[CffiNativeHandle, OwningCffiNativeHandle, GenericWrapper]:
"""Wrap an object, if need be, so that its C API pointer appears accessible via a 'ptr' property
Args:
Expand All @@ -397,7 +419,7 @@ def wrap_as_pointer_handle(
# 2016-01-28 allowing null pointers, to unlock behavior of EstimateERRISParameters.
# Reassess approach, even if other C API function will still catch the issue of null ptrs.
if obj_wrapper is None:
return None
return GenericWrapper(None)
# return GenericWrapper(FFI.NULL) # Ended with kernel crashes and API call return, but unclear why
elif isinstance(obj_wrapper, CffiNativeHandle):
return obj_wrapper
Expand Down
9 changes: 7 additions & 2 deletions tests/test_native_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,13 @@ def test_wrapper_helper_functions():
def test_wrap_as_pointer_handle():
pointer = ut_dll.create_dog()
dog = wrap_cffi_native_handle(pointer, "dog", ut_dll.release)
assert wrap_as_pointer_handle(None, False) is None
assert wrap_as_pointer_handle(None, True) is None

# Allow passing None via a wrapper, to facilitate uniform code generation with c-api-wrapper-generation
assert isinstance(wrap_as_pointer_handle(None, False), GenericWrapper)
assert isinstance(wrap_as_pointer_handle(None, True), GenericWrapper)
assert wrap_as_pointer_handle(None, True).ptr is None
assert wrap_as_pointer_handle(None, False).ptr is None

x = ut_ffi.new("char[10]", init="foobarbaz0".encode('utf-8'))
assert isinstance(wrap_as_pointer_handle(x, False), OwningCffiNativeHandle)
assert isinstance(wrap_as_pointer_handle(x, True), OwningCffiNativeHandle)
Expand Down

0 comments on commit 56c0aa0

Please sign in to comment.