Skip to content

add to_object to the vtable and fix numpy "linking" #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 16, 2019

Conversation

llllllllll
Copy link
Contributor

@llllllllll llllllllll commented Apr 9, 2019

No description provided.

Copy link

@ssanderson ssanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change looks reasonable assuming we're confident in the current and future behavior of PyArray_Scalar. Is there a quick test we can add for this functionality, especially in the any_ref<scoped_ref> case?

Copy link

@ssanderson ssanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@llllllllll llllllllll force-pushed the numpy-conversion branch 2 times, most recently from c53a9d4 to 8662ab1 Compare April 10, 2019 03:41
@llllllllll llllllllll changed the title Numpy conversion add to_object to the vtable and fix numpy "linking" Apr 11, 2019
Copy link

@ssanderson ssanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good to me. I had some suggestions on the numpy linking description.

@@ -1,6 +1,16 @@
#include "gtest/gtest.h"
#include <Python.h>

#define LIBPY_TEST_MAIN

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment here linking the user to the comment that explains what this is about?

@@ -7,6 +7,34 @@
#include <vector>

#include <Python.h>

// numpy expects code is being used in a way that has a 1:1 correspondence from source

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be helpful for a reader to remind what numpy expects a normal c extension to look like, e.g.:

// numpy expects that users will write C extensions that look like this:

    // my_extension.c
    #include arrayobject.h
    ...
    PyMODINIT_FUNC PyInit_myextension() {
        import_array();
        // Rest of module setup.
    }

// external linkage. Then, because the tests are declaring the same symbol as extern, they
// will all resolve to the same `PyArray_API` instance and we only need to call
// `import_array` once in `main.cc`.
#if defined(LIBPY_COMPILING_FOR_TESTS) && !defined(LIBPY_TEST_MAIN)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this instead be:

#if defined(LIBPY_COMPILING_FOR_TESTS)
#define PY_ARRAY_UNIQUE_SYMBOL PyArray_API_libpy
#if !defined(LIBPY_TEST_MAIN)
#define NO_IMPORT_ARRAY
#endif
#endif

That would remove the need for setting PY_ARRAY_UNIQUE_SYMBOL separately (to the same value) in main.cc, right?

Though, reading this now, I don't actually understand why we need to set PY_ARRAY_UNIQUE_SYMBOL. Why does it matter which symbol gets used for looking up the API functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to set that macro at all to get it to have external linkage. The name doesn't matter, just defining it does.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. So just defining NO_IMPORT_ARRAY by itself doesn't do anything?

// numpy expects code is being used in a way that has a 1:1 correspondence from source
// file to Python extension (as a shared object). Numpy wants to be able to use the Python
// import system to find the numpy shared object(s), therefore numpy uses it's own linking
// system where the `numpy/arrayobject.h` will put a `static void** PyArray_API = nullptr`
Copy link

@ssanderson ssanderson Apr 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a reader who doesn't already understand this topic might have a hard time following the logic of this paragraph. Here, for example, when we write "numpy uses its own linking system where numpy/arrayobject.h will put a static void** PyArray_API = nullptr name into each TU that includes it.", I'm not sure it's clear how the second half of that sentence follows from the first half, because really the first half of the sentence is introducing the topic of the next 2-3 sentences.

It seems like the goal of this paragraph wants to be to explain to the user how numpy expects things to normally work, so that the next paragraph can explain why the usual way doesn't work for us, so that the paragraph after that can explain why we're doing something unusual to fix the problem.

Partly for checking my understanding, and partly as a suggestion for the structuring this explanation, here's my high-level understanding of the problem being solved here:

  1. Numpy wants callers to be able to use its API functions without having to dynamically link against numpy.
  2. To make that work, numpy expects you to define a static variable named PyArray_API that holds the address of an array of function pointers. Most of numpy's "API functions" are actually macros that resolve to calls through one of these pointers. By default, numpy will define PyArray_API for you when you #include numpy/arrayobject.h, and numpy provides an import_array macro that you're expected to "call" at runtime in your extension setup. import_array uses the Python import machinery to import numpy, find the correct address of its array of function pointers and sets the value of PyArray_API to that address.
  3. The fact that numpy defines PyArray_API for you (as opposed to merely declaring it) is unusual. Headers generally only provide declarations of variables, not definitions, because if you include a header in multiple files that get linked together, you end up with multiple definitions, which is bad. Numpy defines PyArray_API for you anyway, because it assumes by default that you're writing a single-file C extension, which means you're only going to #include numpy/arrayobject.h once.
  4. In our case, however, we're linking lots of test files together into a single executable, so having multiple definitions of PyArray_API is bad. Numpy provides a way to disable its default behavior, which is to define the symbol NO_IMPORT_ARRAY. Defining NO_IMPORT_ARRAY causes numpy to declare PyArray_API as extern "C", which tells the compiler/linker to find the definition of PyArray_API in another castle translation unit.
  5. We want NO_IMPORT_ARRAY to be defined for every #include of numpy/arrayobject.h except one. We do that with some include shenanigans to make it so that main.cc is the one place where that happens.

// (`LIBPY_COMPILING_FOR_TESTS`) which will control the `NO_IMPORT_ARRAY` flag. This flag
// tells numpy to declare the `PyArray_API` flag as an `extern "C" void** PyArray_API`,
// meaning we expect to have this symbol defined by another object we are to be linked
// with. In `main.cc` we also set `LIBPY_TEST_MAIN` to disable this feature, but instead

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this "instead"? It seems like we're defining PY_ARRAY_UNIQUE_SYMBOL in both places.

// single program. This has made it very hard to figure out when and how to initialize the
// `PyArray_API` value. Instead, we now set a macro when compiling for the tests
// (`LIBPY_COMPILING_FOR_TESTS`) which will control the `NO_IMPORT_ARRAY` flag. This flag
// tells numpy to declare the `PyArray_API` flag as an `extern "C" void** PyArray_API`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming my understanding from above is correct, I might remove the void ** here and below.

The important difference between the two cases here is that one of them is extern "C" and the other one isn't, but that isn't obvious to the reader when they're on this sentence, so they might focus on the void ** rather than the extern "C".

@@ -165,10 +180,14 @@ class any_vtable {
return m_impl->ne(lhs, rhs);
}

inline bool eq(const void* lhs, const void* rhs) {
inline bool eq(const void* lhs, const void* rhs) const {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this just an oversight? I wonder if we could set up a linter of some sort to suggest obvious missing consts like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this just means we don't have a test for this, idk if a linter is the correct tool. These qualifications aren't "obvious" or not, they are part of the API

return ob;
}
};
struct new_dtype<std::int8_t> : new_dtype_from_typecode<NPY_INT8> {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a nice simplification. Not sure this matters, but could this also be a template using to avoid the need for subclassing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explicit specialization, not just an alias so I don't think a using works here.

@@ -695,7 +601,7 @@ scoped_ref<> move_to_numpy_array(std::vector<T>&& values) {
return nullptr;
}
return move_to_numpy_array<std::vector<T>, 1>(std::move(values),
descr,
std::move(descr),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a functional difference here, or is this just to avoid an unnecessary ref inc/dec?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just moving into the function to transfer the reference, no functional difference.

@llllllllll llllllllll merged commit a8e85cb into master Apr 16, 2019
@llllllllll llllllllll deleted the numpy-conversion branch April 16, 2019 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants