Skip to content
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

Backward Compatibility for OPS Structures #1078

Open
lplewa opened this issue Feb 5, 2025 · 19 comments
Open

Backward Compatibility for OPS Structures #1078

lplewa opened this issue Feb 5, 2025 · 19 comments

Comments

@lplewa
Copy link
Contributor

lplewa commented Feb 5, 2025

Our OPS structures for pools and providers are currently not backward compatible, even though they include a version field. The following are four separate requirements we must fulfill:

  1. User calls umfDisjointPoolOps() and passes the resulting structure to umf*Create().
    (No manual struct creation—just calling the UMF-provided function.)

  2. User creates their own provider/pool structure (manually defining the fields) and passes it to umf*Create().
    (We need to detect the old structure version and handle it properly.)

  3. User obtains our OPS structure, duplicates it (it SHOULD be const), and modifies it.
    (Challenge: if the new UMF version’s OPS structure has a different size, the user doesn’t know how much memory to allocate.)

  4. User compiles a program (written against an older UMF release) without any source changes, with a new UMF version.
    (The program should compile without problems and continue to work.)


Option 1: Make OPS Structure Opaque

  • Concept

    • Remove the public definition of the OPS structure.
    • Provide creation, duplication, and freeing functions (e.g., umfPoolOpsCreate(), umfPoolOpsDup(), umfPoolOpsDestroy()).
    • Provide setter/getter functions for each field (there are currently ~27 fields).
  • Advantages

    • Future changes do not break binary compatibility: fields can be added or removed internally.
    • Removing a field just turns the associated getter/setter into a no-op.
  • Disadvantages

    • Backward Compatibility: This breaks existing code if it directly references the struct fields.
    • User Adoption Effort: High. Users must replace direct struct assignments with multiple setter/getter calls.
    • A large number of new APIs (at least 2× the number of fields, and we have 27 atm).

Option 2: Keep Old Versions Internally

We maintain multiple versions of the OPS structure and the functions that return them. That way, older user code can call older “versions” and still work. There are two sub-approaches:

Option 2a: Symbol Versioning (Linker Script)

  • Concept

    • Use a linker script with versioned symbols. For instance, umfDisjointPoolOps might exist in multiple versions: _umfDisjointPoolOps@UMF_1.0, _umfDisjointPoolOps@UMF_2.0, etc.
    • Older binaries link to the older version automatically, newer binaries link to the newer symbol.
  • Advantages

    • Backward Compatibility: Great for old code—no source changes needed; the correct symbol version is picked up during linking.
    • User calls remain exactly the same if they compile the same code.
  • Disadvantages

    • Library Maintenance Complexity: High. Each new version must keep all old symbols alive.
    • If the user use dlsym changes will break compatibility unless the user uses dlvsym.

Option 2b: Version-Suffixed Functions + Macro

  • Concept

    • Instead of linker-script magic, we explicitly name each version, e.g., umfDisjointPoolOpsV1(), umfDisjointPoolOpsV2(), etc.
    • A macro #define umfDisjointPoolOps(...) umfDisjointPoolOpsV2(...) points to the newest version.
  • Advantages

    • Backward Compatibility: Also easy for old code—no changes if they keep calling umfDisjointPoolOpsV1() (or if the macro for umfDisjointPoolOps points to V1 initially).
    • Simpler than linker scripts; no “symbol version” magic required.
  • Disadvantages

    • Library Maintenance Complexity: Still high—multiple function variants must be preserved.
    • If users want new functionality, they must update the dlsym calls to umfDisjointPoolOpsV2() or whichever is current.

