Skip to content

Commit 433e310

Browse files
committed
Solved issues in c loader and py port, add python port ci, add tests for python to c pointers.
1 parent 2569acb commit 433e310

File tree

14 files changed

+272
-88
lines changed

14 files changed

+272
-88
lines changed

.github/workflows/release-python.yml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Release Python Package
2+
3+
on:
4+
push:
5+
branches: [ master, develop ]
6+
paths:
7+
- 'source/ports/py_port/setup.py'
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
env:
14+
PYTHON_PIPY_USER: ${{ secrets.PYTHON_PIPY_USER }}
15+
PYTHON_PIPY_PASSWORD: ${{ secrets.PYTHON_PIPY_PASSWORD }}
16+
17+
jobs:
18+
release:
19+
name: Release Python Port
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Check out the repo
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
- name: Release the port
27+
run: |
28+
cd source/ports/py_port
29+
bash ./upload.sh

source/loaders/py_loader/source/py_loader_port.c

+7-34
Original file line numberDiff line numberDiff line change
@@ -819,10 +819,8 @@ static const char py_loader_capsule_reference_id[] = "__metacall_capsule_referen
819819

820820
static void py_loader_port_value_reference_destroy(PyObject *capsule)
821821
{
822-
void *ref = PyCapsule_GetPointer(capsule, py_loader_capsule_reference_id);
823-
void *v = PyCapsule_GetContext(capsule);
822+
void *v = PyCapsule_GetPointer(capsule, py_loader_capsule_reference_id);
824823

825-
metacall_value_destroy(ref);
826824
metacall_value_destroy(v);
827825
}
828826

@@ -831,7 +829,7 @@ static PyObject *py_loader_port_value_reference(PyObject *self, PyObject *args)
831829
static const char format[] = "O:metacall_value_reference";
832830
PyObject *obj;
833831
loader_impl impl;
834-
void *v, *ref;
832+
void *v;
835833
PyObject *capsule;
836834

837835
(void)self;
@@ -858,30 +856,15 @@ static PyObject *py_loader_port_value_reference(PyObject *self, PyObject *args)
858856
goto error_none;
859857
}
860858

861-
ref = metacall_value_reference(v);
862-
863-
if (ref == NULL)
864-
{
865-
PyErr_SetString(PyExc_ValueError, "Failed to create the reference from MetaCall value.");
866-
goto error_value;
867-
}
868-
869-
capsule = PyCapsule_New(ref, py_loader_capsule_reference_id, &py_loader_port_value_reference_destroy);
859+
capsule = PyCapsule_New(v, py_loader_capsule_reference_id, &py_loader_port_value_reference_destroy);
870860

871861
if (capsule == NULL)
872862
{
873-
goto error_ref;
874-
}
875-
876-
if (PyCapsule_SetContext(capsule, v) != 0)
877-
{
878-
goto error_ref;
863+
goto error_value;
879864
}
880865

881866
return capsule;
882867

883-
error_ref:
884-
metacall_value_destroy(ref);
885868
error_value:
886869
metacall_value_destroy(v);
887870
error_none:
@@ -893,7 +876,7 @@ static PyObject *py_loader_port_value_dereference(PyObject *self, PyObject *args
893876
static const char format[] = "O:metacall_value_dereference";
894877
PyObject *capsule;
895878
const char *name = NULL;
896-
void *ref, *v;
879+
void *v;
897880
loader_impl impl;
898881
PyObject *result;
899882

@@ -922,21 +905,11 @@ static PyObject *py_loader_port_value_dereference(PyObject *self, PyObject *args
922905
return py_loader_port_none();
923906
}
924907

925-
/* Get the reference */
926-
ref = PyCapsule_GetPointer(capsule, name);
927-
928-
if (ref == NULL)
929-
{
930-
return py_loader_port_none();
931-
}
932-
933908
/* Get the value */
934-
v = metacall_value_dereference(ref);
909+
v = PyCapsule_GetPointer(capsule, name);
935910

936-
/* Validate the result */
937-
if (v != PyCapsule_GetContext(capsule))
911+
if (v == NULL)
938912
{
939-
PyErr_SetString(PyExc_TypeError, "Invalid reference, the PyCapsule context does not match the dereferenced value");
940913
return py_loader_port_none();
941914
}
942915

source/ports/py_port/README.rst

+155-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ transparently execute code from Python to any programming language, for
1313
example, calling JavaScript, NodeJS, Ruby or C# code from Python.
1414

1515
Install
16-
========
16+
=======
1717

1818
Install MetaCall binaries first:
1919

@@ -28,7 +28,10 @@ Then install MetaCall Python package through MetaCall:
2828
metacall pip3 install metacall
2929
3030
Example
31-
========
31+
=======
32+
33+
Calling Ruby from Python
34+
------------------------
3235

3336
``multiply.rb``
3437

@@ -44,7 +47,7 @@ Example
4447
4548
from metacall import metacall_load_from_file, metacall
4649
47-
metacall_load_from_file('rb', [ 'multiply.rb' ]);
50+
metacall_load_from_file('rb', [ 'multiply.rb' ])
4851
4952
metacall('multiply', 3, 4); # 12
5053
@@ -53,3 +56,152 @@ Running the example:
5356
.. code:: console
5457
5558
metacall main.py
59+
60+
Using pointers (calling to a C library)
61+
---------------------------------------
62+
63+
For a simple case, let's imagine that we have a simple C function that
64+
has an 'in' parameter and we want to pass a pointer to a long, from
65+
Python side, and then store some value there for reading it later on.
66+
Let's assume we have a ``loadtest.h`` and ``libloadtest.so`` and a C
67+
function from this library could be this one:
68+
69+
.. code:: c
70+
71+
void modify_int_ptr(long *l)
72+
{
73+
*l = 111;
74+
}
75+
76+
Now if we want to call it from Python side, we should do the following:
77+
78+
.. code:: py
79+
80+
from metacall import metacall_load_from_package, metacall, metacall_value_reference, metacall_value_dereference
81+
82+
# Load the library (we can configure the search paths for the .so and .lib with metacall_execution_path)
83+
# metacall_execution_path('c', '/usr/local/include')
84+
# metacall_execution_path('c', '/usr/local/lib')
85+
metacall_load_from_package('c', 'loadtest')
86+
87+
# Create value pointer (int *)
88+
int_val = 324444
89+
int_val_ref = metacall_value_reference(int_val)
90+
91+
# Pass the pointer to the function
92+
metacall('modify_int_ptr', int_val_ref)
93+
94+
# Get the value from pointer
95+
int_val_deref = metacall_value_dereference(int_val_ref)
96+
print(int_val_deref, '==', 111)
97+
98+
For a more complex case, where we have an in/out parameter, for example
99+
an opaque struct that we want to alloc from C side. First of all, with
100+
the following header ``loadtest.h``:
101+
102+
.. code:: c
103+
104+
#ifndef LIB_LOAD_TEST_H
105+
#define LIB_LOAD_TEST_H 1
106+
107+
#if defined(WIN32) || defined(_WIN32)
108+
#define EXPORT __declspec(dllexport)
109+
#else
110+
#define EXPORT __attribute__((visibility("default")))
111+
#endif
112+
113+
#ifdef __cplusplus
114+
extern "C" {
115+
#endif
116+
117+
#include <cstdint>
118+
119+
typedef struct
120+
{
121+
uint32_t i;
122+
double d;
123+
} pair;
124+
125+
typedef struct
126+
{
127+
uint32_t size;
128+
pair *pairs;
129+
} pair_list;
130+
131+
EXPORT int pair_list_init(pair_list **t);
132+
133+
EXPORT double pair_list_value(pair_list *t, uint32_t id);
134+
135+
EXPORT void pair_list_destroy(pair_list *t);
136+
137+
#ifdef __cplusplus
138+
}
139+
#endif
140+
141+
#endif /* LIB_LOAD_TEST_H */
142+
143+
With the following implementation ``loadtest.cpp``:
144+
145+
.. code:: c
146+
147+
#include "loadtest.h"
148+
149+
int pair_list_init(pair_list **t)
150+
{
151+
static const uint32_t size = 3;
152+
153+
*t = new pair_list();
154+
155+
(*t)->size = size;
156+
(*t)->pairs = new pair[(*t)->size];
157+
158+
for (uint32_t i = 0; i < size; ++i)
159+
{
160+
(*t)->pairs[i].i = i;
161+
(*t)->pairs[i].d = (double)(((double)i) * 1.0);
162+
}
163+
164+
return 0;
165+
}
166+
167+
double pair_list_value(pair_list *t, uint32_t id)
168+
{
169+
return t->pairs[id].d;
170+
}
171+
172+
void pair_list_destroy(pair_list *t)
173+
{
174+
delete[] t->pairs;
175+
delete t;
176+
}
177+
178+
In this case the structs are not opaque, but they can be opaque and it
179+
will work in the same way. Now, we can call those functions in the
180+
following manner:
181+
182+
.. code:: py
183+
184+
from metacall import metacall_load_from_package, metacall, metacall_value_create_ptr, metacall_value_reference, metacall_value_dereference
185+
186+
metacall_load_from_package('c', 'loadtest')
187+
188+
# Create a pointer to void* set to NULL
189+
list_pair = metacall_value_create_ptr(None)
190+
191+
# Create a reference to it (void**)
192+
list_pair_ref = metacall_value_reference(list_pair)
193+
194+
# Call the function
195+
result = metacall('pair_list_init', list_pair_ref)
196+
197+
# Get the result updated (struct allocated)
198+
list_pair = metacall_value_dereference(list_pair_ref)
199+
200+
# Pass it to a function
201+
result = metacall('pair_list_value', list_pair, 2)
202+
203+
# Destroy it
204+
metacall('pair_list_destroy', list_pair)
205+
206+
# Here result will be 2.0 because is the third element in the array of pairs inside the struct
207+
print(result, '==', 2.0)

source/ports/py_port/VERSION

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.6.0

source/ports/py_port/setup.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@
3232
with open(os.path.join(current_path, 'README.rst'), encoding='utf-8') as f:
3333
long_description = f.read()
3434

35+
# Get the version
36+
with open(os.path.join(current_path, 'VERSION')) as f:
37+
version = f.read()
38+
3539
# Define set up options
3640
options = {
3741
'name': 'metacall',
3842

3943
# Versions should comply with PEP440. For a discussion on single-sourcing
4044
# the version across setup.py and the project code, see
4145
# https://packaging.python.org/en/latest/single_source_version.html
42-
'version': '0.5.0',
46+
'version': version,
4347

4448
'description': 'A library for providing inter-language foreign function interface calls',
4549
'long_description': long_description,
@@ -82,6 +86,10 @@
8286
'Programming Language :: Python :: 3.7',
8387
'Programming Language :: Python :: 3.8',
8488
'Programming Language :: Python :: 3.9',
89+
'Programming Language :: Python :: 3.10',
90+
'Programming Language :: Python :: 3.11',
91+
'Programming Language :: Python :: 3.12',
92+
'Programming Language :: Python :: 3.13',
8593
],
8694

8795
# Keywords
@@ -114,23 +122,23 @@
114122
}
115123

