File: ChangeLog-0.7.2.rst

package info (click to toggle)
python-jpype 1.6.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,348 kB
  • sloc: python: 19,275; cpp: 18,049; java: 8,638; xml: 1,454; makefile: 155; sh: 37
file content (427 lines) | stat: -rw-r--r-- 24,165 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
:orphan:

Buffers and NumPy removal
=========================

NumPy was used primarily for supplying Python buffers on slicing.  Numpy has
always been problematic for JPype.  As an optional extra is may or may not be
built into the distribution, but if it is compiled in it is required.
Therefore, is itn't really on "extra".  Therefore, removing it would make
distribution for binary versions of JPype much easier.

NumPy returns on slicing is but one part of the three uses of Python buffers in
JPype.  Thus to properly remove it we need to rework to remove it we need to
review all three of the paths.  These paths are

 - Conversion of Python buffers to Java arrays on setArrayRange. 
 - Conversion of Java arrays to slices (and then from slices to buffers so that
   they can be transferred to NumPy.)
 - Connecting bytearray to Java direct byte buffers.

We reviewed and revised each of the paths accordingly.

On conversion of Python buffers, the implementation was dated from Python 2.6
era where there was no formal support for buffers.  Thus the buffer
implementation never consulted the buffer type to see what type of object was
being transferred nor how the memory was oriented.  This entire section had to
be replaced and was given the same conversion rules as NumPy.  Any conversion is
possible (including lossy ones like float to bool).  The rules to trigger the
conversion is by slicing just as with NumPy.  By replicating the rules of NumPy
we hide the fact that NumPy is no longer used and increase typesafety.  There
was a number of cases where in the past it would reinterpret cast the memory
that will now function properly.  The old behavior was a useless side effect of
the implementation and was unstable with machine architecture so not likely used
be a user.

The getArrayRange portion has to be split into two pieces.  Under the previous
implementation the type of the return changes from Python list to NumPy array
depending on the compile option.  Thus the test suite tested different
behaviors for each.  In removing NumPy we replace the Java array slice return
with a Java array.  Thus the type is always consistent.  The Java array that is
returned is still backed by the same array as before and has the start, end, and
step set appropriately.  This does create one change in behavior as the slice
now has left assignment (just like NumPy) and before it was a copy.  It is
difficult exercise this but to do so we have to copy a slice to a new variable
then use a second array dereference to assign an element.  Because we converted 
to either list or NumPy, the second dereference could not affect the original.

There is no way to avoid this behavior change without adding a large transaction 
cost.  Which is why NumPy has the exact same behavior as our replacement
implementation.  We could in principle make the slice read only rather than
allowing for double slicing, but that would also be an API change.  

There is one other consequence of producing a view of the original having to do
with passing back to Java.  As Java does not recognize the concept of an array
view we must force it back to a Java type at the JNI level.  We will force copy
the array slice into a Java array at that time.  Thus replicates the same
functionality.  This induces one special edge case as `java.lang.Object` and
`java.lang.Serializable` which are both implemented by arrays must be aware of
the slice copy.

Before we trigger the conversion to NumPy or list by calling the slice without
limits operator `[:]` on the array.  Under the new implementation this is
effectively a no-op.  Thus we haven't broken or forced any changes in the API. 

The third case of direct byte buffers also revealed problems.  Again the type of
the buffer was not being check resulting in weird reinterpret cast like
behaviors.  Additionally, the memory buffer interface was making a very bad
assumption about the referencing of the buffer by assuming that referencing the
object that holds the buffer is the same as referencing the buffer itself.  It
was working only because the buffer was being leaked entirely and was likely
possible to break under situations as the leaked buffer essentially locked the
object forever.

To implement all of this properly unfortunately requires making the Python
wrapper of Java arrays a direct type.  This is possible in JPype 0.8 series
where we converted all classes to CPython roots, thus our only choice is to
backport the JPype speed patch into JPype 0.7.

API change summary
------------------

 - The type of a slice is no longer polymorphic but is always a Java array now.
 - The unbounded slice is now a no op.
 - Buffer types now trigger conversion routines rather than reinterpret casting 
   the memory.
 - Direct buffers now guard the memory layout such that they work only with
   mutable bytearray like types.
 - Assignment of elements though double slicing of an array now affects the
   original rather than just doing nothing effective like before.


JPype Speed Patch
=================

Speed has always been an issue for JPype.  While the interface of JPype is great
the underlying implementation is wanting.  Part of this was choices made early
in the development that favors ease of implementation over speed.  This forced a
very thin interface using Python capsules and then the majority of the code in
a pure Python module.  

The speed issue stems from two main paths.  The first being the method
resolution phase in which we need to consider each method overload argument by
argument which means many queries applied to each object.  The second is the
object construction penality when we return a Java object back to Python.  The
object bottleneck is both on the cost of the wrappers we produce, but
additionaly all of the objects that are constructed to communicate with a Python
function.  Every int, list, string and tuple we use in the process is another
object to construct.  Thus returning one object triggers dozens of object to be
constructed.

We have addressed these problems in five ways

 - Improve resolution of methods by removing the two phase approach to resolving
   a type match cutting the resolution time in half.
 - Caching the types in C so that they don't have to go back to Python to execute a
   method to check the Python cache and construct if necessary.
 - Converting all of the base types to CPython so that they can directly access
   the C++ guts without going back through entry points.
 - Remove all Python new and init method from the Java class tree so that we
   don't leave C during the construction phase.  Thus avoiding having to
   construct Python objects for each argument used during object construction.
 - Adding a Java slot to so that we can directly access Java resources both
   increasing the speed of the queries and saving us an additional object
   during construction.

All of these are being implemented for JPype 0.8 series.  For now we are
backporting the last four to the JPype 0.7 series.  The first is not possible to
backport as it requires larger structural changes.

Lets briefly discuss each of the items


Method resolution 
-----------------

During method resolution the previous implementation had a two phase approach.
First is tried to match the arguments to the Java types in canConvertToJava.  In
this each Java argument had to walk through each possible type conversion and
decide if the conversion is possible.  Once the we have the method overload
resovled we then perform the conversion by calling convertToJava.  That would
then walk through each possible type conversion to find the right one and apply
it.  Thus we did tha work twice.

To prevent this from happening we need to reserve memory for each argument we
are trying to convert.  When we walk through the list and find a conversion
rather than just returning true, we place a pointer to the conversion routine.
That way when we call convertToJava we don't have to walk the list a second time
but instead go straight to the pointer to get the routine to execute. 

This change has two additional consequences. First the primary source of bugs in
the type conversion was a mismatch between canConvertToJava and convertToJava
thus we are removing that problem entirely.  The second and more important to
the user is that the type system is now open.  By installing a routine we can
now add a user rule.  Therefore if we need `java.sql.TimeStamp` to accept a
Python time object we just need to add this to the type conversion table at the
Python level.  This is implemented in the ClassHints patch.  About half of our
customizer code was to try to achieve this on a per method level.  Thus this
elimiates a lot of our current Python customizer code.  The remaining customizer
code is to rename Java methods to Python methods and that will remain.


Caching of Python wrappers
--------------------------

In the previous implementation there was a text keyed dictionary that was
consulted to get type wrappers.  To access it C++ called to a Python function
that decided when to return a cached type and when to create a new one.  This
meant dozens of object constructed just to find the wrapper.  To solve this we
simply move the cache and add it to the JClass directly.  We have to back
reference the Python class so it can't go away while the JVM is running.

There is one section of code that also uses the wrapper dict in the customizers
which needs to decided does a wrapper already exist for the customizer.  We have
replaced these calls with methods on the module.



Conversion of the Base classes
------------------------------

JPype has a number of base classes (object, primitive, exception, string, array)
which hold the methods for the class.  If they are implemented as pure Python
than every access from C++ to these elements needs to create objects accordingly
when then are passed back through the module entry points to get back to C++.

We can avoid this by implementing each of these in CPython first at the module
layer and then extending them in the exposed module so that they have the same
outward appearance as before.  

We made one refinement during the conversion by implementing all of the CPython
classes using the Heep type API which has the distinct advantage that unlike
static types, it can be changed at runtime.  Thus from Python we can add
behavior to the heap types simply with by using `type.__setattr_`.  This was
a bit of a challenge as the documentation on heap types is much more sparse than
for static types.  However, after going through the process I would recommend
that all new CPython modules should use heap types rather than static as API is
much better and the result much more flexable and stable.  The only downside
being the memory footprint increases from 400 bytes to 900 bytes.  There are a
few rough spots in the heap type API in that certain actions like setting the
Buffer have to be added to the type after creation, but otherwise it is a big
improvement.  Now if all of the documentation would just drop the old static API
in favor of heap types it would be great.


Constructor simplifications
---------------------------

In order to benefit from moving all of the base classes to C, we have to make
sure that derived classes do not transfer control back to Python.  Currently
this happens due to the factory nature of our classes.  The entry point for
JObject is shared between the construction of objects from Python and a return
from Java.  Thus we have to either separate the factory behavior by pushing
those types out of the type tree or pushing the factory behavior into the C
layer.

