c++typescriptwebassemblycgal

cgal wasm compiled library, memory access out of bounds


i am currently trying to make a library which runs a wasm compiled cgal programm. the

the main.cpp looks like this:

#ifdef EMSCRIPTEN
// #define CGAL_ALWAYS_ROUND_TO_NEAREST
#define CGAL_NO_ASSERTIONS
#define CGAL_NO_PRECONDITIONS
#define CGAL_NO_POSTCONDITIONS
#define CGAL_NO_WARNINGS
#include <emscripten.h>
#include <emscripten/bind.h>
#endif

#include <iostream>
#include <vector>
#include <CGAL/Uncertain.h>
#include <CGAL/enum.h>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Polygon_with_holes_2.h>
// #include <CGAL/create_weighted_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/create_weighted_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/create_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/Straight_skeleton_2/IO/print.h>
#include <boost/shared_ptr.hpp>
#include <cassert>

// Override make_certain to fix errors. Not sure why Uncertain doesn't work properly in Wasm environment.
template <>
bool CGAL::Uncertain<bool>::make_certain() const
{
    return _i;
}

template <>
CGAL::Sign CGAL::Uncertain<CGAL::Sign>::make_certain() const
{
    return _i;
}

typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point;
typedef CGAL::Polygon_2<K> Polygon_2;
typedef CGAL::Polygon_with_holes_2<K> Polygon_with_holes;
typedef CGAL::Straight_skeleton_2<K> Ss;
typedef boost::shared_ptr<Ss> SsPtr;
typedef CGAL::Exact_predicates_inexact_constructions_kernel CGAL_KERNEL;

// Decodes rings from data and generates a weighted skeleton from them.
// Data contains a list of rings, each ring is represented by a number of points (uint32_t), followed by the points
// themselves (each point is represented by 2 floats: x, y).
// The last value is 0.
// Additionally, a vector of weights is provided, where each weight corresponds to a vertex.
SsPtr generate_weighted_skeleton(void *data, const std::vector<std::vector<float>> &weights)
{
    uint32_t *data_uint32 = (uint32_t *)data;
    uint32_t points = data_uint32[0];

    assert(points != 0);
    assert(points > 2);

    ++data_uint32;

    Polygon_2 outer;
    Polygon_2 hole;
    Polygon_with_holes poly;
    bool outer_set = false;

    while (points != 0)
    {
        Polygon_2 *target = outer_set ? &hole : &outer;

        for (long i = 0; i < points; i++)
        {
            float x = *((float *)data_uint32 + i * 2);
            float y = *((float *)data_uint32 + i * 2 + 1);

            target->push_back(Point(x, y));
        }

        data_uint32 += points * 2;

        points = data_uint32[0];

        ++data_uint32;

        if (!outer_set)
        {
            assert(outer.is_counterclockwise_oriented());
            poly = Polygon_with_holes(outer);
            outer_set = true;
        }
        else
        {
            assert(hole.is_clockwise_oriented());
            poly.add_hole(hole);
            hole.clear();
        }
    }

    return CGAL::create_interior_weighted_straight_skeleton_2(poly, weights, CGAL_KERNEL());
}

// Serializes a skeleton into a format that can be sent to the JS side.
// The first part of the data describes the vertices:
// The first value (uint32_t) specifies the number of vertices.
// After that, each vertex is represented by 3 floats: x, y, time.
// Then, the second part describes the faces:
// Each face is represented by a uint32_t specifying the number of vertices in the face, followed by the indices
// of its vertices (also uint32_t).
// The last value is 0.
void *serialize_skeleton(SsPtr iss)
{
    if (iss == nullptr)
    {
        return nullptr;
    }

    std::unordered_map<Ss::Vertex_const_handle, int> vertex_map;
    std::vector<std::tuple<float, float, float>> vertices;

    for (auto vertex = iss->vertices_begin(); vertex != iss->vertices_end(); ++vertex)
    {
        CGAL::Point_2 point = vertex->point();

        vertices.emplace_back(point.x(), point.y(), vertex->time());
        vertex_map[vertex] = vertices.size() - 1;
    }

    std::vector<std::vector<uint32_t>> faces;
    int total_vertices = 0;

    for (auto face = iss->faces_begin(); face != iss->faces_end(); ++face)
    {
        std::vector<uint32_t> face_polygon;

        for (auto h = face->halfedge();;)
        {
            auto vertex_index = (uint32_t)vertex_map[h->vertex()];
            face_polygon.push_back(vertex_index);
            ++total_vertices;

            h = h->next();

            if (h == face->halfedge())
            {
                break;
            }
        }

        faces.emplace_back(face_polygon);
    }

    int total_size = 1 + vertices.size() * 3 + faces.size() + total_vertices + 1;
    uint32_t *data = (uint32_t *)malloc(total_size * sizeof(uint32_t));
    float *data_float = (float *)data;
    int i = 0;

    data[i++] = vertices.size();

    for (auto vertex : vertices)
    {
        data_float[i++] = std::get<0>(vertex);
        data_float[i++] = std::get<1>(vertex);
        data_float[i++] = std::get<2>(vertex);
    }

    for (auto face : faces)
    {
        data[i++] = face.size();

        for (auto vertex_index : face)
        {
            data[i++] = vertex_index;
        }
    }

    data[i++] = 0;

    return data;
}

extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void *create_weighted_straight_skeleton(void *data, const std::vector<std::vector<float>> &weights)
    {

    // // Polygon-Daten
    //  uint32_t polygon_data[] = {
            
    //      4,      // Anzahl der Punkte im äußeren Polygon
    //      0, 0,   // Punkt 1
    //      10, 8, // Punkt 3
    //      10, 10, // Punkt 3
    //      0, 10,  // Punkt 2

    //      // 4,       // Anzahl der Punkte im inneren Loch
    //      // 2, 2,    // Punkt 1
    //      // 2, 8,    // Punkt 2
    //      // 8, 8,    // Punkt 3
    //      // 8, 2,    // Punkt 4
    //      0       // Ende der Daten
    //  };

    //  // Gewichte
    //  std::vector<std::vector<float>> testweights = {
    //      {}, // Gewichte für das äußere Polygon
    //      // {0.5, 0.5, 0.5, 0.5}  // Gewichte für das innere Loch
    //  };

        SsPtr skeleton = generate_weighted_skeleton(data, weights);

        return serialize_skeleton(skeleton);
    }
}

the wapper in ts looks like this:


//@ts-ignore
import Module from '../core/build/main.js';

interface WasmModule {
    HEAPU8: Uint8Array;
    HEAPU32: Uint32Array;
    HEAPF32: Float32Array;
    _malloc(size: number): number;
    _free(ptr: number): void;
    _create_weighted_straight_skeleton(ptr: number, weightsPtr: number): number;
}

/**
 * Each skeleton vertex is represented by x, y and time. Time can be used to calculate z coordinate.
 */
export type Vertex = [number, number, number];

/**
 * Each polygon is represented by an array of vertex indices.
 */
export type Polygon = number[];

/**
 * Straight skeleton calculation result.
 */
export interface Skeleton {
    vertices: Vertex[];
    polygons: Polygon[];
}

export class SkeletonBuilder {
    private static module: WasmModule = null;

    /**
     * Initializes the WebAssembly module. Must be called before any other method.
     */
    public static async init(): Promise<void> {
        return Module().then((library: WasmModule) => {
            this.module = library;
        });
    }

    /**
     * Builds a skeleton from a GeoJSON polygon.
     * The polygon must have at least one ring. The first ring is always the outer ring, and the rest are inner rings.
     * Outer rings must be counter-clockwise oriented and inner rings must be clockwise oriented.
     * All rings must be weakly simple.
     * Each ring must have a duplicate of the first vertex at the end.
     * @param polygon The GeoJSON polygon.
     * @param weights The weights for each vertex.
     */
    public static buildFromGeoJSONPolygon(polygon: GeoJSON.Polygon, weights: number[][]): Skeleton {
        this.checkModule();
        return this.buildFromPolygon(polygon.coordinates, weights);
    }