116124
# Exclude base packages
117-
exclude_packages = ['contrib', 'docs', 'test', 'CMakeLists.txt']
125+
exclude_packages = ['contrib', 'docs', 'test', 'test.py' 'CMakeLists.txt', '.gitignore', 'upload.sh']
118126

119127
# TODO: Review helper
120128
# # Detect if metacall port is already installed
121129
# port_installed = False
122130

123-
# Append environment variable or default install path when building manually (TODO: Cross-platform paths)
124-
sys.path.append(os.environ.get('PORT_LIBRARY_PATH', os.path.join(os.path.sep, 'usr', 'local', 'lib')));
131+
# # Append environment variable or default install path when building manually (TODO: Cross-platform paths)
132+
# sys.path.append(os.environ.get('PORT_LIBRARY_PATH', os.path.join(os.path.sep, 'usr', 'local', 'lib')));
125133

126-
# Find is MetaCall is installed as a distributable tarball (TODO: Cross-platform paths)
127-
rootdir = os.path.join(os.path.sep, 'gnu', 'store')
128-
regex = re.compile('.*-metacall-.*')
134+
# # Find is MetaCall is installed as a distributable tarball (TODO: Cross-platform paths)
135+
# rootdir = os.path.join(os.path.sep, 'gnu', 'store')
136+
# regex = re.compile('.*-metacall-.*')
129137

130-
for root, dirs, _ in os.walk(rootdir):
131-
for folder in dirs:
132-
if regex.match(folder) and not folder.endswith('R'):
133-
sys.path.append(os.path.join(rootdir, folder, 'lib'))
138+
# for root, dirs, _ in os.walk(rootdir):
139+
# for folder in dirs:
140+
# if regex.match(folder) and not folder.endswith('R'):
141+
# sys.path.append(os.path.join(rootdir, folder, 'lib'))
134142

135143
# TODO: Review helper
136144
# # Find if module is installed
@@ -171,7 +179,7 @@
171179
# instead of the old one, but we keep the ./helper folder in order to provide future support for
172180
# extra commands, although the main idea is to keep the OS dependant install, this can be useful
173181
# for updating or doing Python related things. Meanwhile, it will be avoided.
174-
exclude_packages.append('helper')
182+
exclude_packages.extend(['helper', 'helper.py'])
175183

176184
# TODO: Review helper
177185
# if port_installed == True:
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
load py ${CMAKE_CURRENT_SOURCE_DIR}/run_tests.py
1+
load py ${CMAKE_CURRENT_SOURCE_DIR}/test.py
22
call main()
33
exit

0 commit comments

Comments
 (0)