Option 3: Replace version Field with Structure Size

  • Concept

    • Remove the version field and replace it with a size field.
    • New fields are always appended to the end of the structure. Old fields remain, even if deprecated.
    • When duplicating an OPS structure, the user references ops->size to know how many bytes to copy.
    • Important: We must remove sub-structures (e.g., ext, ipc) from the provider struct and place everything in one contiguous ops structure. This prevents layout fragmentation when new fields are inserted.
  • Advantages

    • Maintenance: Relatively easier than Option 2—only one structure version in the library, and we simply fill or ignore new fields based on size.
    • User Adoption: Easy—they only need to switch from setting a version to setting a size, for custom pools/providers
  • Disadvantages

    • Backward Compatibility: A breaking change for current code, because version is removed and replaced by size.
    • We cannot remove or rename existing fields (we can only deprecate them).
    • Potential user error if they do sizeof or assume a fixed struct size instead of using the size field.

Comparison Table

Requirement / Criteria Option 1
(Opaque Structure)
Option 2a
(Symbol Versioning)
Option 2b
(Version-Suffixed + Macro)
Option 3
(Structure Size)
(1) Calling umfDisjointPoolOps() and passing to umfCreate() Works – User do not see any changes in ops structure Works – if user calls function directly of thru dlvsym() if called thru dlsym it breaks Works – old code can call umfDisjointPoolOpsV1() (or macro for V1) Works – user do not need access to fields in this case
(2) User-Defined Custom Structure Must use creation functions (cannot define the struct themselves) Handled by checking structure’s version field for older code Handled similarly—older “V1” creation function and version field checks Handled – library checks ops->size; if it’s smaller, treat missing fields as zero
(3) Duplicating UMF’s OPS Structure Must call a duplication API that returns an opaque handle Works (the user obtains the correct older structure version if they used the old function) Also works—umfDisjointPoolOpsV1() returns the older structure, user duplicates that Works—user must allocate ops->size bytes, then memcpy
(4) Old code (compiled against old UMF) linking with new UMF Works – we will not remove any functions Works – automatically uses new symbol version via linker Works – but user need to update function names in dlsym Works – There is only a change of size field value, and new fields added. No changes in code needed.
Complexity in library maintenance Medium – after rewriting, we only maintain the new opaque code, but we add ~54 setter/getter APIs High – must keep multiple symbol versions plus a linker script High – multiple function variants (V1, V2, etc.), though simpler than symbol versioning alone Low – only one struct version; check size to set new fields to NULL
Ability to add fields High – we can add new getters/setters freely High – add them in the new internal version of the struct High – add them in the new function variant (e.g., umfDisjointPoolOpsV2()) High – append at the end, older code sees a smaller size
Ability to remove/rename fields Easy – older setters/getters become no-ops, or return NOSUPPORTED Not feasible – older versions musk keep old fields; can only deprecate by comment Not feasible – we must keep old fields; can only deprecate by comment Not feasible – must preserve the layout for old code; can only deprecate by comment
Risk of user errors Lowest – direct struct access is hidden behind an API Medium – primarily with dlsym Low – user might call the wrong suffix or mix up macros with symbols Medium – user might do sizeof(struct) instead of ops->size

@lplewa
Copy link
Contributor Author

lplewa commented Feb 5, 2025

I personal vote for option 3.
@bratpiorka implemented option 2a in the #1063

@pbalcer
Copy link
Contributor

pbalcer commented Feb 6, 2025

I need to take a look at #1063, but there isn't really any issue of just using the version variable as a sort of size (like in your option 3).

Basically I think this should work like this:

  1. We add a field to a struct. This requires we bump up the version (e.g., from 0.11.3 to 0.11.4).
  2. We have a function that maps versions to features:
IsFeatureSupported(version, feature) {
  switch(feature) {
     case FEATURE_FOO:
           if (version > 0114) return true;
  }
}
  1. we make sure that we don't use the new field without checking feature support.

Let's not overcomplicate this...

@bratpiorka
Copy link
Contributor

@lplewa in fact in #1063 I implemented a mix of 2a + b - lets call it just "2":

  • I added new function versions with appropriate suffixes (_0_10, _0_11) so they clearly indicate the version where they should be used
  • I also used linker scripts to keep the "defaults" unchanged (slightly different implementation for win and linux but both work)