    /**
     * Builds a skeleton from a polygon represented as an array of rings.
     * The polygon must have at least one ring. The first ring is always the outer ring, and the rest are inner rings.
     * Outer rings must be counter-clockwise oriented and inner rings must be clockwise oriented.
     * All rings must be weakly simple.
     * Each ring must have a duplicate of the first vertex at the end.
     * @param coordinates The polygon represented as an array of rings.
     * @param weights The weights for each vertex.
     */
    public static buildFromPolygon(coordinates: number[][][], weights: number[][]): Skeleton {
        this.checkModule();
    
        const inputBuffer = this.serializeInput(coordinates);
        const inputPtr = this.module._malloc(inputBuffer.byteLength);
        this.module.HEAPU8.set(new Uint8Array(inputBuffer), inputPtr);
    
        const weightsBuffer = this.serializeWeights(weights);
        console.log("DEBUG WEIGHTSBUFFER", new Float32Array(weightsBuffer))
        const weightsPtr = this.module._malloc(weightsBuffer.byteLength);
        this.module.HEAPU8.set(new Uint8Array(weightsBuffer), weightsPtr);
    
    
        const ptr = this.module._create_weighted_straight_skeleton(inputPtr, weightsPtr);
        if (ptr === 0) {
            return null;
        }
    
        let offset = ptr / 4;
        const arrayU32 = this.module.HEAPU32;
        const arrayF32 = this.module.HEAPF32;
    
        const vertices: Vertex[] = [];
        const polygons: number[][] = [];
    
        const vertexCount = arrayU32[offset++];

        for (let i = 0; i < vertexCount; i++) {
            const x = arrayF32[offset++];
            const y = arrayF32[offset++];
            const time = arrayF32[offset++];
    
            vertices.push([x, y, time]);
        }
    
        let polygonVertexCount = arrayU32[offset++];
    
        while (polygonVertexCount > 0) {
            const polygon = [];
    
            for (let i = 0; i < polygonVertexCount; i++) {
                polygon.push(arrayU32[offset++]);
            }
    
            polygons.push(polygon);
            polygonVertexCount = arrayU32[offset++];
        }
    
        // this.module._free(ptr);
        // this.module._free(inputPtr);
        // this.module._free(weightsPtr);
    
        return { vertices, polygons };
    }

    private static checkModule(): void {
        if (this.module === null) {
            throw new Error('The WebAssembly module has not been initialized, call SkeletonBuilder.init() first.');
        }
    }

    private static serializeInput(input: number[][][]): ArrayBuffer {
        let size: number = 1;

        for (const ring of input) {
            size += 1 + (ring.length - 1) * 2;
        }

        const uint32Array = new Uint32Array(size);
        const float32Array = new Float32Array(uint32Array.buffer);
        let offset = 0;

        for (const ring of input) {
            uint32Array[offset++] = ring.length - 1;

            for (let i = 0; i < ring.length - 1; i++) {
                float32Array[offset++] = ring[i][0];
                float32Array[offset++] = ring[i][1];
            }
        }

        uint32Array[offset++] = 0;
        return float32Array.buffer;
    }

    private static serializeWeights(weights: number[][]): ArrayBuffer {
        let size: number = 0;

        for (const ringWeights of weights) {
            size += ringWeights.length;
        }

        const float32Array = new Float32Array(size);
        let offset = 0;

        for (const ringWeights of weights) {
            for (const weight of ringWeights) {
                float32Array[offset++] = weight;
            }
        }

        return float32Array.buffer;
    }
}

the issue seems to be the serializeWeights function. somehow i am messing up the types or pointers, or anything i am not seeing.

once i am using weights i get a 'memory access out of bounds' error from wasm. if i do not use the weights which were transfered to the cpp but instead use an empty {} here in the last line of the cpp (generate_weighted_skeleton(data, testWeights); then the code runs and does not return issues.

my guess is that i am setting a wrong buffer here:

        const weightsBuffer = this.serializeWeights(weights);
        console.log("DEBUG WEIGHTSBUFFER", new Float32Array(weightsBuffer))
        const weightsPtr = this.module._malloc(weightsBuffer.byteLength);
        this.module.HEAPU8.set(new Uint8Array(weightsBuffer), weightsPtr);

but i cannot figure out what the issue is here.

since i have no idea how i would log something from the cpp file once it is compiled for wasm, i am pretty stuck to figure out what the problem is.

if anyone have any idea how i can proceed to find the problem i would be verry happy!!


Solution

  • The answer has not to do with wasm, but more the datatype. on the typescript side i had to change the way the buffer was made:

    https://github.com/pcace/weighted-straight-skeleton/blob/d84eddc9a25091433cccf1728ab71c305552a9be/src/wrapper/index.ts#L92-L107

    on the cpp side the it also needed to be adapted to the type:

    https://github.com/pcace/weighted-straight-skeleton/blob/d84eddc9a25091433cccf1728ab71c305552a9be/src/core/main.cpp#L183-L192

    cheers. its working now :)