blob: a95ced29e572eccc627b41b96457d714e8d90ddd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stdio.h>
#include "geomserde.h"
#include "geos_c_dyn.h"
#include "pygeos/c_api.h"
PyDoc_STRVAR(module_doc, "Geometry serialization/deserialization module.");
#define ERR_MSG_BUF_SIZE 1024
#ifdef __GNUC__
#define thread_local __thread
#elif __STDC_VERSION__ >= 201112L
#define thread_local _Thread_local
#elif defined(_MSC_VER)
#define thread_local __declspec(thread)
#else
#error Cannot define thread_local
#endif
static PyObject *load_libgeos_c(PyObject *self, PyObject *args) {
PyObject *obj;
char err_msg[ERR_MSG_BUF_SIZE];
if (!PyArg_ParseTuple(args, "O", &obj)) {
return NULL;
}
if (PyLong_Check(obj)) {
unsigned long long handle = PyLong_AsUnsignedLongLong(obj);
if (load_geos_c_from_handle((void *)handle, err_msg, sizeof(err_msg)) !=
0) {
PyErr_Format(PyExc_RuntimeError, "Failed to find libgeos_c functions: %s",
err_msg);
return NULL;
}
} else if (PyUnicode_Check(obj)) {
const char *libname = PyUnicode_AsUTF8(obj);
if (load_geos_c_library(libname, err_msg, sizeof(err_msg)) != 0) {
PyErr_Format(PyExc_RuntimeError, "Failed to find libgeos_c functions: %s",
err_msg);
return NULL;
}
} else {
PyErr_SetString(PyExc_TypeError,
"load_libgeos_c expects a string or long argument");
return NULL;
}
Py_INCREF(Py_None);
return Py_None;
}
static thread_local GEOSContextHandle_t handle;
static thread_local char *geos_err_msg;
static void geos_msg_handler(const char *fmt, ...) {
if (geos_err_msg == NULL) return;
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
vsnprintf(geos_err_msg, ERR_MSG_BUF_SIZE, fmt, ap);
va_end(ap);
}
static GEOSContextHandle_t get_geos_context_handle() {
if (handle == NULL) {
if (!is_geos_c_loaded()) {
PyErr_SetString(
PyExc_RuntimeError,
"libgeos_c was not loaded, please call load_libgeos_c first");
return NULL;
}
handle = dyn_GEOS_init_r();
if (handle == NULL) {
goto oom_failure;
}
geos_err_msg = malloc(ERR_MSG_BUF_SIZE);
if (geos_err_msg == NULL) {
goto oom_failure;
}
dyn_GEOSContext_setErrorHandler_r(handle, geos_msg_handler);
}
geos_err_msg[0] = '\0';
return handle;
oom_failure:
PyErr_NoMemory();
if (handle != NULL) {
dyn_GEOS_finish_r(handle);
handle = NULL;
}
if (geos_err_msg != NULL) {
free(geos_err_msg);
geos_err_msg = NULL;
}
return NULL;
}
static void handle_geomserde_error(SedonaErrorCode err) {
const char *errmsg = sedona_get_error_message(err);
if (err == SEDONA_ALLOC_ERROR) {
PyErr_NoMemory();
} else if (err == SEDONA_INTERNAL_ERROR) {
PyErr_Format(PyExc_RuntimeError, "%s", errmsg);
} else if (err == SEDONA_GEOS_ERROR) {
const char *errmsg = sedona_get_error_message(err);
PyErr_Format(PyExc_RuntimeError, "%s: %s", errmsg, geos_err_msg);
} else {
const char *errmsg = sedona_get_error_message(err);
PyErr_Format(PyExc_ValueError, "%s", errmsg);
}
}
static PyObject *do_serialize(GEOSGeometry *geos_geom) {
if (geos_geom == NULL) {
Py_INCREF(Py_None);
return Py_None;
}
GEOSContextHandle_t handle = get_geos_context_handle();
if (handle == NULL) {
return NULL;
}
char *buf = NULL;
int buf_size = 0;
SedonaErrorCode err =
sedona_serialize_geom(handle, geos_geom, &buf, &buf_size);
if (err != SEDONA_SUCCESS) {
handle_geomserde_error(err);
return NULL;
}
PyObject *bytearray = PyByteArray_FromStringAndSize(buf, buf_size);
free(buf);
return bytearray;
}
static GEOSGeometry *do_deserialize(PyObject *args,
GEOSContextHandle_t *out_handle,
int *p_bytes_read) {
Py_buffer view;
if (!PyArg_ParseTuple(args, "y*", &view)) {
return NULL;
}
GEOSContextHandle_t handle = get_geos_context_handle();
if (handle == NULL) {
return NULL;
}
/* The Py_buffer filled by PyArg_ParseTuple is guaranteed to be C-contiguous,
* so we can simply proceed with view.buf and view.len */
const char *buf = view.buf;
int buf_size = view.len;
GEOSGeometry *geom = NULL;
SedonaErrorCode err =
sedona_deserialize_geom(handle, buf, buf_size, &geom, p_bytes_read);
PyBuffer_Release(&view);
if (err != SEDONA_SUCCESS) {
handle_geomserde_error(err);
return NULL;
}
*out_handle = handle;
return geom;
}
/* serialize/deserialize functions for Shapely 2.x */
static PyObject *serialize(PyObject *self, PyObject *args) {
PyObject *pygeos_geom = NULL;
if (!PyArg_ParseTuple(args, "O", &pygeos_geom)) {
return NULL; // Argument parsing failed; error already set by
// PyArg_ParseTuple
}
GEOSGeometry *geos_geom = NULL;
char success = PyGEOS_GetGEOSGeometry(pygeos_geom, &geos_geom);
if (success == 0) {
// Retrieve the type of the supplied object
PyObject *type = (PyObject *)Py_TYPE(pygeos_geom);
PyObject *type_name = PyObject_GetAttrString(type, "__name__");
if (type_name == NULL) {
// Fallback error if we can't get the type name
PyErr_SetString(PyExc_TypeError, "Argument is of incorrect type.");
} else {
// Construct the error message with the type name
const char *type_str = PyUnicode_AsUTF8(type_name);
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Argument is of incorrect type: '%s'. Please provide only "
"Geometry objects.",
type_str);
PyErr_SetString(PyExc_TypeError, error_msg);
Py_DECREF(type_name); // Cleanup the reference to type_name
}
return NULL;
}
return do_serialize(geos_geom);
}
static PyObject *deserialize(PyObject *self, PyObject *args) {
GEOSContextHandle_t handle = NULL;
int length = 0;
GEOSGeometry *geom = do_deserialize(args, &handle, &length);
if (geom == NULL) {
return NULL;
}
PyObject *pygeom = PyGEOS_CreateGeometry(geom, handle);
return Py_BuildValue("(Ni)", pygeom, length);
}
/* serialize/deserialize functions for Shapely 1.x */
static PyObject *serialize_1(PyObject *self, PyObject *args) {
GEOSGeometry *geos_geom = NULL;
if (!PyArg_ParseTuple(args, "K", &geos_geom)) {
return NULL;
}
return do_serialize(geos_geom);
}
static PyObject *deserialize_1(PyObject *self, PyObject *args) {
GEOSContextHandle_t handle = NULL;
int length = 0;
GEOSGeometry *geom = do_deserialize(args, &handle, &length);
if (geom == NULL) {
return NULL;
}
/* These functions would be called by Shapely using ctypes when constructing
* a Shapely BaseGeometry object from GEOSGeometry pointer. We call them here
* to get rid of the extra overhead introduced by ctypes. */
int geom_type_id = dyn_GEOSGeomTypeId_r(handle, geom);
char has_z = dyn_GEOSHasZ_r(handle, geom);
return Py_BuildValue("(Kibi)", geom, geom_type_id, has_z, length);
}
/* Module definition for Shapely 2.x */
static PyMethodDef geomserde_methods_shapely_2[] = {
{"load_libgeos_c", load_libgeos_c, METH_VARARGS, "Load libgeos_c."},
{"serialize", serialize, METH_VARARGS,
"Serialize geometry object as bytearray."},
{"deserialize", deserialize, METH_VARARGS,
"Deserialize bytes-like object to geometry object."},
{NULL, NULL, 0, NULL}, /* Sentinel */
};
static struct PyModuleDef geomserde_module_shapely_2 = {
PyModuleDef_HEAD_INIT, "geomserde_speedup", module_doc, 0,
geomserde_methods_shapely_2};
/* Module definition for Shapely 1.x */
static PyMethodDef geomserde_methods_shapely_1[] = {
{"load_libgeos_c", load_libgeos_c, METH_VARARGS, "Load libgeos_c."},
{"serialize_1", serialize_1, METH_VARARGS,
"Serialize geometry object as bytearray."},
{"deserialize_1", deserialize_1, METH_VARARGS,
"Deserialize bytes-like object to geometry object."},
{NULL, NULL, 0, NULL}, /* Sentinel */
};
static struct PyModuleDef geomserde_module_shapely_1 = {
PyModuleDef_HEAD_INIT, "geomserde_speedup", module_doc, 0,
geomserde_methods_shapely_1};
PyMODINIT_FUNC PyInit_geomserde_speedup(void) {
if (import_shapely_c_api() != 0) {
/* As long as the capsule provided by Shapely 2.0 cannot be loaded, we
* assume that we're working with Shapely 1.0 */
PyErr_Clear();
return PyModuleDef_Init(&geomserde_module_shapely_1);
}
return PyModuleDef_Init(&geomserde_module_shapely_2);
}