Advantages:

  • full backward compatibility on linking, dlsym support for "one version" back
  • for new versions - excellent adoption - users need nothing to change. No new fields were added and no "things to remember" were written in the docs

Disadvantages:

  • for dlsym() support - we could set only single "default", so if we got a_0_13, a_0_12, and a_0_11, and the user just calls dlsym("a") then we need to choose which version would be the default, so effectively we could support one version back without any problems (should be enough?)

Now if we compare option 2 with 3:
(1) Calling umfDisjointPoolOps() and passing to umfCreate()

  • works for both options

(2) User-Defined Custom Structure

  • option 2 - just works. Users need to set the version to VERSION_CURRENT
  • option 3 - works. Users need to set the proper size (e.g. using sizeof()) (*)

(3) Duplicating UMF’s OPS Structure

  • option 2 - just works
  • option 3 - works but user has to use ops->size instead of sizeof(). This is not consistent with 2) and user need to "keep this in mind" which could lead to potential errors

(4) Old code (compiled against old UMF) linking with new UMF

  • works for both options

Complexity in library maintenance

  • option 2 - needs to keep old structures and handle differences between versions (simple)
  • option 3 - needs to handle differences globally (hard, possible mix of changes between structs - some fields added, some deprecated)

Ability to add fields

  • similar

Ability to remove/rename fields

  • option 2 - easy - just define new structure
  • option 3 - rename - complex and little odd, remove - by comment (error-prone)

Risk of user errors

  • option 2 - low
  • option 3 - The user needs to carefully read the docs. Easy to write tests that would not work

Additionally: break changes with the current release:

  • option 2 - no
  • option 3 - break. Also, we need to remove ext and ipc sets from ops structure

From what I see option 2 wins with 3 in all fields except the dlsym case where option 2 could easily support only single version back

@pbalcer let's not oversimplify this :) this whole thread is about this single sentence "This requires we bump up the version (e.g., from 0.11.3 to 0.11.4)" - the "version" here could be an int or size of structure, and we are trying to figure out here which option is the best

@pbalcer
Copy link
Contributor

pbalcer commented Feb 6, 2025

the "version" here could be an int or size of structure, and we are trying to figure out here which option is the best

I don't think it matters. We can map both numbers to "can I use this field or not". The only consideration might be how simple it is to do that mapping. Size might be easier, whereas version might require a switch like I suggested. Personally I have no preference, both will work.

I don't understand the need to keep other versions of the structs, returning older versions etc.
We can only add new fields, never remove them. All the UMF functions return those structs by pointer. So even if we return a larger struct than before in a new UMF version, the application, if not recompiled, will not even know that the object's size changed.

@bratpiorka
Copy link
Contributor

@pbalcer ops structures are not opaque - the user sees their definition

and what about:

ops arr[3]; // sizeof(ops) == 32
arr[0] = *umfCUDAMemoryProviderOps() // here sizeof ops == 48
arr[1] = *umfCUDAMemoryProviderOps() // here sizeof ops == 48

I do not understand why we could use the size as a version. Size is size, version is version. Whatever we use, there need to be a way to get the "version" from the ops structure. Consider:

ops { // ver 0.10 from the included file
.a,
.b,
.c
} my_ops;
my_ops = *umfGetOps(); // here we call UMF ver 11 and return ops ver 11 { .a, .b // deprecated .c .d // added }
my_ops.b = abc()

umfProviderCreate(my_ops);

In the example above umfProviderCreate needs to recognize the version of my_ops to properly handle .b.
Also, as the user expect that in some cases his abc() handler would be called, we still need to keep the code around .b

@pbalcer
Copy link
Contributor

pbalcer commented Feb 6, 2025

Copying structs like this will never just work. Not with size or version. It needs to be updated after such a copy. The answer here is documentation.

Size is size, version is version

You can map version to size. Again, I don't care either way.

In the example above umfProviderCreate needs to recognize the version of my_ops to properly handle .b.