We have chosen to split the factories and use overrides of the type system in
the meta class to apply `isinstance` and `issubtype` behavior.  We can further
restrict the type system if we need to by adding verifications that the
`__new__` and `__init__` methods must point the original base class
implementations if need.  Howver, we have not taken this step as of yet.
The split approach effectively removes these heavy elements from type creation.
The concequence of this is that means all of the rest of logic needs to be in
CPython implementation.  These can be rather cumbersome at times.

It is always a slippery slope when pushing code from Python back to CPython.
Some thing are needed as they are on the critical path while others are
called only occasionally and thus represent no cost to leave in Python.  On the
other hand some things are easy to implement in CPython because the have
direct access rather than having to go through a module entry point.  We have
gone with the approach that all critical path and all code the eliminates the
need for an entry point should be pushed back to C.


Java Slots
-------------

In order to get any reasonable speed with Python, the majority of the code
needs to be in C.  But additionally there needs to be the use of slots which are
hard coded locations to search for a particular piece of information.  This
presents a challenge when wrapping Java as we need a slot for the Java value
structure which must appear on Python object, long, float, exception, and type.
These different types each have their own memory layout and thus we can't just
add the slot at the base as once something is added to the base object it can
no longer be used in multiple inheritance.  Thus we require a different
approach.

Looking through the CPython source, we find they have the same quandary with
respect to `__dict__` and `__weakref__` slots.  Those slots do not appear one
the base object but get added along the way.  The method they use is to add
those slots to the back of the object be increasing the basesize of the object
and then referencing them with two offset slots in the type object.  If the type
is variable length the slot offset are negative thus referencing from the end of
the object, or positive if the object is a fixed layout.

Thus we tried a few formulations to see what would work best.


Broken tree approach
~~~~~~~~~~~~~~~~~~~~

The problem with just directly adding the slots in the tree is that the Java
inheritance tree forces the order of the Python tree we have to apply.  If we
add a slot to `java.lang.Object` we have to keep the slot on
`java.lang.Throwable` but that is not possible because Throwable requires it to
be derived from Python `Exception`. Thus if we are going to add a slot to the
base we would have to break the tree into pieces on the Python side.  This is
possible due to Python inheritance hacking with some effort.

But this approach had significant down sides. When we go to access the slot
we have to first figure out if the slot is present and if not then fall back to
looking in the dictionary.  But one of the most common cases is one in which the
item has no slot at all.  Thus if we have to both look for the slot and then hit
the dictionary, this is worse than just going to the dictionary in the first
place. Thus this defeats the point of a slot in many cases.


Python dict approach
~~~~~~~~~~~~~~~~~~~~~~~~~

We attempted the same trick by increasing the basesize to account for our extra
slot.  This leaves to difficulties.  First, the slot has no offset so we need to
find it each time by implying its location.  Second, the extra objects have to
be "invisible" during the type construction phase, or Python will conclude the
memory layout of the object is in conflict.  We can fool the type system by
subtracting the extra space from the type during the declaration phase and then
adding it back after the base types are created.  

This approach failed because the "invisible" part is checked each and every time
a new type is added to the system.  Thus every dynamic type we add checks the
base types for consistency and at some point the type system will find the
inconsistency and cause a failure.  Therefore, this system can never be robust.

Dict and weakref appear to be very special cases within the Python system and as
there is no general facility to replicate them working within the system does
not appear to be viable.


Memory hacking approach
~~~~~~~~~~~~~~~~~~~~~~~

The last system we attempted to mess with the memory layout of the object during
the creation phase to append our memory after Pythons.  To do this we need to
override the memory allocator to allocate the requested memory plus our extra.
We can then access this appended memory by computing the correct size of the
object and thus our slot is on the end.

We can test if the slot is present by looking to see if both `tp_alloc` and
`tp_finalize` point to our Java slot handlers.  This means we are still
effectively a slot as we can test and access with O(1).

The downside of this approach is there are certain cases in which the type of an
object can be changed during the destruction phase which means that our slot can
point to the wrong space if the basesize is changed out from under us.  To guard
against this we need to close our type system by imposing a ClassMeta which
closes off mixin types that do not inherit from one of the special case base
classes we have defined.  

The API implications should be small.  There was never a functional case where 
extending a Java object within Python actually made sense as the Python portion 
is just lost when passed to Java and unlike Proxies there is no way to retrieve
it.  Further the extending a Java object within Python does not bind the
lifespan of the objects so any code that used this is likely already buggy.  We
will properly support this option with `@JExtends` at a latter point.

With this limitiation in mind, this appears to be the best implementation
 - It adds the slot to all of the required types.
 - The slot is immediately accessable using just two fields (basesize, itemsize)
 - The slot can be tested for easily (check tp_alloc, tp_finalize)
 - It closes the type system by forcing a meta class that guards against
   inappropraite class constuction.

