C function with pointer of points wrapping - python

I have a method define in C/C++ DLL that takes 2 args
void SetLines(char** args,int argCount);
I need to call it from Python, what is the proper way to do so.
from ctypes import *
path="test.dll"
lib = cdll.LoadLibrary(path)
Lines=["line 2","line 2"]
lib.SetLines(Lines,len(lines))
print(code)
Excuting the Python code gives the following error:
Traceback (most recent call last):
File "<test.py>", line 6, in <module>
ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1

After some code digging I figure it out:
any C/C++ parameter that accepts a pointer to a list of values should be wrapped in python with
MyType=ctypes.ARRAY(/*any ctype*/,len)
MyList=MyType()
and filled with
MyList[index]=/*that ctype*/
in mycase the solution was:
from ctypes import *
path="test.dll"
lib = cdll.LoadLibrary(path)
Lines=["line 1","line 2"]
string_pointer= ARRAY(c_char_p,len(Lines))
c_Lines=string_pointer()
for i in range(len(Lines)):
c_Lines[i]=c_char_p(Lines[i].encode("utf-8"))
lib.SetLines(c_Lines,len(lines))

Related

Pass object to DLL function imported with ctypes

I need to use an existing library in my python application.
This is a library to read a particular data file.
If you are curious you can download it from here https://www.hbm.com/en/2082/somat-download-archive/ (somat libsie).
The first thing I need to do it open the file so my python scripts starts like:
import ctypes
hllDll = ctypes.WinDLL(r"libsie.dll")
context = hllDll.sie_context_new()
file = hllDll.sie_file_open(context, "test.sie".encode())
but I get the following error:
Traceback (most recent call last):
File "C:\Program Files\JetBrains\PyCharm 2019.3.5\plugins\python\helpers\pydev\_pydevd_bundle\pydevd_exec2.py", line 3, in Exec
exec(exp, global_vars, local_vars)
File "<input>", line 1, in <module>
OSError: exception: access violation reading 0x000000009F6C06B8
I verified that the .sie file is accessible.
I think the problem lies in the "context" object that gets passed as first argument. I think the type is the issue.
Here is part of the header file where context is defined:
typedef void sie_Context;
...
SIE_DECLARE(sie_Context *) sie_context_new(void);
/* > Returns a new library context. */
Am I calling these functions correctly?
Is there a problem with passing the context object?
Thanks in advance
On a 64-bit system, return values default to c_int (32-bit). At a minimum, set the .restype to at least a c_void_p to indicate a 64-bit pointer is returned.
Ideally, set .argtypes and .restype for each function called.
import ctypes
hllDll = ctypes.WinDLL(r"libsie.dll")
hllDll.sie_context_new.argtypes = () # optional but recommended
hllDll.sie_context_new.restype = ctypes.c_void_p # add this
hllDll.sie_context_new.argtypes = ctypes.c_void_p, ctypes.c_char_p # guess, need prototype
# hllDll.sie_context_new.restype = ???
context = hllDll.sie_context_new()
file = hllDll.sie_file_open(context, b"test.sie")

SWIG Converting Python list to a char ** example fails

I am following the SWIG tutorial and I'm currently on the section: "32.9.1 Converting Python list to a char **". The example in question returns a malloc error on my machine:
import example
example.print_args(["a","bc","dc"])
python(57911,0x10bd32e00) malloc: *** error for object 0x7f7ee0406b90: pointer being freed was not allocated
python(57911,0x10bd32e00) malloc: *** set a breakpoint in malloc_error_break to debug
1 57911 abort python
1 57911 abort python
The error is unexpected as this is exactly the code that the tutorial offers. Any help welcome! Thanks in advance
Specs:
MacOS Big Sur
Python 3.8
C++17
Here are my setup.py (the whole archive for reproducibility):
#!/usr/bin/env python
"""
setup.py file for SWIG example
"""
from distutils.core import setup, Extension
import os
import sys
import glob
# gather up all the source files
srcFiles = ['example.i']
includeDirs = []
srcDir = os.path.abspath('src')
for root, dirnames, filenames in os.walk(srcDir):
for dirname in dirnames:
absPath = os.path.join(root, dirname)
globStr = "%s/*.c*" % absPath
files = glob.glob(globStr)
includeDirs.append(absPath)
srcFiles += files
extra_args = ['-stdlib=libc++', '-mmacosx-version-min=10.7', '-std=c++17', '-fno-rtti']
os.environ["CC"] = 'clang++'
#
example_module = Extension('_example',
srcFiles, # + ['example.cpp'], # ['example_wrap.cxx', 'example.cpp'],
include_dirs=includeDirs,
swig_opts=['-c++'],
extra_compile_args=extra_args,
)
setup(name='example',
version='0.1',
author="SWIG Docs",
description="""Simple swig example from docs""",
ext_modules=[example_module],
py_modules=["example"],
)
The example code would work with Python 2, but has a bug as well as a syntax change for Python 3. char** must be passed byte strings, which are the default in Python 2 when using "string" syntax, but need a leading b, e.g. b"string" in Python 3.
This works:
import example
example.print_args([b"a",b"bc",b"dc"])
The crash is due to a bug calling free twice if an incorrect parameter type is found. Make the following change to the example:
if (PyString_Check(o)) {
$1[i] = PyString_AsString(PyList_GetItem($input, i));
} else {
//free($1); // REMOVE THIS FREE
PyErr_SetString(PyExc_TypeError, "list must contain strings");
SWIG_fail;
SWIG_fail; ends up called the freearg typemap, which calls free a second time. With this change, you should see the following if passing incorrect arguments, such as a non-list or Unicode strings instead of byte strings:
>>> import argv
>>> argv.print_args(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\argv.py", line 66, in print_args
return _argv.print_args(argv)
TypeError: not a list
>>> argv.print_args(['abc','def'])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\argv.py", line 66, in print_args
return _argv.print_args(argv)
TypeError: list must contain strings
>>> argv.print_args([b'abc',b'def'])
argv[0] = abc
argv[1] = def
2
Changing the error message to "list must contain byte strings" would help as well 😊

Python function that takes the name of another Python function as an argument

I would like to define a general function that calls another python function. I wrote the general function that takes the path to another python function and the name of another python function as arguments.
import sys
def general_function(path, func):
sys.path.insert(0, path)
import func
However, when I ran the general function (saved in /MY/PATH/general_function.py), I received the following error:
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/MY/PATH/general_function.py", line 4
import func
^
ModuleNotFoundError: No module named 'func'
How can I fix this? I tried import * but it did not work either. I really appreciate your help!
Try this?
import os
def general_function(path, func):
os.chdir(path)
import func

Call a python method from C/C++, and fail in access violation exception

We wanna call a c/c++ extension in python, and in the extension we need to callback one function in the python script.
Here is the python code
# -*- coding: UTF-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import ctypes
class MyClass:
def func(self):
print "func"
open("out.txt","w").write("func")
print "func2"
return
mDll = ctypes.CDLL("xx.dll", ctypes.RTLD_GLOBAL)
c = MyClass()
# c.func() is what we wanna run below
mDll.callFunc.argtypes = [ctypes.py_object]
mDll.callFunc(c)
Below is the c source for xx.dll, built by VS 2008:
__declspec(dllexport) int callFunc(PyObject* self){
printf("callFunc\n");
//PyObject* ret = PyObject_CallMethod(self, "func", NULL);
PyObject* ret2 = PyObject_CallFunctionObjArgs(PyObject_GetAttrString(self, (char*)"func") , self, NULL);
printf("callFunc2\n");
return 0;
}
We have tried two methods (PyObject_CallFunctionObjArgs / PyObject_CallMethod),and neither of the two methods failed in the calling. The results shows:
Traceback (most recent call last):
File "test\tCall.py", line 19, in <module>
mDll.callFunc(c)
WindowsError: exception: access violation writing 0x0000000C
How could we call the MyClass.func ?
ctypes releases the GIL before calling anything loaded with CDLL. You need to explicitly hold it, or use ctypes.PyDLL, or actually, it's probably best to just write a normal extension module instead of loading DLLs manually.
Also, manage your references, and don't pass self to the func method; that happens implicitly.
(I don't know if those are all of the problems, but those are the ones I noticed.)

Why does monkeypatching os.path require a path argument?

pytest has this example in the monkeypatching docs:
import os.path
def getssh(): # pseudo application code
return os.path.join(os.path.expanduser("~admin"), '.ssh')
def test_mytest(monkeypatch):
def mockreturn(path):
return '/abc'
monkeypatch.setattr(os.path, 'expanduser', mockreturn)
x = getssh()
assert x == '/abc/.ssh'
When I remove the path argument from the mockreturn function, I get the error
def getssh(): # pseudo application code
> return os.path.join(os.path.expanduser("~admin"), '.ssh')
E TypeError: mockreturn() takes 0 positional arguments but 1 was given
I don't understand what is providing that positional argument?
Also, when I reimplement the same thing for pathlib.Path.home() I cannot have this argument path there, otherwise it won't work. Unfortunately, the documentation does not say anything about that ominous path argument.
Any illumination for what magic is happening here would be very helpful!
You're trying to replace os.path.expanduser that takes one argument with a mock that doesn't take arguments at all which results to an error when called.
Under the hood monkeypatch.setattr uses builtin setattr so the original version is basically doing following which works since both expanduser and mock take single argument:
>>> import os.path
>>> def mock(path):
... return '/abc'
...
>>> setattr(os.path, 'expanduser', mock)
>>> os.path.expanduser('~admin')
'/abc'
Now if you try to replace expanduser with a method that doesn't take arguments and keep calling it the same way you'll get an error:
>>> setattr(os.path, 'expanduser', mock)
>>> os.path.expanduser('~admin')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: mock() takes 0 positional arguments but 1 was given
Note that you'll get exactly the same error if you try to call mock directly:
>>> mock('~admin')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: mock() takes 0 positional arguments but 1 was given

Categories