Deprecating features is something that will need to be handled case-by-case. I don't understand how we could handle it generically.

@lplewa
Copy link
Contributor Author

lplewa commented Feb 6, 2025

@lplewa in fact in #1063 I implemented a mix of 2a + b - lets call it just "2":

Ok - my bad - i did not see macros

Now if we compare option 2 with 3: (1) Calling umfDisjointPoolOps() and passing to umfCreate()

* works for both options

(3) Duplicating UMF’s OPS Structure

* option 2 - just works

* option 3 - works but user has to use ops->size instead of sizeof(). This is not consistent with 2) and user need to "keep this in mind" which could lead to potential errors

We need good documentation - if used do anything with ops structure must look on the docs in header, so it should be fine.

(4) Old code (compiled against old UMF) linking with new UMF

* works for both options

Only if you do not rename, or remove field from the struct

Complexity in library maintenance

* option 2 - needs to keep old structures and handle differences between versions (simple)

* option 3 - needs to handle differences globally (hard, possible mix of changes between structs - some fields added, some deprecated)

You cannot change struct anyway - you can add only optional fields in both cases.

Ability to remove/rename fields

* option 2 - easy - just define new structure

* option 3 - rename - complex and little odd, remove - by comment (error-prone)

Nope - you cannot remove fields in option two anyway. See cause (4)

Additionally: break changes with the current release:

* option 2 - no

* option 3 - break. Also, we need to remove ext and ipc sets from ops structure

We broke it anyway by moving free from ext to main struct.

From what I see option 2 wins with 3 in all fields except the dlsym case where option 2 could easily support only single version back

Thinks that win in option 2 are things you cannot do anyway do to requirement (4), as this is public structure, so we cannot remove or rename fields. User program must always compile,
The difference between versions are:
potential errors with user using wrong size vs extra burden with maintaing more version of the struct internaly, and dlopen issue.

@lplewa
Copy link
Contributor Author

lplewa commented Feb 6, 2025

You can map version to size. Again, I don't care either way.

We need size - user might get osprovider ops, duplicate them, and then i.e change one function to his (i.e to implement some extra logging). to duplicate struct user must know size of the struct, as it might be larger then this what he got from his header.

@pbalcer
Copy link
Contributor

pbalcer commented Feb 6, 2025

We need size - user might get osprovider ops, duplicate them, and then i.e change one function to his (i.e to implement some extra logging). to duplicate struct user must know size of the struct, as it might be larger then this what he got from his header.

The possible scenario is something like this:


struct some_ops user_ops = *umfCreateSomeOps(); // struct some_ops is smaller than returned object, and the new fields are ignored. but version is new

// fix if we go with version:
user_ops.version = UMF_CURRENT_VERSION; // this will be the version from the umf headers

// fix if we go with size:
user_ops.size = sizeof(user_ops);

Not much difference either way. We can even add a convince macro/function for this specific purpose:
struct some_ops user_ops = UMF_OPS_DEREF(umfCreateSomeOps());
or:

struct some_ops user_ops;
UMF_OPS_COPY_BY_VALUE(&user_ops, *umfCreateSomeOps());

Again, I don't mind either solution. If you all think size is better, we can do size. The only consideration is that we are breaking the existing structs. But that might be a worthwhile compromise...

@vinser52
Copy link
Contributor

vinser52 commented Feb 6, 2025

I agree with @pbalcer that it does not matter size vs version. From my perspective version is more precise and allows to get the right size from the version.

Our OPS structures for pools and providers are currently not backward compatible, even though they include a version field. The following are four separate requirements we must fulfill:

  1. User calls umfDisjointPoolOps() and passes the resulting structure to umf*Create().
    (No manual struct creation—just calling the UMF-provided function.)

  2. User creates their own provider/pool structure (manually defining the fields) and passes it to umf*Create().
    (We need to detect the old structure version and handle it properly.)

  3. User obtains our OPS structure, duplicates it (it SHOULD be const), and modifies it.
    (Challenge: if the new UMF version’s OPS structure has a different size, the user doesn’t know how much memory to allocate.)

  4. User compiles a program (written against an older UMF release) without any source changes, with a new UMF version.
    (The program should compile without problems and continue to work.)