We could in principle add the slot to the "front" of the Python object but that
could cause additional issues as we would require also require overriding the
deallocation slot to disappear our memory from the free.  The Python GC module
has already reserved the memory in the front of the object so the back is 
the next best option.


Speed patch implications
------------------------

Other than improving the speed, the speed patch has a lot of below the hood
changes.  So long as the user was not accessing private members there is no API
change, but everything below that is gone.  All private symbols like
`__javaclass__` and `__javavalue__` as well as all exposed private members
vanish from the interface.  There is no longer a distinction between class
wrappers and `java.lang.Class` instances for purposes of the casting system.
The wrapper is an extension of a Python type and has the class methods and 
fields, and the instance is an extension of a Python object without these.
Both hold Java slots to the same object.  Therefore a lot of complexity of the
private portions is effectively removed from the user view.  Every path now has
the same resolution, check the Java slot first and if not assume it is Python.

Two private methods now appear on the wrapper (though I may be able to hide them
from the user view.)  These are the test entry points `_canConvertToJava` and 
`_convertToJava`.  Thus the speed patch should be transparent all user code that
does not access our private members.  That said some code like the Database
wrappers using JPype have roots in some code that did make access to the private
tables.  I have sent corrections when we upgraded to 0.7 series thus making them
conforming enough not to touch the private members.  But that does mean some
modules may be making such accesses out in the wild.

The good new is after the speed patch pretty much everything that is supposed to
be under the hood is now out of the reach of the user.  Thus the chance of
future API breakage is much lower.


Below the hood changes
======================
 - We have tried to prevent backend changes from reaching the API, though this
   is not always entirely the case.  The majority of the cases not already noted
   appear to be in the range of implementation side effects.  The old
   implementation had bugs that create undefined behaviors like reinterpet
   casting buffers and the like.  It is not possible to both fix the bugs in the
   backend and make preserve buggy behavours on the front end.  We have limited
   our changes to only those for which we can see no desirable use existing.
   Calling a list slice assignment on a float from a numpy array of ints and
   getting a pile of gibberish was as far as we can tell not useful to the user.
 - setResource is dropped in favor of caching the module dictionary once at
   start of the JVM.  We have a lot of resources we will need and the
   setResources method was cumbersome.
 - There is a lot of thrashing for the Python module style between C and C++
   style.  The determining blow was that C++ exception warning showed up when
   the proper linkage was given.  Thus the perferred style flipped from C++
   style to C.  Thus the naming style change accordingly.
 - In addition to the style change there is also an attempt to isolate symbols
   between the different classes.  The older style with a formal header that
   declares all the symbols at the top encouraged access to the functions and
   increased the complexity.  Moving to a C style and making everything static
   forces the classes to be much more independent.
 - With the change to C style there is natural split in CPython class files
   between the structure declaration, static methods that implement Python API
   functions, the declaration of the type, and the exposed C++ style API used by
   the rest of the module. 
 - There is some thrashing on how much of the C++ wrapper style Python API to
   keep.  The rewrapping of the API was mainly so support differences between
   Python 2.7 and 3.x.  So we dropped where we could.  Only the JPPyObject which
   acts as memory handling device over pure Python style (because Python style
   is not exception safe) is strongly needed.
 - There is some spacing thrashing between different editors and the continuing
   debate of why C was written to have the pointer stick to to the variable
   rather than the type.  When writing `Object* foo` it implies that the star
   is stuck to type rather than the variable where C reads it as the opposite.
   Hence there is the endless churn between what is correct `Object *foo` and
   what we would say in which `Object*` is actually a type.  As Python favors
   the former and we currently have the latter that means at some point we
   should just have formatter force consistency.
 - We introduced Py_IsInstanceSingle.  Is is a missing Python API function first
   for fast type check method of a single inherited type using the mro.  Then
   something is singlely inherited we can bypass the whole pile of redirects and
   list searchs and just compare if the mro list matches at the same point
   counted from the end.  As all of our base types are single inherited this
   saves time and dodges the more expensive calls that would trigger due to the
   PyJPClassMeta overrides.
 - We introduced Py_GetAttrDescriptor.  This was previously implemented in
   Python and was very slow.  Python has very good implementations for GetAttr
   but unfortunately it has the behavior that it always dereferences
   descriptors.  Thus it is useless in the case that we need to get the
   descriptor from the type tree.  We have reimplemented the Python type search
   without the descriptor dereferencing behavior.  It is not nearly as complete
   as the Python method as we only need to consult the dictionary and not handle
   every edge case.  That of course means that we are much faster.
 - As with every rewrite there is the need to cleanup.  Anything that wasn't
   reached by the test bench or was identified as being called only from one
   location was removed or reencorperated back in into the function that call
   it.