I think bullets 1 - 3 are possible scenarios and bullet 4 is the only subject for discussion regarding backward compatibility (in the abovementioned scenarios). If versions of UMF at compile-time and runtime are the same there are no issues with the abovementioned scenarios 1 - 3.

Scenario 1 is not an issue, since runtime returns the pointer to the ops structure used by the runtime. So there is no version mismatch.

Scenario 2 is not an issue as well, because the version of the UMF at runtime should be higher or equal to the version of UMF used by the client at compile-time. And UMF should know how to handle older versions of ops structures.

Scenario 3:
Regarding copying the ops structs it is an interesting point. We also should consider two possible ways of copying: byte-copying, field-copying:

// byte copying
umf_memory_pool_ops_t *src_ops = umfDisjointPoolOps();
umf_memory_pool_ops_t dst_ops;

memcpy(&dst_ops, src_ops, sizeof(umf_memory_pool_ops_t ));
// do not forget to set the right version in dst_ops.
// Otherwise, it will contain the version from src_ops
dst_ops.version = UMF_VERSION_CURRENT;

// field-copying
dst_ops.version = UMF_VERSION_CURRENT;
dst_ops.initialize = src_ops->initialize;
dst_ops.finalize = src_ops->finalize;
...

In case the ops struct is public we can only add new fields to the end of the ops struct to support backward compatibility. In case the newer version of UMF adds a new field to the ops struct, but the client is compiled with an older one there is no issue with the above-mentioned byte-copying and field-copying scenarios because the size of dst_ops is smaller or equal to the size of src_ops.

In case of the version of UMF used at run-time is less than the version of UMF used to compile the client code, it is a forward compatibility scenario that we are not going to support.

@lplewa
Copy link
Contributor Author

lplewa commented Feb 6, 2025

There is multiple options:

const umf_memory_provider_ops_t * ops = umfOsMemoryProviderOps(void);
umf_memory_provider_ops_t  *myops = malloc(ops.size);
memcpy(myops, ops, ops.size);
myops.free = &my_custom_free_function; 
// this code is wrong
//umf_memory_provider_ops_t  *myops = malloc(sizeof(*myops));
//memcpy(myops, ops, sizeof(*myops));

// this is fine
umf_memory_provider_ops_t  *myops = malloc(sizeof(*myops)); 
//or 
umf_memory_provider_ops_t  o , *myops = &o;

myops->version = UMF_VERSION_CURRENT; //or myops->size= sizeof(*myops)

@bratpiorka's solution solves this pitfall for the user by providing different version umfOsMemoryProviderOps(), for each change in ops structure. So always struct returned has the same size, that user compiled against their program. In his case user can just do.

const umf_memory_provider_ops_t * ops = umfOsMemoryProviderOps(void);
umf_memory_provider_ops_t  *myops = malloc(sizeof(*myops));
memcpy(myops, ops, sizeof(*myops));
assert(myops.size == sizeof(*myops); // allways true in this approach.

But this solution comes at cost extra complication in case of dlsym() usage, and extra maintenance burden in umf code.

@vinser52
Copy link
Contributor

vinser52 commented Feb 6, 2025

First, I would prefer the umfOsMemoryProviderOps function to return a const pointer to ops so that the user cannot modify in-place.

Second, for me, this looks like a hacky error-prone approach:

umf_memory_provider_ops_t  *myops = malloc(ops.size);
memcpy(myops, ops, ops.size);
myops.free = &my_custom_free_function; 

We might think of providing API to create a copy of ops so that users can modify the copy in-place. But how important is such a use case?

I would not say this example is wrong:

// this code is wrong
//umf_memory_provider_ops_t  *myops = malloc(sizeof(*myops));
//memcpy(myops, ops, sizeof(*myops));

It is similar to the "byte copying" example I provided above.

@bratpiorka's solution solves this pitfall for the user by providing different version umfOsMemoryProviderOps(), for each change in ops structure. So always struct returned has the same size, that user compiled against their program. In his case user can just do.

const umf_memory_provider_ops_t * ops = umfOsMemoryProviderOps(void);
umf_memory_provider_ops_t  *myops = malloc(sizeof(*myops));
memcpy(myops, ops, sizeof(*myops));
assert(myops.size == sizeof(*myops); // allways true in this approach.

But this solution comes at cost extra complication in case of dlsym() usage, and extra maintenance burden in umf code.

That is why we should not overcomplicate the solution to cover "fancy" scenarios. It is good that we are discussing it here, but the solution should take into account which scenario we want to handle and which we don't.

@pbalcer
Copy link
Contributor

pbalcer commented Feb 6, 2025

100% agreed with @vinser52

@bratpiorka
Copy link
Contributor

Great to see everyone agreed :) so either please review my PR, or propose a PR with a solution + list what case it covers.

Please note that we plan to clean up the API + add CTL-related functions to the ops and ideally we do not want to break the API in either case.

@lplewa
Copy link
Contributor Author

lplewa commented Feb 6, 2025

First, I would prefer the umfOsMemoryProviderOps function to return a const pointer to ops so that the user cannot modify in-place.

This is separate issue - please see: #1080

Second, for me, this looks like a hacky error-prone approach:

umf_memory_provider_ops_t *myops = malloc(ops.size);
memcpy(myops, ops, ops.size);
myops.free = &my_custom_free_function;

You must do this, or reset version/size field after memcpy. This is also error-prone.

We might think of providing API to create a copy of ops so that users can modify the copy in-place. But how important is such a use case?
Maybe, not sure if it's needed - we would need it if we make ops struct opaque.

I would not say this example is wrong:

// this code is wrong
//umf_memory_provider_ops_t *myops = malloc(sizeof(*myops));
//memcpy(myops, ops, sizeof(*myops));

It is similar to the "byte copying" example I provided above.

You must reset size/version field. Otherwise if user compile agains older umf version and then uses it with new one.

But this solution comes at cost extra complication in case of dlsym() usage, and extra maintenance burden in umf code.

That is why we should not overcomplicate the solution to cover "fancy" scenarios. It is good that we are discussing it here, but the solution should take into account which scenario we want to handle and which we don't.

I agree. Just wanted to explain it's use case if the other option.

@vinser52
Copy link
Contributor

vinser52 commented Feb 6, 2025

Second, for me, this looks like a hacky error-prone approach:
umf_memory_provider_ops_t *myops = malloc(ops.size);
memcpy(myops, ops, ops.size);
myops.free = &my_custom_free_function;

You must do this, or reset version/size field after memcpy. This is also error-prone.

Agree that it is error-prone as well.

Furthermore, now I am thinking that even copying the ops structure and modifying a particular callback is error-prone because it breaks encapsulation. For example, to substitute free callback you need to know how alloc callback is implemented, the same is true for initialize/finalize pair, etc. So for me it is hard to imagine why user would want to copy the ops structure.

You must reset size/version field. Otherwise if user compile agains older umf version and then uses it with new one.

In general I think that we (UMF developers) should avoid delegation of compatibility issues to the customers if possible. It is our (not the user's) responsibility to provide backward compatibility.

I agree. Just wanted to explain it's use case if the other option.

yeah, fully agree that we need to discuss such questions to make weighted decisions.

@lplewa
Copy link
Contributor Author

lplewa commented Feb 6, 2025

Furthermore, now I am thinking that even copying the ops structure and modifying a particular callback is error-prone because it breaks encapsulation. For example, to substitute free callback you need to know how alloc callback is implemented, the same is true for initialize/finalize pair, etc. So for me it is hard to imagine why user would want to copy the ops structure.

This is why i think size > version but this is not a huge deal

@lplewa
Copy link
Contributor Author

lplewa commented Feb 7, 2025

Let me summarize discussion,

  • We stay with Version in ops structure (eventually we replace it with size)
  • If we stay with version we must replace UMF_VERSION_CURRENT for provider and pool ops, with separate versions (we can update them separately, and not each update will break ops version). We did not discuss about it here but it was a part of the Add backward compatibility workflow and tests #1063.
  • In this Release we decided to move free from ops.ext.free to ops.free, as it is not longer optional. This mean that this release is a breaking compatibility - this was a reason for Add backward compatibility workflow and tests #1063 to reduce impact of this change, but we are not doing it.
  • There is the issue with ext and ipc substructures. Bellow you have provider ops (without comments). We ether must put IPC struct inside of the ext struct, or remove ext, and ipc structs, and put everything in single struct. Otherwise as extra functions must be added at the end of the struct, we cannot add it to ext struct, as there is ipc after it.
typedef struct umf_memory_provider_ext_ops_t {
    umf_result_t (*purge_lazy)(void *provider, void *ptr, size_t size);
    umf_result_t (*purge_force)(void *provider, void *ptr, size_t size);
    umf_result_t (*allocation_merge)(void *hProvider, void *lowPtr,
                                     void *highPtr, size_t totalSize);
    umf_result_t (*allocation_split)(void *hProvider, void *ptr,
                                     size_t totalSize, size_t firstSize);
} umf_memory_provider_ext_ops_t;

typedef struct umf_memory_provider_ipc_ops_t {
    umf_result_t (*get_ipc_handle_size)(void *provider, size_t *size);
    umf_result_t (*get_ipc_handle)(void *provider, const void *ptr, size_t size,
                                   void *providerIpcData);
    umf_result_t (*put_ipc_handle)(void *provider, void *providerIpcData);
    umf_result_t (*open_ipc_handle)(void *provider, void *providerIpcData,
                                    void **ptr);
    umf_result_t (*close_ipc_handle)(void *provider, void *ptr, size_t size);
} umf_memory_provider_ipc_ops_t;

typedef struct umf_memory_provider_ops_t {
    uint32_t version;
    umf_result_t (*initialize)(void *params, void **provider);
    void (*finalize)(void *provider);
    umf_result_t (*alloc)(void *provider, size_t size, size_t alignment,
                          void **ptr);
    umf_result_t (*free)(void *provider, void *ptr, size_t size);
    void (*get_last_native_error)(void *provider, const char **ppMessage,
                                  int32_t *pError);
    umf_result_t (*get_recommended_page_size)(void *provider, size_t size,
                                              size_t *pageSize);
    umf_result_t (*get_min_page_size)(void *provider, void *ptr,
                                      size_t *pageSize);
    const char *(*get_name)(void *provider);
    umf_memory_provider_ext_ops_t ext;
    umf_memory_provider_ipc_ops_t ipc;
} umf_memory_provider_ops_t;

@bratpiorka
Copy link
Contributor

bratpiorka commented Feb 7, 2025

ok, so here is my summary/proposal:

  • extract changes related to compatibility workflow, separate provider/pool versions, and version assert removal from my Add backward compatibility workflow and tests #1063
  • break compatibility vs 0.10.1 (oneAPI 2025.1) and with 0.11.0-devX (current 2025.2 used in UR)
  • keep all ipc and ext functions inside the main structure, but add ext_* and ipc_* prefix where needed, eg. purge_lazy -> ext_purge_lazy (something to consider). As IPC functions already contain "ipc" they could be renamed to get_ipc_handle -> ipc_get_handle
  • add a variadic elem at the end of ops structs + add a size after version (have both size and version - yes, they could be mapped to each other but means something different)
  • create a tag for this change (0.12.0-dev1) and use it in the compatibility workflow

draft: #1087

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

No branches or pull requests

4 participants