1672445340
Note: While Proton is already a very powerful and flexible framework, it is still in early stages of development. The APIs and public interfaces are still undergoing revisions and may introduce breaking changes with every version bump before reaching stable version 1.0.0.
Proton is a simple library that allows you to extend the behavior of a textview to add rich content that you always wanted. It provides simple API that allows you to extend the textView to include complex content like nested textViews or for that matter, any other UIView. In the simplest terms - It's what you always wanted UITextView
to be.
Proton is designed keeping the following requirements in mind:
At it's core, Proton constitutes of following key components:
UITextView
that can be extended to add custom views including other EditorViews.TextProcessor
.EditorView
. Attachment is a supercharged NSTextAttachment
that can have automatic constraints applied on it to size it in various configurations like matching content, range of width, fixed width and so on. It also has helper functions to get it's range in it's container as well as to remove itself from the container.The power of EditorView
to host rich content is made possible by the use of Attachment
which allows hosting any UIView
in the EditorView
. This is further enhanced by use of TextProcessor
and EditorCommand
to add interactive behavior to the editing experience.
Let's take an example of a Panel
and see how that can be created in the EditorView
. Following are the key requirements for a Panel
:
Editor
.>>
char.backspace
key when empty similar to a Blockquote
.The first thing that is required is to create a view that represents the Panel
. Once we have created this view, we can add it to an attachment and insert it in the EditorView
.
extension EditorContent.Name {
static let panel = EditorContent.Name("panel")
}
class PanelView: UIView, BlockContent, EditorContentView {
let container = UIView()
let editor: EditorView
let iconView = UIImageView()
var name: EditorContent.Name {
return .panel
}
override init(frame: CGRect) {
self.editor = EditorView(frame: frame)
super.init(frame: frame)
setup()
}
var textColor: UIColor {
get { editor.textColor }
set { editor.textColor = newValue }
}
override var backgroundColor: UIColor? {
get { container.backgroundColor }
set {
container.backgroundColor = newValue
editor.backgroundColor = newValue
}
}
private func setup() {
// setup view by creating required constraints
}
}
As the Panel
contains an Editor
inside itself, the height will automatically change based on the content as it is typed in. To restrict the height to a given maximum value, an absolute size or autolayout constraint may be used.
Using the textColor
property, the default font color may be changed.
For the ability to add Panel
to the Editor
using a button, we can make use of EditorCommand
. A Command
can be executed on a given EditorView
or via CommandExecutor
that automatically takes care of executing the command on the focussed EditorView
. To insert an EditorView
inside another, we need to first create an Attachment
and then used a Command
to add to the desired position:
class PanelAttachment: Attachment {
var view: PanelView
init(frame: CGRect) {
view = PanelView(frame: frame)
super.init(view, size: .fullWidth)
view.delegate = self
view.boundsObserver = self
}
var attributedText: NSAttributedString {
get { view.attributedText }
set { view.attributedText = newValue }
}
}
class PanelCommand: EditorCommand {
func execute(on editor: EditorView) {
let selectedText = editor.selectedText
let attachment = PanelAttachment(frame: .zero)
attachment.selectBeforeDelete = true
editor.insertAttachment(in: editor.selectedRange, attachment: attachment)
let panel = attachment.view
panel.editor.maxHeight = 300
panel.editor.replaceCharacters(in: .zero, with: selectedText)
panel.editor.selectedRange = panel.editor.textEndRange
}
}
The code in PanelCommand.execute
reads the selectedText
from editor
and sets it back in panel.editor
. This makes it possible to take the selected text from main editor, wrap it in a panel and then insert the panel in the main editor replacing the selected text.
To allow insertion of a Panel
using a shortcut text input instead of clicking a button, you can use a TextProcessor
:
class PanelTextProcessor: TextProcessing {
private let trigger = ">> "
var name: String {
return "PanelTextProcessor"
}
var priority: TextProcessingPriority {
return .medium
}
func process(editor: EditorView, range editedRange: NSRange, changeInLength delta: Int, processed: inout Bool) {
let line = editor.currentLine
guard line.text.string == trigger else {
return
}
let attachment = PanelAttachment(frame: .zero)
attachment.selectBeforeDelete = true
editor.insertAttachment(in: line.range, attachment: attachment)
}
For a requirement like deleting the Panel
when backspace is tapped at index 0 on an empty Panel, EdtiorViewDelegate
may be utilized:
In the code above, PanelViewDelegate
is acting as a passthrough for EditorViewDelegate
for the Editor
inside the PanelView
.
Checkout the complete code in the ExamplesApp.
extension PanelAttachment: PanelViewDelegate {
func panel(_ panel: PanelView, didReceiveKey key: EditorKey, at range: NSRange, handled: inout Bool) {
if key == .backspace, range == .zero, panel.editor.attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
removeFromContainer()
handled = true
}
}
}
Changing text as it is typed using custom TextProcessor
:
Adding attributes as it is typed using custom TextProcessor
:
Nested editors
Panel from existing text:
Relaying attributes to editor contained in an attachment:
Highlighting using custom command in Editor:
Find text and scroll in Editor:
Feel free to create issues in github should you have any questions or feature requests. While Proton is created as a side project, I'll endeavour to respond to your issues at earliest possible.
Author: Rajdeep
Source Code: https://github.com/rajdeep/proton
License: View license
1671698902
Writing multiple native applications for different mobile platforms can be a daunting task, but taking a non-native approach comes with pitfalls of its own. In this article, Toptal Mobile Developer Joel Frank outlines the concept of Write Once, Deploy Everywhere (WODE) and explains how to choose the right development path for your project.
Write Once, Deploy Everywhere (WODE) has been the promise of many development frameworks over the past decade, whose goal is to ease the pain of writing multiple native applications. Determining which path to take is one of the most critical decisions a project manager has to make due to its high startup cost, impact upon the development team, and potential infeasibility to reverse.
Hybrid solutions such as Ionic leverage web technologies to render applications across platforms, but often, the end product falls short of a user’s expectations of a native look and feel.
However, even the term “native” has recently been muddied by frameworks that compile down to native code (e.g., React Native, Xamarin, etc.).
This article breaks down the pros and cons of various mobile development paths and weighs them against team makeup, cost, and user experience in an effort to empower product managers to make a more informed decision.
Write Once, Deploy Everywhere
The concept of Write Once, Deploy Everywhere refers to the ability of a development team to write an application once—using a single development stack, abstract of the platform(s) upon which the application will be deployed—and yet maintain the ability to deploy the application to all desired platforms, e.g., Android, iOS, Windows, etc. Ideally, this is accomplished without sacrificing maintainability, performance, or user experience (UX).
The alternative - and historical - method of mobile application development involves the straightforward process of simply writing a separate application for each platform, which, of course, carries with it its own potential high cost of time and resources.
In general, the factors to consider when choosing a development path include:
Unfortunately, applying each of these factors to each of the available paths, as well as wading through the myriad of available opinions on the subject, can be quite daunting. Furthermore, this process often leaves the project manager with a sense of uncertainty as to which path is best in order to meet the application’s requirements.
At a high level, the different mobile development paths can be put into two categories: native or WODE, i.e., native or everything else. Put simply, one either writes a native application or one doesn’t. The WODE category is further broken down into two groups:
The majority of WODE frameworks are hybrid; however, in order to improve both performance and UX limitations of hybrid frameworks while still providing the benefits of a WODE framework, the current trend is toward non-hybrid. Due to this trend, frameworks such as React Native, Xamarin, and Appcelerator are gaining popularity.
Each of these paths—native, hybrid, and non-hybrid—has differing strengths and weaknesses, and as a result, each has different use cases for which it is most suited. The rest of this article breaks down the pros and cons of each mobile development path when considering competing priorities such as team makeup, project cost, and UX. With the exception of a few specialized use cases, writing native applications maximizes user experience at a slightly higher cost.
In general, the adage “you get what you pay for” applies, so if cost matters more than the customers’ experience, native may not be the right choice. However, once UX becomes vital, native applications become the clear choice since, in order to improve the UX, WODE-based applications incur a considerable cost in the form of either time or native expertise, which defeats the purpose of choosing a non-native development path in the first place.
Furthermore, even if that cost is paid, the WODE-based end product will always deliver an inferior UX when compared to its native counterpart. As a result, native is almost always the right choice for most development teams and for most projects.
Native applications are written in the core language of the given platform. For example, Android applications are written in Java, while iOS applications are written in either Obj-C or Swift. They require the development engineer to understand the language as well as the platform-specific nuances, which include, for example, third-party package integration, layout management, operating system (OS) interaction, and so on.
Highly customizable. Since each application is written utilizing native components, the only limitation to customization is the interface to the underlying frameworks, and sometimes not even then. As most native development engineers will attest, there is often a way to accomplish a given task in spite of a limited interface.
A simple proof of this idea can be found by browsing the support communities for a given platform. One will find numerous examples of how to accomplish a task that might be “off the reservation,” despite the limitations of the underlying frameworks.
A concrete iOS example of such a seemingly simple task might be to show a fullscreen overlay, above all external UI elements, e.g., a tab bar, navigation bar, etc. As shown in Figure 1, this is normally outside of the scope of the normal UI layer currently being presented. As such, in order to have a fullscreen overlay, it must be added to the hidden layer above the tab bar in the view stack. This kind of customization is typically only possible on native applications.
Figure 1: iOS TabBar layering example.
Highest performance. As expected, a native application sets the benchmark for performance. Since most other framework types add one or more intermediate layers, they inherently run more slowly than a native application.
Most maintainable. Operating systems change constantly. Period. When they do, depending on whether breaking changes were made, an application must be updated in a timely fashion so as not to lose the part of one’s user base that upgrades to the newer OS. Obviously, the less time that passes between when the change is made available to one’s users and an application is updated, the better. This time is minimized when there are no dependencies that need to be updated in order to support this new OS, which is the case when working on a native application.
Additional resources. When writing applications for multiple platforms, a development team typically consists of one or more mobile software engineers for each supported platform. This, of course, inherently increases the size and cost of a development team. It also requires the team of engineers to have a variety of skills, as opposed to having a homogenous skill base. This has the potential to fragment a team with regard to support and collaboration.
Slower development cycle. Native apps have the potential to have a slower development cycle simply because a separate application must be written for each desired platform. The extreme case is when there is a single mobile development engineer on the team since each application is essentially written in series.
Low performance. It might seem odd to have performance as both a pro and a con. On one hand, native applications give the developer enough room to create a finely tuned, high-performance application. On the other hand, however, they also give the developer enough rope to hang themselves. If they don’t know what they are doing, in the end, they will end up with a subpar application at best.
Note: In general, this applies to all framework paths (native, hybrid, and non-hybrid). If the engineers developing an application have insufficient skills for what they are attempting, the resulting application will likely neither meet design requirements nor be well accepted by users.
Hybrid applications are typically written using HTML/CSS/LESS to design the user interface: the “V” in the MVC design pattern. The “C,” or controller, is then typically written in JavaScript - ideally, using a JavaScript MVC framework such as AngularJS. The use of a framework like AngularJS allows for a better separation of classes and responsibilities than is typically possible using only jQuery.
An example of this type of hybrid framework stack would be an Ionic view layer backed by AngularJS, which is ultimately converted and rendered in a web view on the desired platform using PhoneGap and Cordova. Obviously, this type of WODE framework comes at the cost of added complexity.
Furthermore, the use of JavaScript brings with it its own design-based limitations and language-based issues. The goal of this article is not to debate the merits or flaws of any one language; however, as a project manager, the choice to use JavaScript in one’s mobile technical stack should not be made lightly. The following are just a few examples of well-written articles on why JavaScript should be avoided if possible:
Minimal development team. Hybrid frameworks enable a small development team—and particularly one whose primary knowledge base is web development—to quickly produce simple applications across multiple platforms. This allows a project manager to keep his team small as well as remove the need for his team to learn the native languages and frameworks for multiple platforms.
Faster development cycle. In addition to a smaller team, hybrid frameworks afford a faster development cycle when deploying to multiple platforms since only a single view layer needs to be designed using web technologies.
Potentially poor UX. The downside of only having to write a single view layer is that one is left with a single view layer. This can result in a poor UX since a one-size-fits-all approach to UI design fails to give one’s application a look and feel that is comfortable and familiar to users on all platforms. In addition, since hybrid applications are essentially a webview embedded within the UI, it can give users the impression that they are actually viewing a web page instead of interacting with a native application. This experience almost always has a negative impact upon user satisfaction, and ultimately retention.
Costly to customize. Improving upon the UX by designing customized UIs for each platform results in complex and unique UI frameworks that can be costly to create and difficult to maintain over time. Furthermore, in order to create UI elements that will help make one’s application stand out (e.g., animation, custom views, etc.), customized bridge components must be created to translate the high-level UI design into something that the lower-level framework, such as Cordova, will understand. In general, the more one customizes and improves the UX of a hybrid application, the more it diminishes the benefit of a fast and inexpensive design cycle.
Lower performance. Since hybrid applications render the application’s views within a webview, there is a large potential to make implementation mistakes when dealing with OS frameworks (e.g., networking, Bluetooth, on-device contacts, etc.), which result in vastly degraded performance. It is also worth noting that, even if great care is taken with regard to performance since everything is displayed via a webview, the hybrid applications’ maximum performance will always be slightly less than their native counterparts.
Non-trivial plugin management. Remember those custom features the design team spent weeks polishing, which was followed by another few weeks while the development team created the necessary bridge components so that Cordova could work with them? Well, they won’t work unless there is a supporting Cordova plugin for what the team is trying to achieve. This means one of two things: either the team creates it themselves or a suitable third-party plugin will need to be found that does the job. Unfortunately, more often than not, option two does not exist. As a result, it requires additional development time to create the custom plugins, followed by build support effort—over time—to manage the growing library of Cordova plugins required by the application. Of course, when Cordova updates occur, there is a high probability that these plugins will need to be updated as well.
OS support lag. The previously mentioned cascading bridge component/Cordova plugin issue is further exacerbated when the OS changes core functionality. Once Cordova, PhoneGap, and Ionic have been updated to support the changes, it is possible that the custom plugins and bridge components will need to be updated as well. Regardless of the order of magnitude this work would require, it results in additional time during which the application doesn’t support end users who have updated to the new OS. This, of course, is a worst-case scenario in which breaking, non-backward compatible changes are made by Apple or Google, which never happens… right? In general, any intermediate framework that is out of the developer’s control and that must be updated first only serves to delay the process. Finally, relying upon an intermediate framework can be a headache for project managers to plan around since the timing of these frameworks is such an unknown.
Non-hybrid Applications
Non-hybrid applications start out life just like their hybrid counterparts - a UI layer designed in a non-native platform language: React Native uses HTML/CSS backed by JavaScript or Xamarin, which is based on C# due to its .NET roots.
However, that is where the similarity ends. Non-hybrid applications compile down to native code and render the application using platform-native components instead of rendering via a webview. This results in a WODE framework that, at least on the surface, has the best of both worlds.
As previously discussed, choosing to use JavaScript should not be a decision that is made lightly, and it might be a deal-breaker for a development team that does not wish to learn or has no interest in using JavaScript.
Higher performance than hybrids. As one might expect, non-hybrids inherently have a higher performance than hybrid applications due to their ability to render the application using native UI components (buttons, views, layout managers, etc.) instead of relying on an embedded webview. Of course, developers are still free to write an application that performs either remarkably or horribly. The benefit of non-hybrid applications is simply that they have a higher performance baseline when compared to similar hybrid applications.
Minimal development team. Similar to hybrid frameworks, non-hybrids enable a small development team—and particularly one whose primary knowledge base is web development—to quickly produce simple applications across multiple platforms. This allows project managers to keep their team small and to preclude the team from learning the native languages and frameworks for multiple platforms.
Faster development cycle. In addition to a smaller team, non-hybrid frameworks afford a faster development cycle when deploying to multiple platforms, since only a single view layer needs to be designed.
Faster iterations (React). The React framework provides a powerful feature that allows changes to the application to be rendered in real time: no need to recompile, rebuild, etc. As a result, the React emulator is an incredibly powerful development tool that dramatically reduces the duration of each implementation cycle.
Costly to customize. Much like its hybrid counterpart, when non-hybrid applications require the UX to be improved by designing customized UIs for each platform, it results in complex and unique UI components that can be costly to create and difficult to maintain over time. This also means writing customized bridge components to supplement gaps in the framework’s native element support. Like hybrids, this cost diminishes the benefit of a fast and inexpensive design cycle, but unlike hybrid applications, bridge components are written for each desired platform in their native language. This means that instead of non-hybrid applications being a flexible alternative to a team that is primarily comprised of web developers, teams choosing the non-hybrid path have to learn not only the particular language of the framework (e.g., JSX or C#) but also the native language of each platform(Java, Obj-C, or Swift).
Third-party dependencies. This limitation takes two different forms. In the case of React Native, it takes the form of numerous dependencies, i.e., roughly 650. The result is that there is a very good chance at any particular time that one or more of those dependencies is out of date. It also means that in the event of a large OS-level change, there is a high probability that most or all of those dependencies will need to be updated. The potential saving grace is that Facebook uses React, so one will have the 300lb gorilla in their corner.
In the case of Xamarin, the third-party dependency issue is simply that it is extremely difficult to integrate them in the first place. Xamarin is aware of this issue and provides a utility tool called Sharpie. The tool’s purpose is to help with some of the integration, but unfortunately, Sharpie often attempts to compile and link incorrect resources, which forces the developer to undertake the painstakingly time-consuming task of manually modifying low-level compilation parameters in order to successfully complete the integration.
OS support lag. Non-hybrid applications are plagued by the same issue as hybrid ones. Any intermediate framework that is out of the developer’s control and that must be updated first only serves to delay the process of updating one’s application to support cutting-edge users. Furthermore, as stated before, relying upon an intermediate framework can be a headache for project managers to plan around since the timing of these frameworks is such an unknown.
Long-term support (React Native). This issue is specific to React Native and pertains to the odd fact that, to date, Facebook has not committed to a long-term support plan for its framework. It can be said that this is a low risk since the company utilizes its own framework for its mobile apps, but it is worth a pause for any project manager to consider why Facebook has refused to comment on the subject.
When cost is not a primary consideration, Figure 2 shows that writing native applications is almost always the best choice when leveraging the makeup of the development team against the application’s requirements. When there are fewer development engineers than the number of desired platforms, it gets a bit more interesting. In that case, using React is the correct choice if the team is under a very tight release schedule; otherwise, going native is still the best option.
Figure 2: Comparison of team makeup vs. application requirements when choosing a mobile development path.
When the team is primarily a web development team, and a customized UX is required, it is better to have some team members change hats or add some team members in order to make one’s applications native. There really isn’t a feasible, maintainable framework option if an application requires custom elements, which many applications do.
However, if a custom UX is not required, then, depending on the release schedule, it might be better to go with Ionic or React. If one’s team has no time to learn JSX, then Ionic is the right choice. Otherwise, it is better to choose React since it already requires many third-party dependencies, and adding more isn’t going to impact one’s development cycle.
Figure 3: Cost vs. User Experience when choosing a mobile development path.
Once the cost of the project is a primary concern, typically, the existing team makeup becomes less of a priority since the first step would be to put the proper team in place to execute the project plan for the projected cost. As shown in Figure 3, native applications have a higher starting cost than their WODE counterparts, but also a higher potential UX. Moreover, WODE applications will always be limited in their UX, regardless of how much money and resources are applied to the project.
I hope this article has shed some light on the pros and cons of various mobile development paths, as well as aided in weighing team makeup against both application requirements and the cost of the project. Its message was not to convey that WODE frameworks are inferior and should never be sought out, but rather that even though there are valid use cases for not going native, one should fully understand the ramifications of doing so.
Original article source at: https://www.toptal.com/
1668041460
Spring Native provides beta support for compiling Spring applications to native executables using GraalVM native-image compiler, in order to provide a native deployment option typically designed to be packaged in lightweight containers.
Watch the video and read the blog post of Spring Native Beta announcement to learn more.
The easiest way to start with Spring Native is probably to go to start.spring.io, add the Spring Native dependency, and read the reference documentation. Make sure to configure properly the Spring AOT Maven and Gradle plugins that are mandatory to get proper native support for your Spring application.
Note | You need to install the GraalVM native-image compiler, check the documentation for more details. |
Download the latest release of this repository.
Go into the samples folder and pick one (e.g. cd samples/commandlinerunner
)
Run ./build.sh
which will run the regular JVM build, then a native image compilation, then test the result.
For more details on the samples see the samples documentation.
If you have not previously done so, please sign the Contributor License Agreement. You will be reminded automatically when you submit the pull request.
Contributions are welcome, especially for adding support via pull requests for libraries widely used in the Spring ecosystem not yet support. Please refer to the how to contribute section for more details.
This project requires Java 11.
Author: Spring-projects-experimental
Source Code: https://github.com/spring-projects-experimental/spring-native
License: Apache-2.0 license
1667641740
Write Perl bindings to non-Perl libraries with FFI. No XS required.
SYNOPSIS
use FFI::Platypus 2.00;
# for all new code you should use api => 2
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef); # search libc
# call dynamically
$ffi->function( puts => ['string'] => 'int' )->call("hello world");
# attach as a xsub and call (much faster)
$ffi->attach( puts => ['string'] => 'int' );
puts("hello world");
DESCRIPTION
Platypus is a library for creating interfaces to machine code libraries written in languages like C, C++, Go, Fortran, Rust, Pascal. Essentially anything that gets compiled into machine code. This implementation uses libffi to accomplish this task. libffi is battle tested by a number of other scripting and virtual machine languages, such as Python and Ruby to serve a similar role. There are a number of reasons why you might want to write an extension with Platypus instead of XS:
FFI / Platypus does not require messing with the guts of Perl
XS is less of an API and more of the guts of perl splayed out to do whatever you want. That may at times be very powerful, but it can also be a frustrating exercise in hair pulling.
FFI / Platypus is portable
Lots of languages have FFI interfaces, and it is subjectively easier to port an extension written in FFI in Perl or another language to FFI in another language or Perl. One goal of the Platypus Project is to reduce common interface specifications to a common format like JSON that could be shared between different languages.
FFI / Platypus could be a bridge to Raku
One of those "other" languages could be Raku and Raku already has an FFI interface I am told.
FFI / Platypus can be reimplemented
In a bright future with multiple implementations of Perl 5, each interpreter will have its own implementation of Platypus, allowing extensions to be written once and used on multiple platforms, in much the same way that Ruby-FFI extensions can be use in Ruby, JRuby and Rubinius.
FFI / Platypus is pure perl (sorta)
One Platypus script or module works on any platform where the libraries it uses are available. That means you can deploy your Platypus script in a shared filesystem where they may be run on different platforms. It also means that Platypus modules do not need to be installed in the platform specific Perl library path.
FFI / Platypus is not C or C++ centric
XS is implemented primarily as a bunch of C macros, which requires at least some understanding of C, the C pre-processor, and some C++ caveats (since on some platforms Perl is compiled and linked with a C++ compiler). Platypus on the other hand could be used to call other compiled languages, like Fortran, Go, Rust, Pascal, C++, or even assembly, allowing you to focus on your strengths.
FFI / Platypus does not require a parser
Inline isolates the extension developer from XS to some extent, but it also requires a parser. The various Inline language bindings are a great technical achievement, but I think writing a parser for every language that you want to interface with is a bit of an anti-pattern.
This document consists of an API reference, a set of examples, some support and development (for contributors) information. If you are new to Platypus or FFI, you may want to skip down to the EXAMPLES to get a taste of what you can do with Platypus.
Platypus has extensive documentation of types at FFI::Platypus::Type and its custom types API at FFI::Platypus::API.
You are strongly encouraged to use API level 1 for all new code. There are a number of improvements and design fixes that you get for free. You should even consider updating existing modules to use API level 1 where feasible. How do I do that you might ask? Simply pass in the API level to the platypus constructor.
my $ffi = FFI::Platypus->new( api => 2 );
The Platypus documentation has already been updated to assume API level 1.
CONSTRUCTORS
my $ffi = FFI::Platypus->new( api => 2, %options);
Create a new instance of FFI::Platypus.
Any types defined with this instance will be valid for this instance only, so you do not need to worry about stepping on the toes of other CPAN FFI / Platypus Authors.
Any functions found will be out of the list of libraries specified with the lib attribute.
api
[version 0.91]
Sets the API level. Legal values are
0
Original API level. See FFI::Platypus::TypeParser::Version0 for details on the differences.
1
Enable version 1 API type parser which allows pass-by-value records and type decoration on basic types.
2
Enable version 2 API. All new code should be written with this set to 1! The Platypus documentation assumes this api level is set.
API version 2 is identical to version 1, except:
Pointer functions that return NULL
will return undef
instead of empty list
This fixes a long standing design bug in Platypus.
Array references may be passed to pointer argument types
This replicates the behavior of array argument types with no size. So the types sint8*
and sint8[]
behave identically when an array reference is passed in. They differ in that, as before, you can pass a scalar reference into type sint8*
.
The fixed string type can be specified without pointer modifier
That is you can use string(10)
instead of string(10)*
as you were previously able to in API 0.
lib
Either a pathname (string) or a list of pathnames (array ref of strings) to pre-populate the lib attribute. Use [undef]
to search the current process for symbols.
0.48
undef
(without the array reference) can be used to search the current process for symbols.
ignore_not_found
[version 0.15]
Set the ignore_not_found attribute.
lang
[version 0.18]
Set the lang attribute.
ATTRIBUTES
$ffi->lib($path1, $path2, ...);
my @paths = $ffi->lib;
The list of libraries to search for symbols in.
The most portable and reliable way to find dynamic libraries is by using FFI::CheckLib, like this:
use FFI::CheckLib 0.06;
$ffi->lib(find_lib_or_die lib => 'archive');
# finds libarchive.so on Linux
# libarchive.bundle on OS X
# libarchive.dll (or archive.dll) on Windows
# cygarchive-13.dll on Cygwin
# ...
# and will die if it isn't found
FFI::CheckLib has a number of options, such as checking for specific symbols, etc. You should consult the documentation for that module.
As a special case, if you add undef
as a "library" to be searched, Platypus will also search the current process for symbols. This is mostly useful for finding functions in the standard C library, without having to know the name of the standard c library for your platform (as it turns out it is different just about everywhere!).
You may also use the "find_lib" method as a shortcut:
$ffi->find_lib( lib => 'archive' );
[version 0.15]
$ffi->ignore_not_found(1);
my $ignore_not_found = $ffi->ignore_not_found;
Normally the attach and function methods will throw an exception if it cannot find the name of the function you provide it. This will change the behavior such that function will return undef
when the function is not found and attach will ignore functions that are not found. This is useful when you are writing bindings to a library and have many optional functions and you do not wish to wrap every call to function or attach in an eval
.
[version 0.18]
$ffi->lang($language);
Specifies the foreign language that you will be interfacing with. The default is C. The foreign language specified with this attribute changes the default native types (for example, if you specify Rust, you will get i32
as an alias for sint32
instead of int
as you do with C).
If the foreign language plugin supports it, this will also enable Platypus to find symbols using the demangled names (for example, if you specify CPP for C++ you can use method names like Foo::get_bar()
with "attach" or "function".
[version 1.11]
my $level = $ffi->api;
Returns the API level of the Platypus instance.
METHODS
$ffi->type($typename);
$ffi->type($typename => $alias);
Define a type. The first argument is the native or C name of the type. The second argument (optional) is an alias name that you can use to refer to this new type. See FFI::Platypus::Type for legal type definitions.
Examples:
$ffi->type('sint32'); # only checks to see that sint32 is a valid type
$ffi->type('sint32' => 'myint'); # creates an alias myint for sint32
$ffi->type('bogus'); # dies with appropriate diagnostic
$ffi->custom_type($alias => {
native_type => $native_type,
native_to_perl => $coderef,
perl_to_native => $coderef,
perl_to_native_post => $coderef,
});
Define a custom type. See FFI::Platypus::Type#Custom-Types for details.
$ffi->load_custom_type($name => $alias, @type_args);
Load the custom type defined in the module $name, and make an alias $alias. If the custom type requires any arguments, they may be passed in as @type_args. See FFI::Platypus::Type#Custom-Types for details.
If $name contains ::
then it will be assumed to be a fully qualified package name. If not, then FFI::Platypus::Type::
will be prepended to it.
my @types = $ffi->types;
my @types = FFI::Platypus->types;
Returns the list of types that FFI knows about. This will include the native libffi
types (example: sint32
, opaque
and double
) and the normal C types (example: unsigned int
, uint32_t
), any types that you have defined using the type method, and custom types.
The list of types that Platypus knows about varies somewhat from platform to platform, FFI::Platypus::Type includes a list of the core types that you can always count on having access to.
It can also be called as a class method, in which case, no user defined or custom types will be included in the list.
my $meta = $ffi->type_meta($type_name);
my $meta = FFI::Platypus->type_meta($type_name);
Returns a hash reference with the meta information for the given type.
It can also be called as a class method, in which case, you won't be able to get meta data on user defined types.
The format of the meta data is implementation dependent and subject to change. It may be useful for display or debugging.
Examples:
my $meta = $ffi->type_meta('int'); # standard int type
my $meta = $ffi->type_meta('int[64]'); # array of 64 ints
$ffi->type('int[128]' => 'myintarray');
my $meta = $ffi->type_meta('myintarray'); # array of 128 ints
$ffi->mangler(\&mangler);
Specify a customer mangler to be used for symbol lookup. This is usually useful when you are writing bindings for a library where all of the functions have the same prefix. Example:
$ffi->mangler(sub {
my($symbol) = @_;
return "foo_$symbol";
});
$ffi->function( get_bar => [] => 'int' ); # attaches foo_get_bar
my $f = $ffi->function( set_baz => ['int'] => 'void' );
$f->call(22); # calls foo_set_baz
my $function = $ffi->function($name => \@argument_types => $return_type);
my $function = $ffi->function($address => \@argument_types => $return_type);
my $function = $ffi->function($name => \@argument_types => $return_type, \&wrapper);
my $function = $ffi->function($address => \@argument_types => $return_type, \&wrapper);
Returns an object that is similar to a code reference in that it can be called like one.
Caveat: many situations require a real code reference, so at the price of a performance penalty you can get one like this:
my $function = $ffi->function(...);
my $coderef = sub { $function->(@_) };
It may be better, and faster to create a real Perl function using the attach method.
In addition to looking up a function by name you can provide the address of the symbol yourself:
my $address = $ffi->find_symbol('my_function');
my $function = $ffi->function($address => ...);
Under the covers, function uses find_symbol when you provide it with a name, but it is useful to keep this in mind as there are alternative ways of obtaining a functions address. Example: a C function could return the address of another C function that you might want to call, or modules such as FFI::TinyCC produce machine code at runtime that you can call from Platypus.
[version 0.76]
If the last argument is a code reference, then it will be used as a wrapper around the function when called. The first argument to the wrapper will be the inner function, or if it is later attached an xsub. This can be used if you need to verify/modify input/output data.
Examples:
my $function = $ffi->function('my_function_name', ['int', 'string'] => 'string');
my $return_string = $function->(1, "hi there");
[version 0.91]
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types => $return_type);
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types => $return_type, \&wrapper);
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types);
my $function = $ffi->function( $name => \@fixed_argument_types => \@var_argument_types => \&wrapper);
Version 0.91 and later allows you to creat functions for c variadic functions (such as printf, scanf, etc) which can take a variable number of arguments. The first set of arguments are the fixed set, the second set are the variable arguments to bind with. The variable argument types must be specified in order to create a function object, so if you need to call variadic function with different set of arguments then you will need to create a new function object each time:
# int printf(const char *fmt, ...);
$ffi->function( printf => ['string'] => ['int'] => 'int' )
->call("print integer %d\n", 42);
$ffi->function( printf => ['string'] => ['string'] => 'int' )
->call("print string %s\n", 'platypus');
Some older versions of libffi and possibly some platforms may not support variadic functions. If you try to create a one, then an exception will be thrown.
[version 1.26]
If the return type is omitted then void
will be the assumed return type.
$ffi->attach($name => \@argument_types => $return_type);
$ffi->attach([$c_name => $perl_name] => \@argument_types => $return_type);
$ffi->attach([$address => $perl_name] => \@argument_types => $return_type);
$ffi->attach($name => \@argument_types => $return_type, \&wrapper);
$ffi->attach([$c_name => $perl_name] => \@argument_types => $return_type, \&wrapper);
$ffi->attach([$address => $perl_name] => \@argument_types => $return_type, \&wrapper);
Find and attach a C function as a real live Perl xsub. The advantage of attaching a function over using the function method is that it is much much much faster since no object resolution needs to be done. The disadvantage is that it locks the function and the FFI::Platypus instance into memory permanently, since there is no way to deallocate an xsub.
If just one $name is given, then the function will be attached in Perl with the same name as it has in C. The second form allows you to give the Perl function a different name. You can also provide an address (the third form), just like with the function method.
Examples:
$ffi->attach('my_function_name', ['int', 'string'] => 'string');
$ffi->attach(['my_c_function_name' => 'my_perl_function_name'], ['int', 'string'] => 'string');
my $string1 = my_function_name($int);
my $string2 = my_perl_function_name($int);
[version 0.20]
If the last argument is a code reference, then it will be used as a wrapper around the attached xsub. The first argument to the wrapper will be the inner xsub. This can be used if you need to verify/modify input/output data.
Examples:
$ffi->attach('my_function', ['int', 'string'] => 'string', sub {
my($my_function_xsub, $integer, $string) = @_;
$integer++;
$string .= " and another thing";
my $return_string = $my_function_xsub->($integer, $string);
$return_string =~ s/Belgium//; # HHGG remove profanity
$return_string;
});
[version 0.91]
$ffi->attach($name => \@fixed_argument_types => \@var_argument_types, $return_type);
$ffi->attach($name => \@fixed_argument_types => \@var_argument_types, $return_type, \&wrapper);
As of version 0.91 you can attach a variadic functions, if it is supported by the platform / libffi that you are using. For details see the function
documentation. If not supported by the implementation then an exception will be thrown.
my $closure = $ffi->closure($coderef);
my $closure = FFI::Platypus->closure($coderef);
Prepares a code reference so that it can be used as a FFI closure (a Perl subroutine that can be called from C code). For details on closures, see FFI::Platypus::Type#Closures and FFI::Platypus::Closure.
my $converted_value = $ffi->cast($original_type, $converted_type, $original_value);
The cast
function converts an existing $original_value of type $original_type into one of type $converted_type. Not all types are supported, so care must be taken. For example, to get the address of a string, you can do this:
my $address = $ffi->cast('string' => 'opaque', $string_value);
Something that won't work is trying to cast an array to anything:
my $address = $ffi->cast('int[10]' => 'opaque', \@list); # WRONG
$ffi->attach_cast("cast_name", $original_type, $converted_type);
$ffi->attach_cast("cast_name", $original_type, $converted_type, \&wrapper);
my $converted_value = cast_name($original_value);
This function attaches a cast as a permanent xsub. This will make it faster and may be useful if you are calling a particular cast a lot.
[version 1.26]
A wrapper may be added as the last argument to attach_cast
and works just like the wrapper for attach
and function
methods.
my $size = $ffi->sizeof($type);
my $size = FFI::Platypus->sizeof($type);
Returns the total size of the given type in bytes. For example to get the size of an integer:
my $intsize = $ffi->sizeof('int'); # usually 4
my $longsize = $ffi->sizeof('long'); # usually 4 or 8 depending on platform
You can also get the size of arrays
my $intarraysize = $ffi->sizeof('int[64]'); # usually 4*64
my $intarraysize = $ffi->sizeof('long[64]'); # usually 4*64 or 8*64
# depending on platform
Keep in mind that "pointer" types will always be the pointer / word size for the platform that you are using. This includes strings, opaque and pointers to other types.
This function is not very fast, so you might want to save this value as a constant, particularly if you need the size in a loop with many iterations.
[version 0.21]
my $align = $ffi->alignof($type);
Returns the alignment of the given type in bytes.
[version 1.24]
my $kind = $ffi->kindof($type);
Returns the kind of a type. This is a string with a value of one of
void
scalar
string
closure
record
record-value
pointer
array
object
[version 1.24]
my $count = $ffi->countof($type);
For array types returns the number of elements in the array (returns 0 for variable length array). For the void
type returns 0. Returns 1 for all other types.
[version 1.24]
$ffi->def($package, $type, $value);
my $value = $ff->def($package, $type);
This method allows you to store data for types. If the $package
is not provided, then the caller's package will be used. $type
must be a legal Platypus type for the FFI::Platypus instance.
[version 1.24]
my $unittype = $ffi->unitof($type);
For array and pointer types, returns the basic type without the array or pointer part. In other words, for sin16[]
or sint16*
it will return sint16
.
[version 0.20]
$ffi->find_lib( lib => $libname );
This is just a shortcut for calling FFI::CheckLib#find_lib and updating the "lib" attribute appropriately. Care should be taken though, as this method simply passes its arguments to FFI::CheckLib#find_lib, so if your module or script is depending on a specific feature in FFI::CheckLib then make sure that you update your prerequisites appropriately.
my $address = $ffi->find_symbol($name);
Return the address of the given symbol (usually function).
[version 0.96 api = 1+]
$ffi->bundle($package, \@args);
$ffi->bundle(\@args);
$ffi->bundle($package);
$ffi->bundle;
This is an interface for bundling compiled code with your distribution intended to eventually replace the package
method documented above. See FFI::Platypus::Bundle for details on how this works.
[version 0.15 api = 0]
$ffi->package($package, $file); # usually __PACKAGE__ and __FILE__ can be used
$ffi->package; # autodetect
Note: This method is officially discouraged in favor of bundle
described above.
If you use FFI::Build (or the older deprecated Module::Build::FFI to bundle C code with your distribution, you can use this method to tell the FFI::Platypus instance to look for symbols that came with the dynamic library that was built when your distribution was installed.
my $href = $ffi->abis;
my $href = FFI::Platypus->abis;
Get the legal ABIs supported by your platform and underlying implementation. What is supported can vary a lot by CPU and by platform, or even between 32 and 64 bit on the same CPU and platform. They keys are the "ABI" names, also known as "calling conventions". The values are integers used internally by the implementation to represent those ABIs.
$ffi->abi($name);
Set the ABI or calling convention for use in subsequent calls to "function" or "attach". May be either a string name or integer value from the "abis" method above.
EXAMPLES
Here are some examples. These examples are provided in full with the Platypus distribution in the "examples" directory. There are also some more examples in FFI::Platypus::Type that are related to types.
use FFI::Platypus 2.00;
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);
$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(atoi => ['string'] => 'int');
puts(atoi('56'));
Discussion: puts
and atoi
should be part of the standard C library on all platforms. puts
prints a string to standard output, and atoi
converts a string to integer. Specifying undef
as a library tells Platypus to search the current process for symbols, which includes the standard c library.
use FFI::CheckLib;
use FFI::Platypus 2.00;
# NOTE: I ported this from anoter Perl FFI library and it seems to work most
# of the time, but also seems to SIGSEGV sometimes. I saw the same behavior
# in the old version, and am not really familiar with the libnotify API to
# say what is the cause. Patches welcome to fix it.
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_exit lib => 'notify');
$ffi->attach(notify_init => ['string'] => 'void');
$ffi->attach(notify_uninit => [] => 'void');
$ffi->attach([notify_notification_new => 'notify_new'] => ['string', 'string', 'string'] => 'opaque');
$ffi->attach([notify_notification_update => 'notify_update'] => ['opaque', 'string', 'string', 'string'] => 'void');
$ffi->attach([notify_notification_show => 'notify_show'] => ['opaque', 'opaque'] => 'void');
notify_init('FFI::Platypus');
my $n = notify_new('','','');
notify_update($n, 'FFI::Platypus', 'It works!!!', 'media-playback-start');
notify_show($n, undef);
notify_uninit();
Discussion: libnotify is a desktop GUI notification library for the GNOME Desktop environment. This script sends a notification event that should show up as a balloon, for me it did so in the upper right hand corner of my screen.
The most portable way to find the correct name and location of a dynamic library is via the FFI::CheckLib#find_lib family of functions. If you are putting together a CPAN distribution, you should also consider using FFI::CheckLib#check_lib_or_exit function in your Build.PL
or Makefile.PL
file (If you are using Dist::Zilla, check out the Dist::Zilla::Plugin::FFI::CheckLib plugin). This will provide a user friendly diagnostic letting the user know that the required library is missing, and reduce the number of bogus CPAN testers results that you will get.
Also in this example, we rename some of the functions when they are placed into Perl space to save typing:
$ffi->attach( [notify_notification_new => 'notify_new']
=> ['string','string','string']
=> 'opaque'
);
When you specify a list reference as the "name" of the function the first element is the symbol name as understood by the dynamic library. The second element is the name as it will be placed in Perl space.
Later, when we call notify_new
:
my $n = notify_new('','','');
We are really calling the C function notify_notification_new
.
use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc free memcpy );
my $ffi = FFI::Platypus->new( api => 2 );
my $buffer = malloc 12;
memcpy $buffer, $ffi->cast('string' => 'opaque', "hello there"), length "hello there\0";
print $ffi->cast('opaque' => 'string', $buffer), "\n";
free $buffer;
Discussion: malloc
and free
are standard memory allocation functions available from the standard c library and. Interfaces to these and other memory related functions are provided by the FFI::Platypus::Memory module.
use FFI::Platypus 2.00;
use FFI::C;
my $ffi = FFI::Platypus->new(
api => 2,
lib => [undef],
);
FFI::C->ffi($ffi);
package Unix::TimeStruct {
FFI::C->struct(tm => [
tm_sec => 'int',
tm_min => 'int',
tm_hour => 'int',
tm_mday => 'int',
tm_mon => 'int',
tm_year => 'int',
tm_wday => 'int',
tm_yday => 'int',
tm_isdst => 'int',
tm_gmtoff => 'long',
_tm_zone => 'opaque',
]);
# For now 'string' is unsupported by FFI::C, but we
# can cast the time zone from an opaque pointer to
# string.
sub tm_zone {
my $self = shift;
$ffi->cast('opaque', 'string', $self->_tm_zone);
}
# attach the C localtime function
$ffi->attach( localtime => ['time_t*'] => 'tm', sub {
my($inner, $class, $time) = @_;
$time = time unless defined $time;
$inner->(\$time);
});
}
# now we can actually use our Unix::TimeStruct class
my $time = Unix::TimeStruct->localtime;
printf "time is %d:%d:%d %s\n",
$time->tm_hour,
$time->tm_min,
$time->tm_sec,
$time->tm_zone;
Discussion: C and other machine code languages frequently provide interfaces that include structured data records (known as "structs" in C). They sometimes provide an API in which you are expected to manipulate these records before and/or after passing them along to C functions. For C pointers to structs, unions and arrays of structs and unions, the easiest interface to use is via FFI::C. If you are working with structs that must be passed as values (not pointers), then you want to use the FFI::Platypus::Record class instead. We will discuss this class later.
The C localtime
function takes a pointer to a C struct. We simply define the members of the struct using the FFI::C struct
method. Because we used the ffi
method to tell FFI::C to use our local instance of FFI::Platypus it registers the tm
type for us, and we can just start using it as a return type!
use FFI::CheckLib;
use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc free );
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_exit lib => 'uuid');
$ffi->type('string(37)*' => 'uuid_string');
$ffi->type('record(16)*' => 'uuid_t');
$ffi->attach(uuid_generate => ['uuid_t'] => 'void');
$ffi->attach(uuid_unparse => ['uuid_t','uuid_string'] => 'void');
my $uuid = "\0" x $ffi->sizeof('uuid_t');
uuid_generate($uuid);
my $string = "\0" x $ffi->sizeof('uuid_string');
uuid_unparse($uuid, $string);
print "$string\n";
Discussion: libuuid is a library used to generate unique identifiers (UUID) for objects that may be accessible beyond the local system. The library is or was part of the Linux e2fsprogs package.
Knowing the size of objects is sometimes important. In this example, we use the sizeof function to get the size of 16 characters (in this case it is simply 16 bytes). We also know that the strings "deparsed" by uuid_unparse
are exactly 37 bytes.
use FFI::Platypus 2.00;
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);
$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(getpid => [] => 'int');
puts(getpid());
Discussion: puts
is part of standard C library on all platforms. getpid
is available on Unix type platforms.
use FFI::Platypus 2.00;
use FFI::CheckLib;
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);
$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(fdim => ['double','double'] => 'double');
puts(fdim(7.0, 2.0));
$ffi->attach(cos => ['double'] => 'double');
puts(cos(2.0));
$ffi->attach(fmax => ['double', 'double'] => 'double');
puts(fmax(2.0,3.0));
Discussion: On UNIX the standard c library math functions are frequently provided in a separate library libm
, so you could search for those symbols in "libm.so", but that won't work on non-UNIX platforms like Microsoft Windows. Fortunately Perl uses the math library so these symbols are already in the current process so you can use undef
as the library to find them.
use FFI::Platypus 2.00;
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef);
$ffi->attach(puts => ['string'] => 'int');
$ffi->attach(strlen => ['string'] => 'int');
puts(strlen('somestring'));
$ffi->attach(strstr => ['string','string'] => 'string');
puts(strstr('somestring', 'string'));
#attach puts => [string] => int;
puts(puts("lol"));
$ffi->attach(strerror => ['int'] => 'string');
puts(strerror(2));
Discussion: ASCII and UTF-8 Strings are not a native type to libffi
but the are handled seamlessly by Platypus. If you need to talk to an API that uses so called "wide" strings (APIs which use const wchar_t*
or wchar_t*
), then you will want to use the wide string type plugin FFI::Platypus::Type::WideString. APIs which use other arbitrary encodings can be accessed by converting your Perl strings manually with the Encode module.
use FFI::TinyCC;
use FFI::Platypus 2.00;
my $ffi = FFI::Platypus->new( api => 2 );
my $tcc = FFI::TinyCC->new;
$tcc->compile_string(q{
int
add(int a, int b)
{
return a+b;
}
});
my $address = $tcc->get_symbol('add');
$ffi->attach( [ $address => 'add' ] => ['int','int'] => 'int' );
print add(1,2), "\n";
Discussion: Sometimes you will have a pointer to a function from a source other than Platypus that you want to call. You can use that address instead of a function name for either of the function or attach methods. In this example we use FFI::TinyCC to compile a short piece of C code and to give us the address of one of its functions, which we then use to create a perl xsub to call it.
FFI::TinyCC embeds the Tiny C Compiler (tcc) to provide a just-in-time (JIT) compilation service for FFI.
use constant ZMQ_IO_THREADS => 1;
use constant ZMQ_MAX_SOCKETS => 2;
use constant ZMQ_REQ => 3;
use constant ZMQ_REP => 4;
use FFI::CheckLib qw( find_lib_or_exit );
use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc );
use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );
my $endpoint = "ipc://zmq-ffi-$$";
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(undef); # for puts
$ffi->attach(puts => ['string'] => 'int');
$ffi->lib(find_lib_or_exit lib => 'zmq');
$ffi->attach(zmq_version => ['int*', 'int*', 'int*'] => 'void');
my($major,$minor,$patch);
zmq_version(\$major, \$minor, \$patch);
puts("libzmq version $major.$minor.$patch");
die "this script only works with libzmq 3 or better" unless $major >= 3;
$ffi->type('opaque' => 'zmq_context');
$ffi->type('opaque' => 'zmq_socket');
$ffi->type('opaque' => 'zmq_msg_t');
$ffi->attach(zmq_ctx_new => [] => 'zmq_context');
$ffi->attach(zmq_ctx_set => ['zmq_context', 'int', 'int'] => 'int');
$ffi->attach(zmq_socket => ['zmq_context', 'int'] => 'zmq_socket');
$ffi->attach(zmq_connect => ['opaque', 'string'] => 'int');
$ffi->attach(zmq_bind => ['zmq_socket', 'string'] => 'int');
$ffi->attach(zmq_send => ['zmq_socket', 'opaque', 'size_t', 'int'] => 'int');
$ffi->attach(zmq_msg_init => ['zmq_msg_t'] => 'int');
$ffi->attach(zmq_msg_recv => ['zmq_msg_t', 'zmq_socket', 'int'] => 'int');
$ffi->attach(zmq_msg_data => ['zmq_msg_t'] => 'opaque');
$ffi->attach(zmq_errno => [] => 'int');
$ffi->attach(zmq_strerror => ['int'] => 'string');
my $context = zmq_ctx_new();
zmq_ctx_set($context, ZMQ_IO_THREADS, 1);
my $socket1 = zmq_socket($context, ZMQ_REQ);
zmq_connect($socket1, $endpoint);
my $socket2 = zmq_socket($context, ZMQ_REP);
zmq_bind($socket2, $endpoint);
do { # send
our $sent_message = "hello there";
my($pointer, $size) = scalar_to_buffer $sent_message;
my $r = zmq_send($socket1, $pointer, $size, 0);
die zmq_strerror(zmq_errno()) if $r == -1;
};
do { # recv
my $msg_ptr = malloc 100;
zmq_msg_init($msg_ptr);
my $size = zmq_msg_recv($msg_ptr, $socket2, 0);
die zmq_strerror(zmq_errno()) if $size == -1;
my $data_ptr = zmq_msg_data($msg_ptr);
my $recv_message = buffer_to_scalar $data_ptr, $size;
print "recv_message = $recv_message\n";
};
Discussion: ØMQ is a high-performance asynchronous messaging library. There are a few things to note here.
Firstly, sometimes there may be multiple versions of a library in the wild and you may need to verify that the library on a system meets your needs (alternatively you could support multiple versions and configure your bindings dynamically). Here we use zmq_version
to ask libzmq which version it is.
zmq_version
returns the version number via three integer pointer arguments, so we use the pointer to integer type: int *
. In order to pass pointer types, we pass a reference. In this case it is a reference to an undefined value, because zmq_version will write into the pointers the output values, but you can also pass in references to integers, floating point values and opaque pointer types. When the function returns the $major
variable (and the others) has been updated and we can use it to verify that it supports the API that we require.
Notice that we define three aliases for the opaque
type: zmq_context
, zmq_socket
and zmq_msg_t
. While this isn't strictly necessary, since Platypus and C treat all three of these types the same, it is useful form of documentation that helps describe the functionality of the interface.
Finally we attach the necessary functions, send and receive a message. If you are interested, there is a fully fleshed out ØMQ Perl interface implemented using FFI called ZMQ::FFI.
use FFI::Platypus 2.00;
use FFI::CheckLib qw( find_lib_or_exit );
# This example uses FreeBSD's libarchive to list the contents of any
# archive format that it suppors. We've also filled out a part of
# the ArchiveWrite class that could be used for writing archive formats
# supported by libarchive
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_exit lib => 'archive');
$ffi->type('object(Archive)' => 'archive_t');
$ffi->type('object(ArchiveRead)' => 'archive_read_t');
$ffi->type('object(ArchiveWrite)' => 'archive_write_t');
$ffi->type('object(ArchiveEntry)' => 'archive_entry_t');
package Archive;
# base class is "abstract" having no constructor or destructor
$ffi->mangler(sub {
my($name) = @_;
"archive_$name";
});
$ffi->attach( error_string => ['archive_t'] => 'string' );
package ArchiveRead;
our @ISA = qw( Archive );
$ffi->mangler(sub {
my($name) = @_;
"archive_read_$name";
});
$ffi->attach( new => ['string'] => 'archive_read_t' );
$ffi->attach( [ free => 'DESTROY' ] => ['archive_t'] => 'void' );
$ffi->attach( support_filter_all => ['archive_t'] => 'int' );
$ffi->attach( support_format_all => ['archive_t'] => 'int' );
$ffi->attach( open_filename => ['archive_t','string','size_t'] => 'int' );
$ffi->attach( next_header2 => ['archive_t', 'archive_entry_t' ] => 'int' );
$ffi->attach( data_skip => ['archive_t'] => 'int' );
# ... define additional read methods
package ArchiveWrite;
our @ISA = qw( Archive );
$ffi->mangler(sub {
my($name) = @_;
"archive_write_$name";
});
$ffi->attach( new => ['string'] => 'archive_write_t' );
$ffi->attach( [ free => 'DESTROY' ] => ['archive_write_t'] => 'void' );
# ... define additional write methods
package ArchiveEntry;
$ffi->mangler(sub {
my($name) = @_;
"archive_entry_$name";
});
$ffi->attach( new => ['string'] => 'archive_entry_t' );
$ffi->attach( [ free => 'DESTROY' ] => ['archive_entry_t'] => 'void' );
$ffi->attach( pathname => ['archive_entry_t'] => 'string' );
# ... define additional entry methods
package main;
use constant ARCHIVE_OK => 0;
# this is a Perl version of the C code here:
# https://github.com/libarchive/libarchive/wiki/Examples#List_contents_of_Archive_stored_in_File
my $archive_filename = shift @ARGV;
unless(defined $archive_filename)
{
print "usage: $0 archive.tar\n";
exit;
}
my $archive = ArchiveRead->new;
$archive->support_filter_all;
$archive->support_format_all;
my $r = $archive->open_filename($archive_filename, 1024);
die "error opening $archive_filename: ", $archive->error_string
unless $r == ARCHIVE_OK;
my $entry = ArchiveEntry->new;
while($archive->next_header2($entry) == ARCHIVE_OK)
{
print $entry->pathname, "\n";
$archive->data_skip;
}
Discussion: libarchive is the implementation of tar
for FreeBSD provided as a library and available on a number of platforms.
One interesting thing about libarchive is that it provides a kind of object oriented interface via opaque pointers. This example creates an abstract class Archive
, and concrete classes ArchiveWrite
, ArchiveRead
and ArchiveEntry
. The concrete classes can even be inherited from and extended just like any Perl classes because of the way the custom types are implemented. We use Platypus's object
type for this implementation, which is a wrapper around an opaque
(can also be an integer) type that is blessed into a particular class.
Another advanced feature of this example is that we define a mangler to modify the symbol resolution for each class. This means we can do this when we define a method for Archive:
$ffi->attach( support_filter_all => ['archive_t'] => 'int' );
Rather than this:
$ffi->attach(
[ archive_read_support_filter_all => 'support_read_filter_all' ] =>
['archive_t'] => 'int' );
);
use FFI::Platypus 2.00;
{
package FD;
use constant O_RDONLY => 0;
use constant O_WRONLY => 1;
use constant O_RDWR => 2;
use constant IN => bless \do { my $in=0 }, __PACKAGE__;
use constant OUT => bless \do { my $out=1 }, __PACKAGE__;
use constant ERR => bless \do { my $err=2 }, __PACKAGE__;
my $ffi = FFI::Platypus->new( api => 2, lib => [undef]);
$ffi->type('object(FD,int)' => 'fd');
$ffi->attach( [ 'open' => 'new' ] => [ 'string', 'int', 'mode_t' ] => 'fd' => sub {
my($xsub, $class, $fn, @rest) = @_;
my $fd = $xsub->($fn, @rest);
die "error opening $fn $!" if $$fd == -1;
$fd;
});
$ffi->attach( write => ['fd', 'string', 'size_t' ] => 'ssize_t' );
$ffi->attach( read => ['fd', 'string', 'size_t' ] => 'ssize_t' );
$ffi->attach( close => ['fd'] => 'int' );
}
my $fd = FD->new("$0", FD::O_RDONLY);
my $buffer = "\0" x 10;
while(my $br = $fd->read($buffer, 10))
{
FD::OUT->write($buffer, $br);
}
$fd->close;
Discussion: The Unix file system calls use an integer handle for each open file. We can use the same object
type that we used for libarchive above, except we let platypus know that the underlying type is int
instead of opaque
(the latter being the default for the object
type). Mainly just for demonstration since Perl has much better IO libraries, but now we have an OO interface to the Unix IO functions.
use FFI::Platypus 2.00;
use FFI::CheckLib qw( find_lib_or_die );
use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar );
use FFI::Platypus::Memory qw( malloc free );
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lib(find_lib_or_die lib => 'bz2');
$ffi->attach(
[ BZ2_bzBuffToBuffCompress => 'compress' ] => [
'opaque', # dest
'unsigned int *', # dest length
'opaque', # source
'unsigned int', # source length
'int', # blockSize100k
'int', # verbosity
'int', # workFactor
] => 'int',
sub {
my $sub = shift;
my($source,$source_length) = scalar_to_buffer $_[0];
my $dest_length = int(length($source)*1.01) + 1 + 600;
my $dest = malloc $dest_length;
my $r = $sub->($dest, \$dest_length, $source, $source_length, 9, 0, 30);
die "bzip2 error $r" unless $r == 0;
my $compressed = buffer_to_scalar($dest, $dest_length);
free $dest;
$compressed;
},
);
$ffi->attach(
[ BZ2_bzBuffToBuffDecompress => 'decompress' ] => [
'opaque', # dest
'unsigned int *', # dest length
'opaque', # source
'unsigned int', # source length
'int', # small
'int', # verbosity
] => 'int',
sub {
my $sub = shift;
my($source, $source_length) = scalar_to_buffer $_[0];
my $dest_length = $_[1];
my $dest = malloc $dest_length;
my $r = $sub->($dest, \$dest_length, $source, $source_length, 0, 0);
die "bzip2 error $r" unless $r == 0;
my $decompressed = buffer_to_scalar($dest, $dest_length);
free $dest;
$decompressed;
},
);
my $original = "hello compression world\n";
my $compressed = compress($original);
print decompress($compressed, length $original);
Discussion: bzip2 is a compression library. For simple one shot attempts at compression/decompression when you expect the original and the result to fit within memory it provides two convenience functions BZ2_bzBuffToBuffCompress
and BZ2_bzBuffToBuffDecompress
.
The first four arguments of both of these C functions are identical, and represent two buffers. One buffer is the source, the second is the destination. For the destination, the length is passed in as a pointer to an integer. On input this integer is the size of the destination buffer, and thus the maximum size of the compressed or decompressed data. When the function returns the actual size of compressed or compressed data is stored in this integer.
This is normal stuff for C, but in Perl our buffers are scalars and they already know how large they are. In this sort of situation, wrapping the C function in some Perl code can make your interface a little more Perl like. In order to do this, just provide a code reference as the last argument to the "attach" method. The first argument to this wrapper will be a code reference to the C function. The Perl arguments will come in after that. This allows you to modify / convert the arguments to conform to the C API. What ever value you return from the wrapper function will be returned back to the original caller.
use utf8;
use FFI::Platypus 2.00;
my $ffi = FFI::Platypus->new(
api => 2,
lib => [undef],
);
# see FFI::Platypus::Lang::Win32
$ffi->lang('Win32');
# Send a Unicode string to the Windows API MessageBoxW function.
use constant MB_OK => 0x00000000;
use constant MB_DEFAULT_DESKTOP_ONLY => 0x00020000;
$ffi->attach( [MessageBoxW => 'MessageBox'] => [ 'HWND', 'LPCWSTR', 'LPCWSTR', 'UINT'] => 'int' );
MessageBox(undef, "I ❤️ Platypus", "Confession", MB_OK|MB_DEFAULT_DESKTOP_ONLY);
Discussion: The API used by Microsoft Windows present some unique challenges. On 32 bit systems a different ABI is used than what is used by the standard C library. It also provides a rats nest of type aliases. Finally if you want to talk Unicode to any of the Windows API you will need to use UTF-16LE
instead of utf-8
which is native to Perl. (The Win32 API refers to these as LPWSTR
and LPCWSTR
types). As much as possible the Win32 "language" plugin attempts to handle this transparently. For more details see FFI::Platypus::Lang::Win32.
ffi/foo.c
:
#include <ffi_platypus_bundle.h>
#include <string.h>
typedef struct {
char *name;
int value;
} foo_t;
foo_t*
foo__new(const char *class_name, const char *name, int value)
{
(void)class_name;
foo_t *self = malloc( sizeof( foo_t ) );
self->name = strdup(name);
self->value = value;
return self;
}
const char *
foo__name(foo_t *self)
{
return self->name;
}
int
foo__value(foo_t *self)
{
return self->value;
}
void
foo__DESTROY(foo_t *self)
{
free(self->name);
free(self);
}
lib/Foo.pm
:
package Foo;
use strict;
use warnings;
use FFI::Platypus 2.00;
{
my $ffi = FFI::Platypus->new( api => 2 );
$ffi->type('object(Foo)' => 'foo_t');
$ffi->mangler(sub {
my $name = shift;
$name =~ s/^/foo__/;
$name;
});
$ffi->bundle;
$ffi->attach( new => [ 'string', 'string', 'int' ] => 'foo_t' );
$ffi->attach( name => [ 'foo_t' ] => 'string' );
$ffi->attach( value => [ 'foo_t' ] => 'int' );
$ffi->attach( DESTROY => [ 'foo_t' ] => 'void' );
}
1;
You can bundle your own C (or other compiled language) code with your Perl extension. Sometimes this is helpful for smoothing over the interface of a C library which is not very FFI friendly. Sometimes you may want to write some code in C for a tight loop. Either way, you can do this with the Platypus bundle interface. See FFI::Platypus::Bundle for more details.
Also related is the bundle constant interface, which allows you to define Perl constants in C space. See FFI::Platypus::Constant for details.
FAQ
This turns out to be a challenge for any language calling into C, which frequently uses #define
macros to define constants like so:
#define FOO_STATIC 1
#define FOO_DYNAMIC 2
#define FOO_OTHER 3
As macros are expanded and their definitions are thrown away by the C pre-processor there isn't any way to get the name/value mappings from the compiled dynamic library.
You can manually create equivalent constants in your Perl source:
use constant FOO_STATIC => 1;
use constant FOO_DYNAMIC => 2;
use constant FOO_OTHER => 3;
If there are a lot of these types of constants you might want to consider using a tool (Convert::Binary::C can do this) that can extract the constants for you.
See also the "Integer constants" example in FFI::Platypus::Type.
You can also use the new Platypus bundle interface to define Perl constants from C space. This is more reliable, but does require a compiler at install time. It is recommended mainly for writing bindings against libraries that have constants that can vary widely from platform to platform. See FFI::Platypus::Constant for details.
The C enum types are integers. The underlying type is up to the platform, so Platypus provides enum
and senum
types for unsigned and singed enums respectively. At least some compilers treat signed and unsigned enums as different types. The enum values are essentially the same as macro constants described above from an FFI perspective. Thus the process of defining enum values is identical to the process of defining macro constants in Perl.
For more details on enumerated types see "Enum types" in FFI::Platypus::Type.
There is also a type plugin (FFI::Platypus::Type::Enum) that can be helpful in writing interfaces that use enums.
There are a couple places where memory is allocated, but never deallocated that may look like memory leaks by tools designed to find memory leaks like valgrind. This memory is intended to be used for the lifetime of the perl process so there normally this isn't a problem unless you are embedding a Perl interpreter which doesn't closely match the lifetime of your overall application.
Specifically:
type cache
some types are cached and not freed. These are needed as long as there are FFI functions that could be called.
attached functions
Attaching a function as an xsub will definitely allocate memory that won't be freed because the xsub could be called at any time, including in END
blocks.
The Platypus team plans on adding a hook to free some of this "leaked" memory for use cases where Perl and Platypus are embedded in a larger application where the lifetime of the Perl process is significantly smaller than the overall lifetime of the whole process.
On some platforms, Perl isn't linked with libpthreads
if Perl threads are not enabled. On some platforms this doesn't seem to matter, libpthreads
can be loaded at runtime without much ill-effect. (Linux from my experience doesn't seem to mind one way or the other). Some platforms are not happy about this, and about the only thing that you can do about it is to build Perl such that it links with libpthreads
even if it isn't a threaded Perl.
This is not really an FFI issue, but a Perl issue, as you will have the same problem writing XS code for the such libraries.
I try as best as possible to support the same range of Perls as the Perl toolchain. That means all the way back to 5.8.1. Unfortunately, 5.10.0 seems to have a problem that is difficult to diagnose. Patches to fix are welcome, if you want to help out on this, please see:
https://github.com/PerlFFI/FFI-Platypus/issues/68
Since this is an older buggy version of Perl it is recommended that you instead upgrade to 5.10.1 or later.
CAVEATS
Platypus and Native Interfaces like libffi rely on the availability of dynamic libraries. Things not supported include:
Systems that lack dynamic library support
Like MS-DOS
Systems that are not supported by libffi
Like OpenVMS
Languages that do not support using dynamic libraries from other languages
This used to be the case with Google's Go, but is no longer the case. This is a problem for C / XS code as well.
Languages that do not compile to machine code
Like .NET based languages and Java.
The documentation has a bias toward using FFI / Platypus with C. This is my fault, as my background in mainly in C/C++ programmer (when I am not writing Perl). In many places I use "C" as a short form for "any language that can generate machine code and is callable from C". I welcome pull requests to the Platypus core to address this issue. In an attempt to ease usage of Platypus by non C programmers, I have written a number of foreign language plugins for various popular languages (see the SEE ALSO below). These plugins come with examples specific to those languages, and documentation on common issues related to using those languages with FFI. In most cases these are available for easy adoption for those with the know-how or the willingness to learn. If your language doesn't have a plugin YET, that is just because you haven't written it yet.
SUPPORT
IRC: #native on irc.perl.org
(click for instant chat room login)
If something does not work the way you think it should, or if you have a feature request, please open an issue on this project's GitHub Issue tracker:
https://github.com/perlFFI/FFI-Platypus/issues
CONTRIBUTING
If you have implemented a new feature or fixed a bug then you may make a pull request on this project's GitHub repository:
https://github.com/PerlFFI/FFI-Platypus/pulls
This project is developed using Dist::Zilla. The project's git repository also comes with the Makefile.PL
file necessary for building, testing (and even installing if necessary) without Dist::Zilla. Please keep in mind though that these files are generated so if changes need to be made to those files they should be done through the project's dist.ini
file. If you do use Dist::Zilla and already have the necessary plugins installed, then I encourage you to run dzil test
before making any pull requests. This is not a requirement, however, I am happy to integrate especially smaller patches that need tweaking to fit the project standards. I may push back and ask you to write a test case or alter the formatting of a patch depending on the amount of time I have and the amount of code that your patch touches.
This project's GitHub issue tracker listed above is not Write-Only. If you want to contribute then feel free to browse through the existing issues and see if there is something you feel you might be good at and take a whack at the problem. I frequently open issues myself that I hope will be accomplished by someone in the future but do not have time to immediately implement myself.
Another good area to help out in is documentation. I try to make sure that there is good document coverage, that is there should be documentation describing all the public features and warnings about common pitfalls, but an outsider's or alternate view point on such things would be welcome; if you see something confusing or lacks sufficient detail I encourage documentation only pull requests to improve things.
The Platypus distribution comes with a test library named libtest
that is normally automatically built by ./Build test
. If you prefer to use prove
or run tests directly, you can use the ./Build libtest
command to build it. Example:
% perl Makefile.PL
% make
% make ffi-test
% prove -bv t
# or an individual test
% perl -Mblib t/ffi_platypus_memory.t
The build process also respects these environment variables:
FFI_PLATYPUS_DEBUG_FAKE32
When building Platypus on 32 bit Perls, it will use the Math::Int64 C API and make Math::Int64 a prerequisite. Setting this environment variable will force Platypus to build with both of those options on a 64 bit Perl as well.
% env FFI_PLATYPUS_DEBUG_FAKE32=1 perl Makefile.PL
DEBUG_FAKE32:
+ making Math::Int64 a prereq
+ Using Math::Int64's C API to manipulate 64 bit values
Generating a Unix-style Makefile
Writing Makefile for FFI::Platypus
Writing MYMETA.yml and MYMETA.json
%
FFI_PLATYPUS_NO_ALLOCA
Platypus uses the non-standard and somewhat controversial C function alloca
by default on platforms that support it. I believe that Platypus uses it responsibly to allocate small amounts of memory for argument type parameters, and does not use it to allocate large structures like arrays or buffers. If you prefer not to use alloca
despite these precautions, then you can turn its use off by setting this environment variable when you run Makefile.PL
:
helix% env FFI_PLATYPUS_NO_ALLOCA=1 perl Makefile.PL
NO_ALLOCA:
+ alloca() will not be used, even if your platform supports it.
Generating a Unix-style Makefile
Writing Makefile for FFI::Platypus
Writing MYMETA.yml and MYMETA.json
V
When building platypus may hide some of the excessive output when probing and building, unless you set V
to a true value.
% env V=1 perl Makefile.PL
% make V=1
...
As Mark Twain was fond of saying there are four types of lies: lies, damn lies, statistics and benchmarks. That being said, it can sometimes be helpful to compare the runtime performance of Platypus if you are making significant changes to the Platypus Core. For that I use `FFI-Performance`, which can be found in my GitHub repository here:
This distribution uses Alien::FFI in fallback mode, meaning if the system doesn't provide pkg-config
and libffi
it will attempt to download libffi
and build it from source. If you are including Platypus in a larger system (for example a Linux distribution) you only need to make sure to declare pkg-config
or pkgconf
and the development package for libffi
as prereqs for this module.
SEE ALSO
Type definitions for Platypus.
Interface for defining structured data records for use with Platypus. It supports C struct
, union
, nested structures and arrays of all of those. It only supports passing these types by reference or pointer, so if you need to pass structured data by value see FFI::Platypus::Record below.
Interface for defining structured data records for use with Platypus. Included in the Platypus core. Supports pass by value which is uncommon in C, but frequently used in languages like Rust and Go. Consider using FFI::C instead if you don't need to pass by value.
The custom types API for Platypus.
Memory functions for FFI.
JIT C compiler for FFI.
Documentation and tools for using Platypus with the C programming language
Documentation and tools for using Platypus with the C++ programming language
Documentation and tools for using Platypus with Fortran
Documentation and tools for using Platypus with Go
Documentation and tools for using Platypus with Free Pascal
Documentation and tools for using Platypus with the Rust programming language
Documentation and tools for using Platypus with the Assembly
Documentation and tools for using Platypus with the Win32 API.
Wasm and Wasm::Wasmtime
Modules for writing WebAssembly bindings in Perl. This allows you to call functions written in any language supported by WebAssembly. These modules are also implemented using Platypus.
Find dynamic libraries in a portable way.
A great interface for decoding C data structures, including struct
s, enum
s, #define
s and more.
Native to Perl functions that can be used to decode C struct
types.
This module can extract constants and other useful objects from C header files that may be relevant to an FFI application. One downside is that its use may require development packages to be installed.
A wrapper around dyncall, which is itself an alternative to libffi.
Promising interface to Platypus inspired by Raku.
Microsoft Windows specific FFI style interface.
Older, simpler, less featureful FFI. It used to be implemented using FSF's ffcall
. Because ffcall
has been unsupported for some time, I reimplemented this module using FFI::Platypus.
Another FFI for Perl that doesn't appear to have worked for a long time.
Embed a tiny C compiler into your Perl scripts.
Yet another FFI like interface that does not appear to be supported or under development anymore.
Provides libffi for Platypus during its configuration and build stages.
ACKNOWLEDGMENTS
In addition to the contributors mentioned below, I would like to acknowledge Brock Wilcox (AWWAIID) and Meredith Howard (MHOWARD) whose work on FFI::Sweet
not only helped me get started with FFI but significantly influenced the design of Platypus.
Dan Book, who goes by Grinnz on IRC for answering user questions about FFI and Platypus.
In addition I'd like to thank Alessandro Ghedini (ALEXBIO) whose work on another Perl FFI library helped drive some of the development ideas for FFI::Platypus.
AUTHOR
Author: Graham Ollis plicease@cpan.org
Contributors:
Bakkiaraj Murugesan (bakkiaraj)
Dylan Cali (calid)
pipcet
Zaki Mughal (zmughal)
Fitz Elliott (felliott)
Vickenty Fesunov (vyf)
Gregor Herrmann (gregoa)
Shlomi Fish (shlomif)
Damyan Ivanov
Ilya Pavlov (Ilya33)
Petr Písař (ppisar)
Mohammad S Anwar (MANWAR)
Håkon Hægland (hakonhagland, HAKONH)
Meredith (merrilymeredith, MHOWARD)
Diab Jerius (DJERIUS)
Eric Brine (IKEGAMI)
szTheory
José Joaquín Atria (JJATRIA)
Pete Houston (openstrike, HOUSTON)
COPYRIGHT AND LICENSE
This software is copyright (c) 2015-2022 by Graham Ollis.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
Author: PerlFFI
Source Code: https://github.com/PerlFFI/FFI-Platypus
1661554740
Expose OneTrust's Native CMP platform to your Flutter project.
The SDK version used must match the version of the JSON published from your OneTrust instance. For example, if you’ve published version 6.10.0 of the JSON in your OneTrust environment, you must use the Flutter plugin version 6.10.0 as well. It is recommended to specify a version to avoid automatic updates, as a OneTrust publish is required when you update your SDK version.
To install the OneTrust plugin, run flutter pub add onetrust_publishers_native_cmp
Specify a version in your pubspec.yaml
file, for example:
dependencies:
onetrust_publishers_native_cmp: '6.10.0'
After adding the plugin to your Flutter project, build the project to check for any immediate errors.
This plugin pulls the OneTrust SDK from mavenCentral and Cocoapods.
OTPublishersNativeSDK supports a minimum iOS version of 11.0. You may have to specify a platform target in your podfile
:
platform :ios, '11.0'
In Android, the underlying Native SDK requires a FragmentActivity
in order to render a UI. In your Android MainActivity.java
file, ensure that the MainActivity
class is extending FlutterFragmentActivity
.
Additionally, Android must use targetSdkVersion 31
and compileSDKVersion 31
or higher.
public class MainActivity extends FlutterFragmentActivity {
}
Usage
Initialize OneTrust to fetch the data configured in your OneTrust tenant. This will make one or two network calls (depending on the type of template) and deliver a JSON object that contains all of the information required to render a banner and preference center.
The init call is an async function that returns a boolean status value; true
if the initialization and download of data was successful, false
if not.
Future<void> initOneTrust() async{
bool status;
Map<String, String> params = { //Params are not required
"countryCode":"US",
"regionCode":"CA"
}
try{
status = await OTPublishersNativeSDK.startSDK("cdn.cookielaw.org","dec6b152-4ad9-487b-8e5a-24a06298417f","en", params);
} on PlatformException{
print("Error communicating with platform-side code");
}
if (!mounted) return;
setState(() {
_cmpDownloadStatus = status ? 'Success!':'Error';
});
}
Name | Type | Description |
---|---|---|
storageLocation | String | [Required] The CDN location for the JSON that the SDK fetches. (Usually, but not always, cdn.cookielaw.org .) |
domainIdentifier | String | [Required] The Application guid (retrieved from OneTrust Admin console) |
languageCode | String | [Required] 2-digit ISO language code used to return content in a specific langauge. Note: Any language code format which is not listed in OneTrust environment will be considered as invalid input. Note: If the languageCode passed by your application is valid, but does not match with a language configuration for your template, then the SDK will return content in the default language configured in OneTrust environment (usually, but not always, English). |
params | Map<String, String> | Parameter map (see below for accepted values) |
All initialization parameters are expected to be of type String
, and all are optional.
Name | Type | Description |
---|---|---|
countryCode | String | 2-digit ISO country code that will override OneTrust's geolocation service for locating the user. Typically used for QA to test other regions, or if your application knows the user's location applicable for consent purposes. |
regionCode | String | 2-digit ISO region code that will overide OneTrust's geolocation service. |
androidUXParams | String | A stringified representation of the OTUXParams JSON object to override styling in-app. See "Android - Custom Styling with UXParams JSON" below. |
The plugin can load a Banner or a Preference center using the methods below:
OTPublishersNativeSDK.showBannerUI(); //load Banner
OTPublishersNativeSDK.showPreferenceCenterUI(); //load Preference Center
To determine if a banner should be shown (based on the template rendered and the status of the user's current consent,) use the shouldShowBanner()
method. This returns a bool
indicating whether or not a banner should be shown.
bool _shouldShowBanner;
try{
_shouldShowBanner = await OTPublishersNativeSDK.shouldShowBanner();
} on PlatformException{
print("Error communicating with platform-side code");
}
if(_shouldShowBanner){
OTPublishersNativeSDK.showBannerUI();
}
A permission prompt can be displayed for several reasons. Currently, two prompts are supported on Flutter: the App Tracking Transparency pre-prompt and the Age Gate prompt.
To surface these prompts, ensure that they have been configured in your OneTrust template. The prompts are surfaced by calling showConsentUI
and passing in an OTDevicePermission
enum. An integer is returned that indicates the result of the prompt.
//IDFA/ATT - iOS Only
int authStatus = await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.idfa);
//Use the following to get the status as an enum -- there are several outcomes for IDFA specifically
OTATTrackingAuthorizationStatus IDFAStatus = OTATTrackingAuthorizationStatus.values[authStatus];
//Age Gate - iOS and Android
int authStatus = await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.AgeGate);
//returns a 0 (user responded no) or a 1 (user responded yes)
This method will not resolve until the user has made a selection for the permission prompt..
This method takes an argument of type OTDevicePermission
, which is provided as an enum
for ease of use. It returns an OTATTrackingAuthorizationStatus
.
The current status of App Tracking Transparency can be obtained by calling
OTATTrackingAuthorizationStatus authStatus = await OTPublishersNativeSDK.getATTrackingAuthorizationStatus();
The above method returns an OTATTTrackingAuthorizationStatus
which is namespaced as such to avoid issues with other ATT plugins being used. For unsupported OS versions (all Android versions and iOS versions < 14,) this function will immediately return platformNotSupported
. Possible values of the enum are
notDetermined
restricted
denied
authorized
platformNotSupported
The current status of the Age Gate can be obtained by calling
int authStatus = await OTPublishersNativeSDK.getAgeGatePromptValue()
This method will return a 0 for a No response to the Age Gate and a 1 for a Yes response.
OneTrust allows you to add custom styling to your preference center by passing in style JSON in a certain format. Build out your JSON by following the guide in the OneTrust Developer Portal.
Pass the JSON as a string into the startSDK
function's params
argument.
String AndroidUXParams = await DefaultAssetBundle.of(context)
.loadString("assets/AndroidUXParams.json");
Map<String, String> params = {
"androidUXParams": AndroidUXParams
};
try {
status = await OTPublishersNativeSDK.startSDK(
"cdn.cookielaw.org", appId, "en", params);
} on PlatformException {
print("Error communicating with platform code");
}
Custom styling can be added to your iOS Cordova application by using a .plist file in the iOS platform code. In addition to adding the .plist file (which can be obtained from the OneTrust Demo Application) to your bundle, there are a few changes that need to be made in the platform code, outlined below. Review the guide in the OneTrust Developer Portal.
In AppDelegate.swift
, import OTPublishersHeadlessSDK. Add an extension below the class to handle the protocol methods:
import OTPublishersHeadlessSDK
...
extension AppDelegate: UIConfigurator{
func shouldUseCustomUIConfig() -> Bool {
return true
}
func customUIConfigFilePath() -> String? {
return Bundle.main.path(forResource: "OTSDK-UIConfig-iOS", ofType: "plist")
}
}
In the didFinishLaunchingWithOptions
protocol method, assign the AppDelegate as the UIConfigurator:
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
OTPublishersHeadlessSDK.shared.uiConfigurator = self //add this line only
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
The plugin implements an EventChannel
that opens a BroadcastStream
with the platform-side code. To listen for changes in the ui.
Note that:
BroadcastStream
will stay open until it is closed by calling .cancel()
on itBroadcastStream
per EventChannel
can be open at a single time. Therefore, you must call .cancel()
on this stream before calling .listenForUIInteractions().listen
again.var interactionListener = OTPublishersNativeSDK.listenForUIInteractions().listen((event) {
print(event);
});
Expected return:
{
'uiEvent':String,
'payload':Object
}
Event Name | Description | Payload |
---|---|---|
onShowBanner | Triggered when banner is shown | null |
onHideBanner | Triggered when banner is closed | null |
onBannerClickedAcceptAll | Triggered when user allows all consent from banner | null |
onBannerClickedRejectAll | Triggered when user rejects all consent from banner | null |
onShowPreferenceCenter | Triggered when Preference Center is displayed | null |
onHidePreferenceCenter | Triggered when Preference Center is closed | null |
onPreferenceCenterAcceptAll | Triggered when user allows all consent from Preference Center | null |
onPreferenceCenterRejectAll | Triggered when user rejects all consent from Preference Center | null |
onPreferenceCenterConfirmChoices | Triggered when user clicked on save choices after updating consent values from Preference Center | null |
onShowVendorList | Triggered when vendor list UI is displayed from an IAB banner/ IAB Preference center | null |
onHideVendorList | Triggered when vendor list UI is closed or when back button is clicked | null |
onVendorConfirmChoices | Triggered when user updates vendor consent / legitimate interests purpose values and save the choices from vendor list | null |
onVendorListVendorConsentChanged | Triggered when user updates consent values for a particular vendor id on vendor list UI | {vendorId:String, consentStatus:Int} |
onVendorListVendorLegitimateInterestChanged | Triggered when user updates Legitimate interests values for a particular vendor id on vendor list UI | {vendorId:String, legitInterest:Int} |
onPreferenceCenterPurposeConsentChanged | Triggered when user updates consent values for a particular category on Preference Center UI | {purposeId:String, consentStatus:Int} |
onPreferenceCenterPurposeLegitimateInterestChanged | Triggered when user updates Legitimate interest values for a particular category on Preference Center UI | {purposeId:String, legitInterest:Int} |
allSDKViewsDismissed | Triggered when all the OT SDK Views are dismissed from the view hierarchy. | {interactionType:String} |
The consent status is returned as an integer value: |Status|Description| |-|-| |1|Consent given| |0|Consent not given| |-1|Consent not gathered or SDK not initialized|
To get the present consent state for a category, you can query for consent:
Future<void> getConsentStatus() async{
int status;
try{
status = await OTPublishersNativeSDK.getConsentStatusForCategory("C0002");
} on PlatformException{
print("Error communicating with platform-side code.");
}
print("Queried Status for C0002 is = "+status.toString());
}
The plugin implements an EventChannel
that opens a BroadcastStream
with the platform-side code. To listen for changes to the consent state.
Note that:
BroadcastStream
will stay open until it is closed by calling .cancel()
on itBroadcastStream
per EventChannel
can be open at a single time. Therefore, you must call .cancel()
on this stream before calling .listenForConsentChanges().listen
again.var consentListener = OTPublishersNativeSDK.listenForConsentChanges(["C0002","C0003"]).listen((event) {
setCategoryState(event['categoryId'], event['consentStatus']);
print("New status for "+event['categoryId']+" is "+event['consentStatus'].toString());
});
//consentListener.cancel() //cancel listener
This listener accepts an array of strings containing Category IDs to listen for. The listener returns an object:
{
'categoryId':String
'consentStatus':int
}
OneTrust sets a GUID to identify the user for audit purposes if one is not specified. The identifier of the current suer can be used to look up the user's consent history in the OneTrust Consent module. To surface the identifier:
String id = await OTPublishersNativeSDK.getCurrentActiveProfile();
If you use WebViews in your applications, you may encounter a situation where you render a webpage running OneTrust’s Cookie Consent module, and the cookie banner is displayed. OneTrust provides a native way to take the consent gathered from the Native SDK and pass it down to the WebView.
To do this, simply call the function below to request a JavaScript variable from the OneTrust SDK. Placing this on the OneTrust SDK before the Cookies SDK loads will suppress the banner and pass consent to the WebView.
String jsToPass = await OTPublishersNativeSDK.getOTConsentJSForWebview();
The OneTrust SDK offers several methods that can be leveraged to build your own interface.
This method returns a map containing all the information about the domain (application) including rulesets and template configurations.
Map<String, dynamic> domainInfo = await OTPublishersNativeSDK.getDomainInfo();
This method returns a map which contains template based information like branding which includes keys for determining colors and styles in the UI specific to a template configured for the user's geolocation along with consent logging information.
Map<String, dynamic> commonData = await OTPublishersNativeSDK.getCommonData();
This method returns a dictionary containing the information about all the categories and sdk IDs specific to a template configured for the user's geolocation. This is generally used to populate the preference center's toggle section.
Map<String, dynamic> groups = await OTPublishersNativeSDK.getDomainGroupData();
This method returns a dictionary which contains all the keys required to render a banner.
Map<String, dynamic> bannerData = await OTPublishersNativeSDK.getBannerData();
This method is used to modify the consent status of a category by passing Custom Group ID as the key and consent status as value. Note that you have to call 'saveConsent(OTInteractionType)', with type being ConsentInteractionType, to have any modifications you perform to actually register in OneTrust SDK. OneTrust will notify the consent status changes to your app on saving changes.
//A user has just toggled on Category C0002
OTPublishersNativeSDK.updatePurposeConsent("C0002", true);
//After the user has made all of their selections and taps save, execute the saveConsent method below
Call this method to save the user's consent when they've indicated they wish for their consent to take effect. Examples include calling this method when a user accepts all on the banner, or after they've made all of their selections inside of a preference center.
//User confirms their choices in Pref Center
OTPublishersNativeSDK.saveConsent(OTInteractionType.preferenceCenterConfirm);
//User accepts all on the banner
OTPublishersNativeSDK.saveConsent(OTInteractionType.bannerAllowAll);
If configured, this will cause a record of consent to be sent. It will also signal the choices to the rest of your application via listeners.
The OTInteractionType
is a convenient way to keep track of interaction types. There are many, but the most commonly used are |Enum Name|Description| |-|-| |bannerAllowAll
|User has accepted all on the banner.| |bannerRejectAll
|User has rejected all on the banner.| |bannerContinueWithoutAccepting
|User has selected the "Continue Without Accepting" CNIL button on the banner.| |bannerClose
|User has closed the banner without making a selection.| |preferenceCenterAllowAll
|User has selected Allow All in the preference center.| |preferenceCenterRejectAll
|User has selected Reject All in the preference center.| |preferenceCenterConfirm
|User has made selections in the Preference Center and tapped Confirm.|
If a user has made selections in the preference center but then closes the preference center without saving, call this method to abandon the "staged" consent changes.
OTPublishersNativeSDK.resetUpdatedConsent();
Universal consent allows your application to collect consent for off-device processing. For example, your application might need to request permission to send a user promotional SMS messages. In OneTrust, after a Universal Consent transaction is committed, integration workflows can be triggered in the OneTrust tool.
Universal Consent requires a user identifier to work properly.
To load the Universal Consent preference center over the current screen, simply call
OTPublishersNativeSDK.showUCPurposesUI()
This plugin exposes async methods to retrieve the current state of the users' consent.
Status | Explanation |
---|---|
1 | Consent Given |
0 | Consent Not Given |
-1 | Consent not yet gathered, or SDK not initialized |
To query for the top-level purpose's consent:
int? consent = await OTPublishersNativeSDK.getUCPurposeConsent('purposeId')
Argument | Type | Description |
---|---|---|
purposeId | String | The GUID of the purpose to retrieve. |
To query for a custom preference nested under a purpose:
int? consent = await OTPublishersNativeSDK.getUCPurposeCustomPreferenceOptionConsent('customPreferenceOptionId',
'customPreferenceId','purposeId')
Argument | Type | Description |
---|---|---|
customPreferenceOptionId | String | The GUID of the custom preference option |
customPreferenceId | String | The GUID of the custom preference group |
purposeId | String | The GUID of the purpose under which the custom preference is nested. |
To query for a topic nested under a purpose:
int? consent = await OTPublishersNativeSDK.getUCPurposeTopicConsent('topicOptionId', 'purposeId')
Argument | Type | Description |
---|---|---|
topicOptionId | String | The GUID of the topic option |
purposeId | String | The GUID of the purpose under which the custom preference is nested. |
The package exposes methods to programatically set the users' consent values. A common use case is when a sign-up form has a checkbox at the bottom to allow the user to opt into emails. In this case, the application would set the value of the associated purpose, and the user could change his or her decision later in the preference center.
After making consent updates, the application must call the saveUCConsent()
function to commit the changes.
To update the top-level purpose's consent:
await OTPublishersNativeSDK.updateUCPurposeConsent('purposeId', true)
Argument | Type | Description |
---|---|---|
purposeId | String | The GUID of the purpose to update |
consent | Boolean | Whether or not consent has been granted for the specified item |
To update a custom preference nested under a purpose:
OTPublishersNativeSDK.updateUCPurposeCutomPreferenceOptionConsent('customPreferenceOptionId',
'customPreferenceId','purposeId', true)
Argument | Type | Description |
---|---|---|
topicId | String | The GUID of the custom preference option |
purposeId | String | The GUID of the custom preference group |
consent | Boolean | Whether or not consent has been granted for the specified item |
To update a topic nested under a purpose:
OTPublishersNativeSDK.updateUCPurposeTopicConsent('topicOptionId', 'purposeId', true)
After making updates to the consent values, the application must call the following to commit the changes:
OTPublishersNativeSDK.saveConsent(OTInteractionType.ucPreferenceCenterConfirm)
In rare cases, one of the dependencies required by OneTrust's underlying Native Android SDK may clash with dependencies in your application. Most of the time, Gradle will resolve these automatically. If that is not the case however, OneTrust provides the option to suppress all transitive dependencies from the OneTrust .aar
file.
To suppress all transitive dependencies, add onetrust.includeTransitiveDependencies=false
to your Android project's local.properties
file. If your application suppresses OneTrust's dependencies, the dependencies must be added to your project manually. A list of required dependencies is available at Developer.OneTrust.com.
Run this command:
With Flutter:
$ flutter pub add onetrust_publishers_native_cmp
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
onetrust_publishers_native_cmp: ^6.39.0
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:onetrust_publishers_native_cmp/onetrust_publishers_native_cmp.dart';
example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import 'package:onetrust_publishers_native_cmp/onetrust_publishers_native_cmp.dart';
import 'debugMenu.dart';
void main() {
runApp(const MaterialApp(title: "OneTrust Flutter Demo", home: HomePage()));
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _cmpDownloadStatus = 'Waiting';
int _c0003Status = -1;
int _c0004Status = -1;
OTATTrackingAuthorizationStatus _attStatus =
OTATTrackingAuthorizationStatus.notDetermined;
String _dataSubjectId = "Unknown";
@override
void initState() {
super.initState();
initOneTrust();
startListening();
}
Future<void> initOneTrust() async {
bool? status;
String appId;
bool? shouldShowBanner;
String? id;
OTATTrackingAuthorizationStatus startupATTStatus;
// Currently App ID is from qa of 6.39.0
// Change this while upgrading to verify if everything is working fine with upgrades.
if (Platform.isAndroid) {
appId = "d572dec8-4990-4452-bcdd-ad94a2f0d827";
} else if (Platform.isIOS) {
appId = "d572dec8-4990-4452-bcdd-ad94a2f0d827";
} else {
Exception("Platform not found!");
return;
}
String AndroidUXParams = await DefaultAssetBundle.of(context)
.loadString("assets/AndroidUXParams.json");
Map<String, String> params = {
"countryCode": "US",
"regionCode": "GA"
//"androidUXParams": AndroidUXParams
};
try {
status = await OTPublishersNativeSDK.startSDK("qa", appId, "en", params);
shouldShowBanner = await OTPublishersNativeSDK.shouldShowBanner();
id = await OTPublishersNativeSDK.getCurrentActiveProfile();
} on PlatformException {
print("Error communicating with platform code");
}
startupATTStatus =
await OTPublishersNativeSDK.getATTrackingAuthorizationStatus();
if (status! && shouldShowBanner!) {
OTPublishersNativeSDK.showBannerUI();
}
if (!mounted) return;
setState(() {
_cmpDownloadStatus = status! ? 'Success!' : 'Error';
_dataSubjectId = id!;
_attStatus = startupATTStatus;
});
}
void startListening() {
var consentListener =
OTPublishersNativeSDK.listenForConsentChanges(["C0003", "C0004"])
.listen((event) {
setCategoryState(event['categoryId'], event['consentStatus']);
print("New status for " +
event['categoryId'] +
" is " +
event['consentStatus'].toString());
});
var interactionListener =
OTPublishersNativeSDK.listenForUIInteractions().listen((event) {
print(event);
});
//consentListener.cancel(); //Cancel event stream before opening a new one
}
void setCategoryState(String category, int status) {
setState(() {
switch (category) {
case "C0003":
_c0003Status = status;
break;
case "C0004":
_c0004Status = status;
break;
default:
break;
}
});
}
void loadATTPrompt() async {
int? status;
status = await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.idfa);
if (status != null) {
setState(() {
_attStatus = OTATTrackingAuthorizationStatus.values[status!];
});
}
}
void loadAgeGatePrompt() async {
int? status =
await OTPublishersNativeSDK.showConsentUI(OTDevicePermission.ageGate);
print("Age Gate Status is $status");
}
Column getATTColumn() {
return Column(
children: [
ElevatedButton(
onPressed: () {
loadATTPrompt();
},
child: Text("Load ATT Prompt")),
Text("ATT Status = $_attStatus\n")
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Center(
child: Column(
children: [
Text('OneTrust Download Status: $_cmpDownloadStatus\n'),
ElevatedButton(
onPressed: () {
OTPublishersNativeSDK.showBannerUI();
},
child: Text("Load Banner")),
ElevatedButton(
onPressed: () {
OTPublishersNativeSDK.showPreferenceCenterUI();
},
child: Text("Load Preference Center")),
ElevatedButton(
onPressed: () {
OTPublishersNativeSDK.showUCPurposesUI();
},
child: Text("Load UC Purposes")),
ElevatedButton(
onPressed: () {
loadAgeGatePrompt();
},
child: Text("Load Age Gate")),
Platform.isIOS //conditionally render ATT Pre-prompt button
? getATTColumn()
: Container(),
Text('Category C0003 Status: $_c0003Status\n'),
Text('Category C0004 Status: $_c0004Status\n'),
Text('Data Subject Identifier is'),
Text(_dataSubjectId, style: TextStyle(fontWeight: FontWeight.bold)),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const EvenMore()),
);
},
child: Text("More")),
],
),
),
);
}
}
Original article source at: https://pub.dev/packages/onetrust_publishers_nativenative_cmp
1659664140
You don't (may not) need Lodash/Underscore
Lodash and Underscore are great modern JavaScript utility libraries, and they are widely used by Front-end developers. However, when you are targeting modern browsers, you may find out that there are many methods which are already supported natively thanks to ECMAScript5 [ES5] and ECMAScript2015 [ES6]. If you want your project to require fewer dependencies, and you know your target browser clearly, then you may not need Lodash/Underscore.
You are welcome to contribute with more items provided below.
If you are targeting legacy JavaScript engine with those ES5 methods, you can use es5-shim
Please note that, the examples used below are just showing you the native alternative of performing certain tasks. For some functions, Lodash provides you more options than native built-ins. This list is not a 1:1 comparison.
Please send a PR if you want to add or modify the code. No need to open an issue unless it's something big and you want to discuss.
Make use of native JavaScript object and array utilities before going big.
—Cody Lindley, Author of jQuery Cookbook and JavaScript Enlightenment
You probably don't need Lodash. Nice List of JavaScript methods which you can use natively.
—Daniel Lamb, Computer Scientist, Technical Reviewer of Secrets of the JavaScript Ninja and Functional Programming in JavaScript
—Tero Parviainen, Author of build-your-own-angular
I'll admit, I've been guilty of overusing #lodash. Excellent resource.
—@therebelrobot, Maker of web things, Facilitator for Node.js/io.js
If you're using ESLint, you can install a plugin that will help you identify places in your codebase where you don't (may not) need Lodash/Underscore.
Install the plugin...
npm install --save-dev eslint-plugin-you-dont-need-lodash-underscore
...then update your config
"extends" : ["plugin:you-dont-need-lodash-underscore/compatible"],
For more information, see Configuring the ESLint Plugin
❗Important: Note that, while many Lodash methods are null safe (e.g. _.keys, _.entries), this is not necessarily the case for their Native equivalent. If null safety is critical for your application, we suggest that you take extra precautions [e.g. consider using the native Object.keys as Object.keys(value || {})].
❗Important: Note that most native equivalents are array methods, and will not work with objects. If this functionality is needed and no object method is provided, then Lodash/Underscore might be the better option. Bear in mind however, that all iterable objects can easily be converted to an array by use of the spread operator.
Creates an array of elements split into groups the length of size.
// Underscore/Lodash
_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]
// Native
const chunk = (input, size) => {
return input.reduce((arr, item, idx) => {
return idx % size === 0
? [...arr, [item]]
: [...arr.slice(0, -1), [...arr.slice(-1)[0], item]];
}, []);
};
chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | 12.0 ✔ | 16.0 ✔ | ✖ | 37.0 ✔ | 8.0 ✔ |
Creates an array with all falsy values removed.
// Underscore/Lodash
_.compact([0, 1, false, 2, '', 3]);
// Native
[0, 1, false, 2, '', 3].filter(Boolean)
array.prototype.filter
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Creates a new array concatenating array with any additional arrays and/or values.
// Underscore/Lodash
var array = [1]
var other = _.concat(array, 2, [3], [[4]])
console.log(other)
// output: [1, 2, 3, [4]]
// Native
var array = [1]
var other = array.concat(2, [3], [[4]])
console.log(other)
// output: [1, 2, 3, [4]]
Array.prototype.concat()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | 5.5 ✔ | ✔ | ✔ |
Similar to without, but returns the values from array that are not present in the other arrays.
// Underscore/Lodash
console.log(_.difference([1, 2, 3, 4, 5], [5, 2, 10]))
// output: [1, 3, 4]
// Native
var arrays = [[1, 2, 3, 4, 5], [5, 2, 10]];
console.log(arrays.reduce(function(a, b) {
return a.filter(function(value) {
return !b.includes(value);
});
}));
// output: [1, 3, 4]
// ES6
let arrays = [[1, 2, 3, 4, 5], [5, 2, 10]];
console.log(arrays.reduce((a, b) => a.filter(c => !b.includes(c))));
// output: [1, 3, 4]
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Creates a slice of array with n elements dropped from the beginning.
// Underscore/Lodash
_.drop([1, 2, 3]);
// => [2, 3]
_.drop([1, 2, 3], 2);
// => [3]
// Native
[1, 2, 3].slice(1);
// => [2, 3]
[1, 2, 3].slice(2);
// => [3]
Array.prototype.slice()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Creates a slice of array with n elements dropped at the end.
// Underscore/Lodash
_.dropRight([1, 2, 3]);
// => [1, 2]
_.dropRight([1, 2, 3], 2);
// => [1]
// Native
[1, 2, 3].slice(0, -1);
// => [1, 2]
[1, 2, 3].slice(0, -2);
// => [1]
Array.prototype.slice()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Fills elements of array with value from start up to, but not including, end. Note that fill
is a mutable method in both native and Lodash/Underscore.
// Underscore/Lodash
var array = [1, 2, 3]
_.fill(array, 'a')
console.log(array)
// output: ['a', 'a', 'a']
_.fill(Array(3), 2)
// output: [2, 2, 2]
_.fill([4, 6, 8, 10], '*', 1, 3)
// output: [4, '*', '*', 10]
// Native
var array = [1, 2, 3]
array.fill('a')
console.log(array)
// output: ['a', 'a', 'a']
Array(3).fill(2)
// output: [2, 2, 2]
[4, 6, 8, 10].fill('*', 1, 3)
// output: [4, '*', '*', 10]
Array.prototype.fill()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 31.0 ✔ | ✖ | 32.0 ✔ | 8 ✔ |
Returns the value of the first element in the array that satisfies the provided testing function. Otherwise undefined is returned.
// Underscore/Lodash
var users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false },
{ 'user': 'pebbles', 'age': 1, 'active': true }
]
_.find(users, function (o) { return o.age < 40; })
// output: object for 'barney'
// Native
var users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false },
{ 'user': 'pebbles', 'age': 1, 'active': true }
]
users.find(function (o) { return o.age < 40; })
// output: object for 'barney'
Array.prototype.find()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 25.0 ✔ | ✖ | 32.0 ✔ | 7.1 ✔ |
Returns the index of the first element in the array that satisfies the provided testing function. Otherwise -1 is returned.
// Underscore/Lodash
var users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false },
{ 'user': 'pebbles', 'age': 1, 'active': true }
]
var index = _.findIndex(users, function (o) { return o.age >= 40; })
console.log(index)
// output: 1
// Native
var users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false },
{ 'user': 'pebbles', 'age': 1, 'active': true }
]
var index = users.findIndex(function (o) { return o.age >= 40; })
console.log(index)
// output: 1
Array.prototype.findIndex()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 25.0 ✔ | ✖ | 32.0 ✔ | 7.1 ✔ |
Returns the first element of an array. Passing n will return the first n elements of the array.
// Underscore/Lodash
_.first([1, 2, 3, 4, 5]);
// => 1
// Underscore
_.first([1, 2, 3, 4, 5], 2);
// => [1, 2]
// Native
[1, 2, 3, 4, 5][0];
// => 1
//or
[].concat(1, 2, 3, 4, 5).shift()
// => 1
//or
[].concat([1, 2, 3, 4, 5]).shift()
// => 1
// Native (works even with potentially undefined/null, like _.first)
[].concat(undefined).shift()
// => undefined
[1, 2, 3, 4, 5].slice(0, 2);
// => [1, 2]
Array.prototype.slice()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Flattens array a single level deep.
// Underscore/Lodash
_.flatten([1, [2, [3, [4]], 5]]);
// => [1, 2, [3, [4]], 5]
// Native
const flatten = [1, [2, [3, [4]], 5]].reduce( (a, b) => a.concat(b), [])
// => [1, 2, [3, [4]], 5]
const flatten = [].concat(...[1, [2, [3, [4]], 5]])
// => [1, 2, [3, [4]], 5]
// Native(ES2019)
const flatten = [1, [2, [3, [4]], 5]].flat()
// => [1, 2, [3, [4]], 5]
const flatten = [1, [2, [3, [4]], 5]].flatMap(number => number)
// => [1, 2, [3, [4]], 5]
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4 ✔ |
Array.prototype.flat()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
69 ✔ | ✖ | 62 ✔ | ✖ | 56 ✔ | 12 ✔ |
Array.prototype.flatMap()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
69 ✔ | ✖ | 62 ✔ | ✖ | 56 ✔ | 12 ✔ |
Recursively flattens array.
// Underscore/Lodash
_.flattenDeep([1, [2, [3, [4]], 5]]);
// => [1, 2, 3, 4, 5]
// Native
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => a.concat(flattenDeep(b)) , [])
: [arr]
flattenDeep([1, [[2], [3, [4]], 5]])
// => [1, 2, 3, 4, 5]
// Native(ES2019)
[1, [2, [3, [4]], 5]].flat(Infinity)
// => [1, 2, 3, 4, 5]
const flattenDeep = (arr) => arr.flatMap((subArray, index) => Array.isArray(subArray) ? flattenDeep(subArray) : subArray)
flattenDeep([1, [[2], [3, [4]], 5]])
// => [1, 2, 3, 4, 5]
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | ✔ | 16.0 ✔ | ✖ | 37.0 ✔ | 7.1 ✔ |
Array.prototype.flat()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
69 ✔ | ✖ | 62 ✔ | ✖ | 56 ✔ | 12 ✔ |
Array.prototype.flatMap()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
69 ✔ | ✖ | 62 ✔ | ✖ | 56 ✔ | 12 ✔ |
Returns an object composed from key-value pairs.
// Underscore/Lodash
_.fromPairs([['a', 1], ['b', 2]]);
// => { 'a': 1, 'b': 2 }
// Native
const fromPairs = function(arr) {
return arr.reduce(function(accumulator, value) {
accumulator[value[0]] = value[1];
return accumulator;
}, {})
}
// Compact form
const fromPairs = (arr) => arr.reduce((acc, val) => (acc[val[0]] = val[1], acc), {})
fromPairs([['a', 1], ['b', 2]]);
// => { 'a': 1, 'b': 2 }
// Native(ES2019)
Object.fromEntries([['a', 1], ['b', 2]])
// => { 'a': 1, 'b': 2 }
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Object.fromEntries()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
73.0 ✔ | 79.0 ✔ | 63.0 ✔ | ✖ | 60 ✔ | 12.1 ✔ |
Gets the first element or all but the first element.
const array = [1, 2, 3]
// Underscore: _.first, _.head, _.take
// Lodash: _.first, _.head
_.head(array)
// output: 1
// Underscore: _.rest, _.tail, _.drop
// Lodash: _.tail
_.tail(array)
// output: [2, 3]
// Native
const [ head, ...tail ] = array
console.log(head)
// output: 1
console.log(tail)
// output [2, 3]
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | 12.0 ✔ | 16.0 ✔ | ✖ | 37.0 ✔ | 8.0 ✔ |
Returns the first index at which a given element can be found in the array, or -1 if it is not present.
// Underscore/Lodash
var array = [2, 9, 9]
var result = _.indexOf(array, 2)
console.log(result)
// output: 0
// Native
var array = [2, 9, 9]
var result = array.indexOf(2)
console.log(result)
// output: 0
Array.prototype.indexOf()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Returns an array that is the intersection of all the arrays. Each value in the result is present in each of the arrays.
// Underscore/Lodash
console.log(_.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]))
// output: [1, 2]
// Native
var arrays = [[1, 2, 3], [101, 2, 1, 10], [2, 1]];
console.log(arrays.reduce(function(a, b) {
return a.filter(function(value) {
return b.includes(value);
});
}));
// output: [1, 2]
// ES6
let arrays = [[1, 2, 3], [101, 2, 1, 10], [2, 1]];
console.log(arrays.reduce((a, b) => a.filter(c => b.includes(c))));
// output: [1, 2]
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Creates a slice of array with n elements taken from the end. :heavy_exclamation_mark: Native slice does not behave entirely the same as the Lodash
method. See example below to understand why.
// Underscore/Lodash
_.takeRight([1, 2, 3]);
// => [3]
_.takeRight([1, 2, 3], 2);
// => [2, 3]
_.takeRight([1, 2, 3], 5);
// => [1, 2, 3]
// Native
[1, 2, 3].slice(-1);
// => [3]
[1, 2, 3].slice(-2);
// => [2, 3]
[1, 2, 3].slice(-5);
// => [1, 2, 3]
// Difference in compatibility
// Lodash
_.takeRight([1, 2, 3], 0);
// => []
// Native
[1, 2, 3].slice(0);
// => [1, 2, 3]
Array.prototype.slice()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Returns true if given value is an array.
// Lodash
var array = []
console.log(_.isArray(array))
// output: true
// Native
var array = []
console.log(Array.isArray(array));
// output: true
Array.isArray()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 10.5 ✔ | 5.0 ✔ |
Checks if value is classified as an ArrayBuffer object.
// Lodash
_.isArrayBuffer(new ArrayBuffer(2));
// output: true
// Native
console.log(new ArrayBuffer(2) instanceof ArrayBuffer);
// output: true
:warning: You will get the wrong result if you get ArrayBuffer
from another realm. See details.
instanceof
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Joins a list of elements in an array with a given separator.
// Lodash
var result = _.join(['one', 'two', 'three'], '--')
console.log(result)
// output: 'one--two--three'
// Native
var result = ['one', 'two', 'three'].join('--')
console.log(result)
// output: 'one--two--three'
Array.prototype.join()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | 5.5 ✔ | ✔ | ✔ |
Returns the last element of an array. Passing n will return the last n elements of the array.
// Underscore/Lodash
const numbers = [1, 2, 3, 4, 5];
_.last(numbers);
// => 5
// Underscore
_.last(numbers, 2);
// => [4, 5]
// Native
const numbers = [1, 2, 3, 4, 5];
numbers[numbers.length - 1];
// => 5
//or
numbers.slice(-1)[0];
// => 5
//or
[].concat(numbers).pop()
// => 5
// Native (works even with potentially undefined/null)
[].concat(undefined).pop()
// => undefined
numbers.slice(-2);
// => [4, 5]
Array.prototype.concat()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | 5.5 ✔ | ✔ | ✔ |
Returns the index of the last occurrence of value in the array, or -1 if value is not present.
// Underscore/Lodash
var array = [2, 9, 9, 4, 3, 6]
var result = _.lastIndexOf(array, 9)
console.log(result)
// output: 2
// Native
var array = [2, 9, 9, 4, 3, 6]
var result = array.lastIndexOf(9)
console.log(result)
// output: 2
Array.prototype.lastIndexOf()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Reverses array so that the first element becomes the last, the second element becomes the second to last, and so on.
// Lodash
var array = [1, 2, 3]
console.log(_.reverse(array))
// output: [3, 2, 1]
// Native
var array = [1, 2, 3]
console.log(array.reverse())
// output: [3, 2, 1]
Voice from the Lodash author:
Lodash's
_.reverse
just callsArray#reverse
and enables composition like_.map(arrays, _.reverse).
It's exposed on _ because previously, likeUnderscore
, it was only exposed in the chaining syntax. --- jdalton
Array.prototype.reverse()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9 ✔ | ✔ | ✔ |
Returns a shallow copy of a portion of an array into a new array object selected from begin
to end
(end
not included)
// Lodash
var array = [1, 2, 3, 4]
console.log(_.slice(array, 1, 3))
// output: [2, 3]
// Native
var array = [1, 2, 3, 4]
console.log(array.slice(1, 3));
// output: [2, 3]
Array.prototype.slice()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Returns an array where matching items are filtered.
// Lodash
var array = [1, 2, 3]
console.log(_.without(array, 2))
// output: [1, 3]
// Native
var array = [1, 2, 3]
console.log(array.filter(function(value) {
return value !== 2;
}));
// output: [1, 3]
Array.prototype.filter()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.5 ✔ | 9 ✔ | ✔ | ✔ |
Returns everything but the last entry of the array. Pass n to exclude the last n elements from the result.
// Underscore
var array = [5, 4, 3, 2, 1]
console.log(_.initial(array, 2))
// output: [5, 4, 3]
// Native
var array = [5, 4, 3, 2, 1]
console.log(array.slice(0, -2));
// output: [5, 4, 3]
Array.prototype.slice()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Removes all provided values from the given array using strict equality for comparisons, i.e. ===.
// Lodash
const array = [1, 2, 3, 1, 2, 3];
_.pull(array, 2, 3);
console.log(array); // output: [1, 1]
// Native JS
const array = [1, 2, 3, 1, 2, 3];
function pull(arr, ...removeList){
var removeSet = new Set(removeList)
return arr.filter(function(el){
return !removeSet.has(el)
})
}
console.log(pull(array, 2, 3)); // output: [1, 1]
console.log(array); // still [1, 2, 3, 1, 2, 3]. This is not in place, unlike lodash!
// TypeScript
const array = [1, 2, 3, 1, 2, 3];
const pull = <T extends unknown>(sourceArray: T[], ...removeList: T[]): T[] => {
const removeSet = new Set(removeList);
return sourceArray.filter(el => !removeSet.has(el));
};
console.log(pull(array, 2, 3)); // output: [1, 1]
console.log(array); // still [1, 2, 3, 1, 2, 3]. This is not in place, unlike lodash!
Array.prototype.filter()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.5 ✔ | 9 ✔ | ✔ | ✔ |
Set.prototype.has()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
38 ✔ | 12 ✔ | 13 ✔ | 11 ✔ | 25 ✔ | 8 ✔ |
Creates an array of unique values, taking an iteratee
to compute uniqueness with (note that to iterate by a key in an object you must use x => x.key
instead of key
for the iteratee
)
// Lodash
var array1 = [2.1];
var array2 = [1.2, 2.3];
var result = _.unionBy(array1, array2, Math.floor)
console.log(result)
// output: [2.1, 1.2]
// Native
var array1 = [2.1];
var array2 = [1.2, 2.3];
function unionBy(...arrays) {
const iteratee = (arrays).pop();
if (Array.isArray(iteratee)) {
return []; // return empty if iteratee is missing
}
return [...arrays].flat().filter(
(set => (o) => set.has(iteratee(o)) ? false : set.add(iteratee(o)))(new Set()),
);
};
console.log(unionBy(array1, array2, Math.floor))
// output: [2.1, 1.2]
Array.prototype.flat()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
69 ✔ | ✖ | 62 ✔ | ✖ | 56 ✔ | 12 ✔ |
Array.isArray()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 10.5 ✔ | 5.0 ✔ |
Set.prototype.has()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
38 ✔ | 12 ✔ | 13 ✔ | 11 ✔ | 25 ✔ | 8 ✔ |
:heavy_exclamation_mark:Important: Note that most native equivalents are array methods, and will not work with objects. If this functionality is needed and no object method is provided, then Lodash/Underscore is the better option.
Iterates over a list of elements, yielding each in turn to an iteratee function.
// Underscore/Lodash
//For arrays
_.each([1, 2, 3], function (value, index) {
console.log(value)
})
// output: 1 2 3
//For objects
_.each({'one':1, 'two':2, 'three':3}, function(value) {
console.log(value)
})
// output: 1 2 3
// Native
//For arrays
[1, 2, 3].forEach(function (value, index) {
console.log(value)
})
// output: 1 2 3
//For objects
Object.entries({'one':1, 'two':2, 'three':3}).forEach(function([key,value],index) {
console.log(value)
})
//output: 1 2 3
Array.prototype.forEach()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Object.entries().forEach()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
54 ✔ | 14 ✔ | 47 ✔ | ✖ | 41 ✔ | 10.1✔ |
Tests whether all elements in the array pass the test implemented by the provided function.
// Underscore/Lodash
function isLargerThanTen (element, index, array) {
return element >= 10
}
var array = [10, 20, 30]
var result = _.every(array, isLargerThanTen)
console.log(result)
// output: true
// Native
function isLargerThanTen (element, index, array) {
return element >= 10
}
var array = [10, 20, 30]
var result = array.every(isLargerThanTen)
console.log(result)
// output: true
Array.prototype.every()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Creates a new array with all elements that pass the test implemented by the provided function.
// Underscore/Lodash
function isBigEnough (value) {
return value >= 10
}
var array = [12, 5, 8, 130, 44]
var filtered = _.filter(array, isBigEnough)
console.log(filtered)
// output: [12, 130, 44]
// Native
function isBigEnough (value) {
return value >= 10
}
var array = [12, 5, 8, 130, 44]
var filtered = array.filter(isBigEnough)
console.log(filtered)
// output: [12, 130, 44]
Array.prototype.filter()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9 ✔ | ✔ | ✔ |
Group items by key.
// Underscore/Lodash
var grouped = _.groupBy(['one', 'two', 'three'], 'length')
console.log(grouped)
// output: {3: ["one", "two"], 5: ["three"]}
// Native
var grouped = ['one', 'two', 'three'].reduce((r, v, i, a, k = v.length) => ((r[k] || (r[k] = [])).push(v), r), {})
console.log(grouped)
// output: {3: ["one", "two"], 5: ["three"]}
// Underscore/Lodash
var grouped = _.groupBy([1.3, 2.1, 2.4], num => Math.floor(num))
console.log(grouped)
// output: {1: [1.3], 2: [2.1, 2.4]}
// Native
var grouped = [1.3, 2.1, 2.4].reduce((r, v, i, a, k = Math.floor(v)) => ((r[k] || (r[k] = [])).push(v), r), {})
console.log(grouped)
// output: {1: [1.3], 2: [2.1, 2.4]}
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Checks if a value is in collection.
var array = [1, 2, 3]
// Underscore/Lodash - also called _.contains
_.includes(array, 1)
// output: true
// Native
var array = [1, 2, 3]
array.includes(1)
// output: true
// Native (does not use same value zero)
var array = [1, 2, 3]
array.indexOf(1) > -1
// output: true
Array.prototype.includes
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
47.0 ✔ | 14.0 ✔ | 43.0 ✔ | ✖ | 34.0 ✔ | 9.0 ✔ |
Array.prototype.indexOf
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
:heavy_exclamation_mark: Not in Underscore.js
Creates an object composed of keys generated from the results of running each element of collection through iteratee.
// Lodash
console.log(_.keyBy(['a', 'b', 'c']))
// output: { a: 'a', b: 'b', c: 'c' }
console.log(_.keyBy([{ id: 'a1', title: 'abc' }, { id: 'b2', title: 'def' }], 'id'))
// output: { a1: { id: 'a1', title: 'abc' }, b2: { id: 'b2', title: 'def' } }
console.log(_.keyBy({ data: { id: 'a1', title: 'abc' }}, 'id'))
// output: { a1: { id: 'a1', title: 'abc' }}
// keyBy for array only
const keyBy = (array, key) => (array || []).reduce((r, x) => ({ ...r, [key ? x[key] : x]: x }), {});
// Native
console.log(keyBy(['a', 'b', 'c']))
// output: { a: 'a', b: 'b', c: 'c' }
console.log(keyBy([{ id: 'a1', title: 'abc' }, { id: 'b2', title: 'def' }], 'id'))
// output: { a1: { id: 'a1', title: 'abc' }, b2: { id: 'b2', title: 'def' } }
console.log(keyBy(Object.values({ data: { id: 'a1', title: 'abc' }}), 'id'))
// output: { a1: { id: 'a1', title: 'abc' }}
// keyBy for array and object
const collectionKeyBy = (collection, key) => {
const c = collection || {};
return c.isArray() ? keyBy(c, key) : Object.values(keyBy(c, key));
}
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | 12.0 ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Translates all items in an array or object to new array of items.
// Underscore/Lodash
var array1 = [1, 2, 3]
var array2 = _.map(array1, function (value, index) {
return value * 2
})
console.log(array2)
// output: [2, 4, 6]
// Native
var array1 = [1, 2, 3]
var array2 = array1.map(function (value, index) {
return value * 2
})
console.log(array2)
// output: [2, 4, 6]
// Underscore/Lodash
var object1 = { 'a': 1, 'b': 2, 'c': 3 }
var object2 = _.map(object1, function (value, index) {
return value * 2
})
console.log(object2)
// output: [2, 4, 6]
// Native
var object1 = { 'a': 1, 'b': 2, 'c': 3 }
var object2 = Object.entries(object1).map(function ([key, value], index) {
return value * 2
})
console.log(object2)
// output: [2, 4, 6]
Object.entries()
and destructuring![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | ✖ | ✔ | ✔ |
Array.prototype.map()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Use Array#reduce for find the maximum or minimum collection item
// Underscore/Lodash
var data = [{ value: 6 }, { value: 2 }, { value: 4 }]
var minItem = _.minBy(data, 'value')
var maxItem = _.maxBy(data, 'value')
console.log(minItem, maxItem)
// output: { value: 2 } { value: 6 }
// Native
var data = [{ value: 6 }, { value: 2 }, { value: 4 }]
var minItem = data.reduce(function(a, b) { return a.value <= b.value ? a : b }, {})
var maxItem = data.reduce(function(a, b) { return a.value >= b.value ? a : b }, {})
console.log(minItem, maxItem)
// output: { value: 2 }, { value: 6 }
Extract a functor and use es2015 for better code
// utils
const makeSelect = (comparator) => (a, b) => comparator(a, b) ? a : b
const minByValue = makeSelect((a, b) => a.value <= b.value)
const maxByValue = makeSelect((a, b) => a.value >= b.value)
// main logic
const data = [{ value: 6 }, { value: 2 }, { value: 4 }]
const minItem = data.reduce(minByValue, {})
const maxItem = data.reduce(maxByValue, {})
console.log(minItem, maxItem)
// output: { value: 2 }, { value: 6 }
// or also more universal and little slower variant of minBy
const minBy = (collection, key) => {
// slower because need to create a lambda function for each call...
const select = (a, b) => a[key] <= b[key] ? a : b
return collection.reduce(select, {})
}
console.log(minBy(data, 'value'))
// output: { value: 2 }
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
array.map
or _.map
can also be used to replace _.pluck
. Lodash v4.0 removed _.pluck
in favor of _.map
with iteratee shorthand. Details can be found in Changelog
// Underscore/Lodash
var array1 = [{name: "Alice"}, {name: "Bob"}, {name: "Jeremy"}]
var names = _.pluck(array1, "name")
console.log(names)
// output: ["Alice", "Bob", "Jeremy"]
// Native
var array1 = [{name: "Alice"}, {name: "Bob"}, {name: "Jeremy"}]
var names = array1.map(function(x){
return x.name
})
console.log(names)
// output: ["Alice", "Bob", "Jeremy"]
Array.prototype.map()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Applies a function against an accumulator and each value of the array (from left-to-right) to reduce it to a single value.
// Underscore/Lodash
var array = [0, 1, 2, 3, 4]
var result = _.reduce(array, function (previousValue, currentValue, currentIndex, array) {
return previousValue + currentValue
})
console.log(result)
// output: 10
// Native
var array = [0, 1, 2, 3, 4]
var result = array.reduce(function (previousValue, currentValue, currentIndex, array) {
return previousValue + currentValue
})
console.log(result)
// output: 10
Array.prototype.reduce()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Creates an array of numbers progressing from start up to.
// Underscore/Lodash
_.range(4) // output: [0, 1, 2, 3]
_.range(-4) // output: [0, -1, -2, -3]
_.range(1, 5) // output: [1, 2, 3, 4]
_.range(0, 20, 5) // output: [0, 5, 10, 15]
// Native ( solution with Array.from )
Array.from({length: 4}, (_, i) => i) // output: [0, 1, 2, 3]
Array.from({length: 4}, (_, i) => -i) // output: [-0, -1, -2, -3]
Array.from({length: 4}, (_, i) => i + 1) // output: [1, 2, 3, 4]
Array.from({length: 4}, (_, i) => i * 5) // output: [0, 5, 10, 15]
// Native ( solution with keys() and spread )
[...Array(4).keys()] // output: [0, 1, 2, 3]
[...Array(4).keys()].map(k => -k) // output: [-0, -1, -2, -3]
[...Array(4).keys()].map(k => k + 1) // output: [1, 2, 3, 4]
[...Array(4).keys()].map(k => k * 5) // output: [0, 5, 10, 15]
Array.from()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 32.0 ✔ | ✖ | ✔ | 9.0 ✔ |
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | 12.0 ✔ | 16.0 ✔ | ✖ | 37.0 ✔ | 7.1 ✔ |
This method is like _.reduce except that it iterates over elements of collection from right to left.
// Underscore/Lodash
var array = [0, 1, 2, 3, 4]
var result = _.reduceRight(array, function (previousValue, currentValue, currentIndex, array) {
return previousValue - currentValue
})
console.log(result)
// output: -2
// Native
var array = [0, 1, 2, 3, 4]
var result = array.reduceRight(function (previousValue, currentValue, currentIndex, array) {
return previousValue - currentValue
})
console.log(result)
// output: -2
Array.prototype.reduceRight()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
The opposite of _.filter; this method returns the elements of collection that predicate does not return truthy for.
// Underscore/Lodash
var array = [1, 2, 3, 4, 5];
var result = _.reject(array, function (x) {
return x % 2 === 0;
});
// output: [1, 3, 5]
// Native
var array = [1, 2, 3, 4, 5];
var reject = function (arr, predicate) {
var complement = function (f) {
return function (x) {
return !f(x);
}
};
return arr.filter(complement(predicate));
};
// output: [1, 3, 5]
Array.prototype.filter()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | 12 ✔ | 1.5 ✔ | 9.0 ✔ | 9.5 ✔ | 3.0 ✔ |
Gets a random element from array
.
// Underscore/Lodash
const array = [0, 1, 2, 3, 4]
const result = _.sample(array)
console.log(result)
// output: 2
// Native
const array = [0, 1, 2, 3, 4]
const sample = arr => {
const len = arr == null ? 0 : arr.length
return len ? arr[Math.floor(Math.random() * len)] : undefined
}
const result = sample(array)
console.log(result)
// output: 2
Array.prototype.length()
and Math.random()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Returns the number of values in the collection.
// Underscore/Lodash
var result = _.size({one: 1, two: 2, three: 3})
console.log(result)
// output: 3
// Native
var result2 = Object.keys({one: 1, two: 2, three: 3}).length
console.log(result2)
// output: 3
Object.keys()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 12.0 ✔ | 5.0 ✔ |
Tests whether any of the elements in the array pass the test implemented by the provided function.
// Underscore/Lodash
function isLargerThanTen (element, index, array) {
return element >= 10
}
var array = [10, 9, 8]
var result = _.some(array, isLargerThanTen)
console.log(result)
// output: true
// Native
function isLargerThanTen (element, index, array) {
return element >= 10
}
var array = [10, 9, 8]
var result = array.some(isLargerThanTen)
console.log(result)
// output: true
Array.prototype.some()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | ✔ 9.0 | ✔ | ✔ |
Sorts an array of object based on an object key provided by a parameter (note this is more limited than Underscore/Lodash).
const fruits = [
{name:"banana", amount: 2},
{name:"apple", amount: 4},
{name:"pineapple", amount: 2},
{name:"mango", amount: 1}
];
// Underscore
_.sortBy(fruits, 'name');
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]
// Lodash
_.orderBy(fruits, ['name'],['asc']);
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]
// Native
const sortBy = (key) => {
return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0);
};
// The native sort modifies the array in place. `_.orderBy` and `_.sortBy` do not, so we use `.concat()` to
// copy the array, then sort.
fruits.concat().sort(sortBy("name"));
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]
Array.prototype.concat()
and Array.prototype.sort()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
1.0 ✔ | ✔ | 1.0 ✔ | 5.5 ✔ | ✔ | ✔ |
Produces a duplicate-free version of the array, using === to test object equality.
// Underscore/Lodash
var array = [1, 2, 1, 4, 1, 3]
var result = _.uniq(array)
console.log(result)
// output: [1, 2, 4, 3]
// Native
var array = [1, 2, 1, 4, 1, 3];
var result = [...new Set(array)];
console.log(result)
// output: [1, 2, 4, 3]
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | 12.0 ✔ | 16.0 ✔ | ✖ | 37.0 ✔ | 8.0 ✔ |
:heavy_exclamation_mark:Note this is an alternative implementation
Creates a version of the function that will only be run after first being called count times. Useful for grouping asynchronous responses, where you want to be sure that all the async calls have finished, before proceeding.
var notes = ['profile', 'settings']
// Underscore/Lodash
var renderNotes = _.after(notes.length, render)
notes.forEach(function (note) {
console.log(note)
renderNotes()
})
// Native
notes.forEach(function (note, index) {
console.log(note)
if (notes.length === (index + 1)) {
render()
}
})
Array.prototype.forEach()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.5 ✔ | 9.0 ✔ | ✔ | ✔ |
Create a new function that calls func with thisArg and args.
var objA = {
x: 66,
offsetX: function(offset) {
return this.x + offset;
}
}
var objB = {
x: 67
};
// Underscore/Lodash
var boundOffsetX = _.bind(objA.offsetX, objB, 0);
// Native
var boundOffsetX = objA.offsetX.bind(objB, 0);
Function.prototype.bind()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
7.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 11.6 ✔ | 5.1 ✔ |
Checks if value is classified as a Function object.
// Lodash
_.isFunction(console.log);
// => true
_.isFunction(/abc/);
// => false
// Native
function isFunction(func) {
return typeof func === "function";
}
isFunction(setTimeout);
// => true
isFunction(123);
// => false
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Create a new function that calls func with thisArg and args.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
}
// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', debounce(calculateLayout, 150));
debounce
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
7.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 11.6 ✔ | 5.1 ✔ |
Create a new function that calls func with args.
// Lodash
function greet(greeting, name) {
return greeting + ' ' + name;
}
var sayHelloTo = _.partial(greet, 'Hello');
var result = sayHelloTo('Jose')
console.log(result)
// output: 'Hello Jose'
// Native
function greet(greeting, name) {
return greeting + ' ' + name;
}
var sayHelloTo = (...args) => greet('Hello', ...args)
var result = sayHelloTo('Jose')
console.log(result)
// output: 'Hello Jose'
// Native
const partial = (func, ...boundArgs) => (...remainingArgs) => func(...boundArgs, ...remainingArgs)
var sayHelloTo = partial(greet, 'Hello');
var result = sayHelloTo('Jose')
console.log(result)
// output: 'Hello Jose'
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
46.0 ✔ | 12.0 ✔ | 16.0 ✔ | ✖ | 37.0 ✔ | 8.0 ✔ |
Create a new function that limits calls to func to once every given timeframe.
function throttle(func, timeFrame) {
var lastTime = 0;
return function (...args) {
var now = new Date();
if (now - lastTime >= timeFrame) {
func(...args);
lastTime = now;
}
};
}
// Avoid running the same function twice within the specified timeframe.
jQuery(window).on('resize', throttle(calculateLayout, 150));
throttle
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | 12.0 ✔ | 3.0 ✔ | 9.0 ✔ | 10.5 ✔ | 4.0 ✔ |
Puts the value into an array of length one if it is not already an array.
// Underscore
console.log(_.castArray(5))
// output: [5]
console.log(_.castArray([2]))
// output: [2]
// Native
function castArray(arr) {
return Array.isArray(arr) ? arr : [arr]
}
// output: true
console.log(castArray(5));
// output: [5]
console.log(castArray([2]));
// output: [2]
Array.isArray()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 10.5 ✔ | 5.0 ✔ |
Checks if value is classified as a Date object.
// Lodash
console.log(_.isDate(new Date));
// output: true
// Native
console.log(Object.prototype.toString.call(new Date) === "[object Date]");
// output: true
String.prototype.toString.call()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Checks if value is greater than other.
// Lodash
console.log(_.gt(3, 1))
// output: true
// Native
console.log(3 > 1);
// output: true
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Checks if value is greater than or equal to other.
// Lodash
console.log(_.gte(3, 1))
// output: true
// Native
console.log(3 >= 1);
// output: true
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Checks if value is an empty object or collection.
:heavy_exclamation_mark:Note that the Native version does not support evaluating a Set or a Map
// Lodash
console.log(_.isEmpty(null))
// output: true
console.log(_.isEmpty(''))
// output: true
console.log(_.isEmpty({}))
// output: true
console.log(_.isEmpty([]))
// output: true
console.log(_.isEmpty({a: '1'}))
// output: false
// Native
const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length;
console.log(isEmpty(null))
// output: true
console.log(isEmpty(''))
// output: true
console.log(isEmpty({}))
// output: true
console.log(isEmpty([]))
// output: true
console.log(isEmpty({a: '1'}))
// output: false
Array.prototype.includes()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
47.0 ✔ | 14.0 ✔ | 43.0 ✔ | ✖ | 34.0 ✔ | 9.0 ✔ |
Checks if value is a finite primitive number.
// Lodash
console.log(_.isFinite('3'))
// output: false
console.log(_.isFinite(3))
// output: true
// Native
console.log(Number.isFinite('3'))
// output: false
console.log(Number.isFinite(3))
// output: true
Number.isFinite()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
19.0 ✔ | ✔ | 16.0 ✔ | ✖ | 15.0 ✔ | 9.0 ✔ |
Checks if value is an integer.
// Lodash
console.log(_.isInteger(3))
// output: true
console.log(_.isInteger('3'))
// output: false
// Native
console.log(Number.isInteger(3))
// output: true
console.log(Number.isInteger('3'))
// output: false
Number.isInteger()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | 12 ✔ | 16.0 ✔ | ✖ | ✔ | ✔ |
Checks if a value is NaN.
// Underscore/Lodash
console.log(_.isNaN(NaN))
// output: true
// Native
console.log(isNaN(NaN))
// output: true
// ES6
console.log(Number.isNaN(NaN))
// output: true
MDN:
In comparison to the global
isNaN()
function,Number.isNaN()
doesn't suffer the problem of forcefully converting the parameter to a number. This means it is now safe to pass values that would normally convert toNaN
, but aren't actually the same value asNaN
. This also means that only values of the type number, that are alsoNaN
, return true. Number.isNaN()
Voice from the Lodash author:
Lodash's
_.isNaN
is equiv to ES6Number.isNaN
which is different than the globalisNaN
. --- jdalton
isNaN
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
Number.isNaN
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
25.0 ✔ | ✔ | 15.0 ✔ | ✖ | ✔ | 9.0 ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Checks if value is null or undefined.
// Lodash
console.log(_.isNil(null))
// output: true
console.log(_.isNil(NaN))
// output: false
console.log(_.isNil(undefined))
// output: true
// Native
console.log(null == null);
// output: true
console.log(NaN == null);
// output: false
console.log(undefined == null)
// output: true
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Checks if value is null or undefined.
// Underscore/Lodash
console.log(_.isNull(null))
// output: true
console.log(_.isNull(void 0))
// output: false
// Native
console.log(null === null);
// output: true
console.log(void 0 === null);
// output: false
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Checks if value is undefined.
// Underscore/Lodash
console.log(_.isUndefined(a))
// output: true
// Native
console.log(typeof a === 'undefined');
// output: true
console.log(a === undefined);
// output: true
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1 ✔ | ✔ | ✔ | ✔ |
The method is used to copy the values of all enumerable own properties from one or more source objects to a target object.
// Underscore: _.extendOwn
// Lodash
function Foo() {
this.c = 3;
}
function Bar() {
this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = _.assign(new Foo, new Bar);
console.log(result);
// output: { 'c': 3, 'e': 5 }
// Native
function Foo() {
this.c = 3;
}
function Bar() {
this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = Object.assign({}, new Foo, new Bar);
console.log(result);
// output: { 'c': 3, 'e': 5 }
Object.assign()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 34.0 ✔ | ✖ | 32.0 ✔ | 9.0 ✔ |
The method is used to apply new values over an object with default values for keys.
// Underscore: _.defaults
// Lodash
const newValues = {a: 3};
const defaultValues = {a: 1, b: 2}
const appliedValues = _.defaults(newValues, defaultValues);
console.log(appliedValues)
// output { a: 3, b: 2 }
// Native
const newValues = {a: 3};
const defaultValues = {a: 1, b: 2}
const appliedValues = Object.assign({}, defaultValues, newValues);
// output { a: 3, b: 2 }
Object.assign()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 34.0 ✔ | ✖ | 32.0 ✔ | 9.0 ✔ |
The method is used to copy the values of all enumerable own and inherited properties from one or more source objects to a target object.
// Underscore
// Lodash: _.assignIn
function Foo() {
this.c = 3;
}
function Bar() {
this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = _.extend({}, new Foo, new Bar);
console.log(result);
// output: { 'c': 3, 'd': 4, 'e': 5, 'f': 6 }
// Native
function Foo() {
this.c = 3;
}
function Bar() {
this.e = 5;
}
Foo.prototype.d = 4;
Bar.prototype.f = 6;
var result = Object.assign({}, new Foo, Foo.prototype, new Bar, Bar.prototype);
console.log(result);
// output: { 'c': 3, 'd': 4, 'e': 5, 'f': 6 }
//Or using a function
const extend = (target, ...sources) => {
const length = sources.length;
if (length < 1 || target == null) return target;
for (let i = 0; i < length; i++) {
const source = sources[i];
for (const key in source) {
target[key] = source[key];
}
}
return target;
};
console.log(extend({}, new Foo, new Bar));
// output: { 'c': 3, 'd': 4, 'e': 5, 'f': 6 }
Object.assign()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 34.0 ✔ | ✖ | 32.0 ✔ | 9.0 ✔ |
Checks if key
is a direct property of object
. Key may be a path of a value separated by .
// Lodash
var object = { a: 1, b: 'settings', c: { d: 'test' } };
var hasA = _.has(object, 'a');
var hasCWhichHasD = _.has(object, 'c.d')
console.log(hasA);
// output: true
console.log(hasCWhichHasD);
// output: true
// Native
const has = function (obj, key) {
var keyParts = key.split('.');
return !!obj && (
keyParts.length > 1
? has(obj[key.split('.')[0]], keyParts.slice(1).join('.'))
: hasOwnProperty.call(obj, key)
);
};
var object = { a: 1, b: 'settings' };
var result = has(object, 'a');
// output: true
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | 12 ✔ | ✔ | 5.5 ✔ | 5 ✔ | 3 ✔ |
Gets the value at path of object. Note: If provided path does not exist inside the object js will generate error.
// Lodash
var object = { a: [{ b: { c: 3 } }] };
var result = _.get(object, 'a[0].b.c', 1);
console.log(result);
// output: 3
// Native (ES6 - IE not supported)
var object = { a: [{ b: { c: 3 } }] };
var { a: [{ b: { c: result = 1 } = {} } = {}] = [] } = object;
console.log(result);
// output: 3
// Native (ES11)
var object = { a: [{ b: { c: 3 } }] };
var result = object?.a?.[0]?.b?.c ?? 1;
console.log(result);
// output: 3
// Native
const get = (obj, path, defaultValue = undefined) => {
const travel = regexp =>
String.prototype.split
.call(path, regexp)
.filter(Boolean)
.reduce((res, key) => (res !== null && res !== undefined ? res[key] : res), obj);
const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/);
return result === undefined || result === obj ? defaultValue : result;
};
var object = { a: [{ b: { c: 3 } }] };
var result = get(object, 'a[0].b.c', 1);
// output: 3
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
49.0 ✔ | 14.0 ✔ | 41.0 ✔ | ✖ | 41.0 ✔ | 8.0 ✔ |
?.
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
80.0 ✔ | 80.0 ✔ | 74.0 ✔ | ✖ | 67.0 ✔ | 13.1 ✔ |
??
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
80.0 ✔ | 80.0 ✔ | 72.0 ✔ | ✖ | ✖ | 13.1 ✔ |
Retrieves all the names of the object's own enumerable properties.
// Underscore/Lodash
var result = _.keys({one: 1, two: 2, three: 3})
console.log(result)
// output: ["one", "two", "three"]
// Native
var result2 = Object.keys({one: 1, two: 2, three: 3})
console.log(result2)
// output: ["one", "two", "three"]
Object.keys()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | ✔ | 4.0 ✔ | 9.0 ✔ | 12.0 ✔ | 5.0 ✔ |
Returns a copy of the object, filtered to omit the keys specified.
var object = { 'a': 1, 'b': '2', 'c': 3 };
// Underscore/Lodash
var result = _.omit(object, ['a', 'c']);
console.log(result)
// output: { 'b': '2' }
// Native
var { a, c, ...result2 } = object;
console.log(result2)
// output: { 'b': '2' }
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
60.0 ✔ | ✖ | 55.0 ✔ | ✖ | 37.0 ✔ | ✖ |
Creates an object composed of the object properties predicate returns truthy for.
var object = { 'a': 1, 'b': '2', 'c': 3 };
// Underscore/Lodash
var result = _.pick(object, ['a', 'c']);
console.log(result)
// output: {a: 1, c: 3}
// Native
const { a, c } = object;
const result = { a, c};
console.log(result);
// output: {a: 1, c: 3}
// for an array of this object --> array.map(({a, c}) => ({a, c}));
// Native
function pick(object, keys) {
return keys.reduce((obj, key) => {
if (object && object.hasOwnProperty(key)) {
obj[key] = object[key];
}
return obj;
}, {});
}
var result = pick(object, ['a', 'c']);
console.log(result)
// output: {a: 1, c: 3}
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
38.0 ✔ | ✔ | 13.0 ✔ | 12.0 ✔ | 25.0 ✔ | 7.1 ✔ |
Creates an object composed of the object properties predicate returns truthy for.
var object = { 'a': 1, 'b': null, 'c': 3, 'd': false, 'e': undefined };
// Underscore/Lodash
var result = _.pickBy(object);
console.log(result)
// output: {a: 1, c: 3}
// Native
function pickBy(object) {
const obj = {};
for (const key in object) {
if (object[key]) {
obj[key] = object[key];
}
}
return obj;
}
var result = pickBy(object);
console.log(result)
// output: {a: 1, c: 3}
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | 6.0 ✔ | ✔ | ✔ |
Retrieves all the given object's own enumerable property [ key, value ]
pairs.
// Underscore - also called _.pairs
// Lodash - also called _.entries
var result = _.toPairs({one: 1, two: 2, three: 3})
console.log(result)
// output: [["one", 1], ["two", 2], ["three", 3]]
// Native
var result2 = Object.entries({one: 1, two: 2, three: 3})
console.log(result2)
// output: [["one", 1], ["two", 2], ["three", 3]]
Object.entries()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
54.0 ✔ | 14.0 ✔ | 47.0 ✔ | ✖ | 41.0 ✔ | 10.1 ✔ |
Retrieves all the given object's own enumerable property values.
// Underscore/Lodash
var result = _.values({one: 1, two: 2, three: 3})
console.log(result)
// output: [1, 2, 3]
// Native
var result2 = Object.values({one: 1, two: 2, three: 3})
console.log(result2)
// output: [1, 2, 3]
Object.values()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
54.0 ✔ | 14.0 ✔ | 47.0 ✔ | ✖ | 41.0 ✔ | 10.1 ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Converts the first character of string to upper case and the remaining to lower case.
// Lodash
var result = _.capitalize('FRED');
console.log(result);
// => 'Fred'
// Native
const capitalize = (string) => {
return string ? string.charAt(0).toUpperCase() + string.slice(1).toLowerCase() : '';
};
var result = capitalize('FRED');
console.log(result);
// => 'Fred'
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Checks if string ends with the given target string.
// Lodash
_.endsWith('abc', 'c');
// => true
_.endsWith('abc', 'b');
// => false
_.endsWith('abc', 'b', 2);
// => true
// Native
'abc'.endsWith('c');
// => true
'abc'.endsWith('b');
// => false
'abc'.endsWith('b', 2);
// => true
String.prototype.endsWith()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
41.0 ✔ | ✔ | 17.0 ✔ | ✖ | 28.0 ✔ | 9.0 ✔ |
Checks if value is classified as a String primitive or object.
// Lodash
_.isString('abc');
// => true
_.isString(123);
// => false
// Native
function isString(str){
if (str != null && typeof str.valueOf() === "string") {
return true
}
return false
}
isString('abc');
// => true
isString(123);
// => false
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Converts the first character of string to lower case.
// Lodash
var result = _.lowerFirst('Fred')
console.log(result)
// output: 'fred'
// Native
const lowerFirst = (string) => {
return string ? string.charAt(0).toLowerCase() + string.slice(1) : ''
}
var result = lowerFirst('Fred')
console.log(result)
// output: 'fred'
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length.
// Lodash
console.log(_.padStart('123', 5, '0'))
// output: '00123'
console.log(_.padEnd('123', 5, '0'))
// output: '12300'
// Native
console.log('123'.padStart(5, '0'))
// output: '00123'
console.log('123'.padEnd(5, '0'))
// output: '12300'
String.prototype.padStart()
and String.prototype.padEnd()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
57.0 ✔ | 15.0 ✔ | 48.0 ✔ | ✖ | 44.0 ✔ | 10.0 ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Repeats the given string n times.
// Lodash
var result = _.repeat('abc', 2)
console.log(result)
// output: 'abcabc'
// Native
var result = 'abc'.repeat(2)
console.log(result)
// output: 'abcabc'
String.prototype.repeat()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
41.0 ✔ | ✔ | 24.0 ✔ | ✖ | 28.0 ✔ | 9.0 ✔ |
returns a new string with some or all matches of a pattern
replaced by a replacement
// Lodash
var re = /apples/gi;
var str = 'Apples are round, and apples are juicy.';
var newstr = _.replace(str, re, 'oranges');
console.log(newstr);
// output: 'oranges are round, and oranges are juicy.'
// Native
var re = /apples/gi;
var str = 'Apples are round, and apples are juicy.';
var result = str.replace(re, 'oranges');
console.log(result);
// output: 'oranges are round, and oranges are juicy.'
String.prototype.replace()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Splits string by separator.
// Lodash
var result = _.split('a-b-c', '-', 2)
console.log(result)
// output: ['a','b']
// Native
var result = 'a-b-c'.split('-', 2)
console.log(result)
// output: ['a','b']
String.prototype.split()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Checks if string starts with the given target string.
// Lodash
var result = _.startsWith('abc', 'b', 1)
console.log(result)
// output: true
// Native
var result = 'abc'.startsWith('b', 1)
console.log(result)
// output: true
String.prototype.startsWith()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
41.0 ✔ | ✔ | 17.0 ✔ | ✖ | 28.0 ✔ | 9.0 ✔ |
:heavy_exclamation_mark: Note this is an alternative implementation. Native template literals not escape html.
Create a template function.
// Lodash/Underscore
const compiled = _.template('hello <%= user %>!');
var result = compiled({ 'user': 'fred' });
console.log(result);
// output: 'hello fred'
// Native
const templateLiteral = (value) => `hello ${value.user}`;
var result = templateLiteral({ 'user': 'fred' });
console.log(result);
// output: 'hello fred'
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
41.0 ✔ | 12.0 ✔ | 34.0 ✔ | ✖ | 28.0 ✔ | 9.0 ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Lowercases a given string.
// Lodash
var result = _.toLower('FOOBAR')
console.log(result)
// output: 'foobar'
// Native
var result = 'FOOBAR'.toLowerCase()
console.log(result)
// output: 'foobar'
String.prototype.toLowerCase()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Uppercases a given string.
// Lodash
var result = _.toUpper('foobar')
console.log(result)
// output: 'FOOBAR'
// Native
var result = 'foobar'.toUpperCase()
console.log(result)
// output: 'FOOBAR'
String.prototype.toUpperCase()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | 1.0 ✔ | ✔ | ✔ | ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Removes the leading and trailing whitespace characters from a string.
// Lodash
var result = _.trim(' abc ')
console.log(result)
// output: 'abc'
// Native
var result = ' abc '.trim()
console.log(result)
// output: 'abc'
String.prototype.trim()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
5.0 ✔ | ✔ | 3.5 ✔ | 9.0 ✔ | 10.5 ✔ | 5.0 ✔ |
:heavy_exclamation_mark:Not in Underscore.js
Uppercases the first letter of a given string
// Lodash
var result = _.upperFirst('george')
console.log(result)
// output: 'George'
// Native
const upperFirst = (string) => {
return string ? string.charAt(0).toUpperCase() + string.slice(1) : ''
}
var result = upperFirst('george')
console.log(result)
// output: 'George'
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
similar to _.uniq except that it accepts comparator which is invoked to compare elements of array. The order of result values is determined by the order they occur in the array.
// Lodash
const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
const result = _.uniqWith(objects, _.isEqual);
console.log(result);
// output: [{ x: 1, y: 2 }, { x: 2, y: 1 }]
// Native
const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);
const array = [1, 2, 2, 3, 4, 5, 2];
const result = uniqWith(array, (a, b) => a === b);
console.log(result);
// output: [1, 2, 3, 4, 5]
const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
const result = uniqWith(objects, (a, b) => JSON.stringify(a) === JSON.stringify(b));
console.log(result);
// output: [{ x: 1, y: 2 }, { x: 2, y: 1 }]
Array.prototype.filter()
and Array.prototype.findIndex()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | 12.0 ✔ | 25.0 ✔ | ✖ | 32.0 ✔ | 8.0 ✔ |
Invokes the iteratee n times, returning an array of the results of each invocation.
// Lodash
var result = _.times(10)
console.log(result)
// output: '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'
// Native
var result = Array.from({length: 10}, (_,x) => x)
console.log(result)
// output: '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'
// Native
var result = [...Array(10).keys()]
console.log(result)
// output: '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'
Array.from()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
45.0 ✔ | ✔ | 32.0 ✔ | ✖ | ✔ | 9.0 ✔ |
Clamps number within the inclusive lower and upper bounds.
// Lodash
_.clamp(-10, -5, 5);
// => -5
_.clamp(10, -5, 5);
// => 5
_.clamp(10, -5);
// => -5
_.clamp(10, 99);
// => 10
// Native
const clamp = (number, boundOne, boundTwo) => {
if (!boundTwo) {
return Math.max(number, boundOne) === boundOne ? number : boundOne;
} else if (Math.min(number, boundOne) === number) {
return boundOne;
} else if (Math.max(number, boundTwo) === number) {
return boundTwo;
}
return number;
};
clamp(-10, -5, 5);
// => -5
clamp(10, -5, 5);
// => 5
clamp(10, -5);
// => -5
clamp(10, 99);
// => 10
Math.min() and Math.max()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Checks if n is between start and up to, but not including, end. If end is not specified, it's set to start with start then set to 0. If start is greater than end the params are swapped to support negative ranges.
// Lodash
_.inRange(3, 2, 4);
// output: true
_.inRange(-3, -2, -6);
// output: true
//Native
const inRange = (num, init, final) => {
if(final === undefined){
final = init;
init = 0;
}
return (num >= Math.min(init, final) && num < Math.max(init, final));
}
//Native
const inRange = (num, a, b=0) => (Math.min(a,b) <= num && num < Math.max(a,b));
inRange(3, 2, 4);
// output: true
inRange(-3, -2, -6);
// output: true
Math.min() and Math.max()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Produces a random number between the inclusive lower and upper bounds. If only one argument is provided a number between 0 and the given number is returned. If floating is true, or either lower or upper are floats, a floating-point number is returned instead of an integer.
// Lodash
_.random(0, 5);
// => an integer between 0 and 5
_.random(5);
// => also an integer between 0 and 5
_.random(5, true);
// => a floating-point number between 0 and 5
_.random(1.2, 5.2);
// => a floating-point number between 1.2 and 5.2
//Native ES6
const random = (a = 1, b = 0) => {
const lower = Math.min(a, b);
const upper = Math.max(a, b);
return lower + Math.random() * (upper - lower);
};
const randomInt = (a = 1, b = 0) => {
const lower = Math.ceil(Math.min(a, b));
const upper = Math.floor(Math.max(a, b));
return Math.floor(lower + Math.random() * (upper - lower + 1))
};
random();
// => a floating-point number between 0 and 1
random(5);
// => a floating-point number between 0 and 5
random(0, 5);
// => also a floating-point number between 0 and 5
random(1.2, 5.2);
// => a floating-point number between 1.2 and 5.2
randomInt();
// => just 0 or 1
randomInt(5);
// => an integer between 0 and 5
randomInt(0, 5);
// => also an integer between 0 and 5
randomInt(1.2, 5.2);
// => an integer between 2 and 5
Math.random()
![Chrome][chrome-image] | ![Edge][edge-image] | ![Firefox][firefox-image] | ![IE][ie-image] | ![Opera][opera-image] | ![Safari][safari-image] |
---|---|---|---|---|---|
✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Author: You-dont-need
Source Code: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
License: MIT license
1658734080
A Flutter plugin which makes it straightforward to show the native equivalent of a CupertinoAlertDialog or CupertinoActionSheet dialog.
To use this plugin, add native_ios_dialog
as a dependency in your pubspec.yaml file.
Dialogs itself in Flutter are pretty awesome. However, the CupertinoAlertDialogs do not provide the same feeling as native iOS dialogs, so I created this plugin. With this plugin, you have all the customization options iOS provides.
Each button can also be disabled
NativeIosDialog(title: "Info", message: "Please consider the following information in this dialog.", style: style, actions: [
NativeIosDialogButton(text: "OK", style: NativeIosDialogButtonStyle.defaultStyle, onPressed: () {}),
]).show();
NativeIosDialog(title: "Confirm", message: "Please confirm the following information in this dialog.", style: style, actions: [
NativeIosDialogButton(text: "OK", style: NativeIosDialogButtonStyle.defaultStyle, onPressed: () {}),
NativeIosDialogButton(text: "Cancel", style: NativeIosDialogButtonStyle.cancel, onPressed: () {}),
]).show();
Run this command:
With Flutter:
$ flutter pub add native_ios_dialog
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
native_ios_dialog: ^0.0.2
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:native_ios_dialog/native_ios_dialog.dart';
example/lib/main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:native_ios_dialog/native_ios_dialog.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const CupertinoApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int currentDialogStyle = 0;
@override
Widget build(BuildContext context) {
final NativeIosDialogStyle style = currentDialogStyle == 0
? NativeIosDialogStyle.alert
: NativeIosDialogStyle.actionSheet;
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text("Native iOS Dialogs"),
),
child: ListView(
children: [
const SizedBox(height: 12),
const SimpleDivider(text: "Basic examples"),
SimpleButton(
text: "Info Dialog",
onPressed: () {
NativeIosDialog(
title: "Info",
message:
"Please consider the following information in this dialog.",
style: style,
actions: [
NativeIosDialogButton(
text: "OK",
style: NativeIosDialogButtonStyle.defaultStyle,
onPressed: () {}),
]).show();
},
),
SimpleButton(
text: "Confirm Dialog",
onPressed: () {
NativeIosDialog(
title: "Confirm",
message:
"Please confirm the following information in this dialog.",
style: style,
actions: [
NativeIosDialogButton(
text: "OK",
style: NativeIosDialogButtonStyle.defaultStyle,
onPressed: () {}),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
SimpleButton(
text: "Destructive Confirm Dialog",
onPressed: () {
NativeIosDialog(
title: "Confirm deletion",
message:
"Do you want to delete this resource? You cannot undo this action.",
style: style,
actions: [
NativeIosDialogButton(
text: "Delete",
style: NativeIosDialogButtonStyle.destructive,
onPressed: () {}),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
SimpleButton(
text: "Three Buttons",
onPressed: () {
NativeIosDialog(
title: "Are you sure?",
message:
"Do you want to apply the changes you made, or do you want to delete this resource?",
style: style,
actions: [
NativeIosDialogButton(
text: "Apply",
style: NativeIosDialogButtonStyle.defaultStyle,
onPressed: () {}),
NativeIosDialogButton(
text: "Delete",
style: NativeIosDialogButtonStyle.destructive,
onPressed: () {}),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
const SimpleDivider(text: "Examples with disabled actions"),
SimpleButton(
text: "Confirm Dialog",
onPressed: () {
NativeIosDialog(
title: "Confirm",
message:
"Please confirm the following information in this dialog.\n\nOh wait!\nSeems like there is something that prohibits you from confirming this dialog.",
style: style,
actions: [
NativeIosDialogButton(
text: "OK",
style: NativeIosDialogButtonStyle.defaultStyle),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
SimpleButton(
text: "Destructive Confirm Dialog",
onPressed: () {
NativeIosDialog(
title: "Confirm deletion",
message:
"Do you want to delete this resource? You cannot undo this action.\n\nOh wait!\nSeems like there is something that prohibits you from confirming this dialog.",
style: style,
actions: [
NativeIosDialogButton(
text: "Delete",
style: NativeIosDialogButtonStyle.destructive),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
SimpleButton(
text: "Three Buttons",
onPressed: () {
NativeIosDialog(
title: "Are you sure?",
message:
"Do you want to apply the changes you made, or do you want to delete this resource?",
style: style,
actions: [
NativeIosDialogButton(
text: "Apply",
style: NativeIosDialogButtonStyle.defaultStyle),
NativeIosDialogButton(
text: "Delete",
style: NativeIosDialogButtonStyle.destructive),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
const SimpleDivider(text: "Other"),
SimpleButton(
text: "100 Buttons",
onPressed: () {
NativeIosDialog(
title: "Uhm...",
message:
"Please do not use this in production... please do not do this! This dialog is just a demonstration of how iOS handles many buttons.",
style: style,
actions: [
for (int i = 1; i <= 100; i++)
NativeIosDialogButton(
text: "Action $i",
style: NativeIosDialogButtonStyle.defaultStyle,
onPressed: () {}),
NativeIosDialogButton(
text: "Cancel",
style: NativeIosDialogButtonStyle.cancel,
onPressed: () {}),
]).show();
},
),
const SimpleDivider(text: "Configuration"),
Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0),
child: CupertinoSlidingSegmentedControl(
groupValue: currentDialogStyle,
children: const {0: Text("Alert"), 1: Text("ActionSheet")},
onValueChanged: (int? newValue) {
if (newValue == null) return;
setState(() {
currentDialogStyle = newValue;
});
}),
)
],
),
);
}
}
class SimpleButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
const SimpleButton({Key? key, required this.text, required this.onPressed})
: super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
child: CupertinoButton(
child: Text(
text,
style: const TextStyle(color: Colors.white),
),
color: Theme.of(context).primaryColor,
onPressed: onPressed,
),
);
}
}
class SimpleDivider extends StatelessWidget {
final String text;
const SimpleDivider({Key? key, required this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8.0, left: 16, right: 16),
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
const Expanded(
child: Divider(
color: Colors.black,
)),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(text),
),
const Expanded(
child: Divider(
color: Colors.black,
)),
]),
);
}
}
Author: Gebes
Source Code: https://github.com/Gebes/native_ios_dialog
License: MIT license
1658459280
The lz4 in this library is a ffi binding to lz4 v1.9.3, the compressed block and frame format are both interoperable with official C api, as well as other interoperable ports, bindings and CLI tools.
import 'dart:ffi';
import 'package:dart_native_compression/dart_native_compression.dart';
final lz4 = Lz4Lib(lib: DynamicLibrary.open('libnative_compression.so'));
print('LZ4 version number: ${lz4.getVersioinNumber()}');
final compressedFrame = lz4.compressFrame(data);
final decompressed = lz4.decompressFrame(compressedFrame);
await for (final decompressedChunk
in lz4.decompressFrameStream(compressedStream)) {
// Your logic here
}
Go to unit test
pub get && pub run test
install rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
pull submodules
git submodule update --init --recursive
go to native_compression directory, run
cargo build --release --all-features
to cross compile to different target
cargo lipo --release --targets=aarch64-apple-ios,x86_64-apple-ios,armv7-apple-ios,armv7s-apple-ios
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release
The shared library will be under target/release . It would be libdart_native_compression.so, dart_native_compression.dll, libdart_native_compression.dylib on linux, windows, osx respectively.
Run this command:
With Dart:
$ dart pub add dart_native_compression
With Flutter:
$ flutter pub add dart_native_compression
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get
):
dependencies:
dart_native_compression: ^1.1.0+4
Alternatively, your editor might support dart pub get
or flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:dart_native_compression/dart_native_compression.dart';
example/example.md
import 'dart:ffi';
import 'package:dart_native_compression/dart_native_compression.dart';
final lz4 = Lz4Lib(lib: DynamicLibrary.open('libnative_compression.so'));
print('LZ4 version number: ${lz4.getVersioinNumber()}');
final compressedFrame = lz4.compressFrame(data);
final decompressed = lz4.decompressFrame(compressedFrame);
await for (final decompressedChunk
in lz4.decompressFrameStream(compressedStream)) {
// Your logic here
}
Go to unit test
Author: Hanabi1224
Source Code: https://github.com/hanabi1224/flutter_native_extensions
License: MIT license
1657148400
Bloom filter is a space-efficient probabilistic data structure that is used to test whether an element is a member of a set. False positives are possible, but false negatives are not. For more detail, check the wikipedia article. Instead of using k different hash functions, this implementation seeds the CRC32 hash with k different initial values (0, 1, ..., k-1). This may or may not give you a good distribution, it all depends on the data.
Performance of the Bloom filter depends on a number of variables:
MRI/C implementation which creates an in-memory filter which can be saved and reloaded from disk.
require 'bloomfilter-rb'
bf = BloomFilter::Native.new(:size => 100, :hashes => 2, :seed => 1, :bucket => 3, :raise => false)
bf.insert("test")
bf.include?("test") # => true
bf.include?("blah") # => false
bf.delete("test")
bf.include?("test") # => false
# Hash with a bloom filter!
bf["test2"] = "bar"
bf["test2"] # => true
bf["test3"] # => false
bf.stats
# => Number of filter bits (m): 10
# => Number of filter elements (n): 2
# => Number of filter hashes (k) : 2
# => Predicted false positive rate = 10.87%
Uses getbit/setbit on Redis strings - efficient, fast, can be shared by multiple/concurrent processes.
bf = BloomFilter::Redis.new
bf.insert('test')
bf.include?('test') # => true
bf.include?('blah') # => false
bf.delete('test')
bf.include?('test') # => false
Uses regular Redis get/set counters to implement a counting filter with optional TTL expiry. Because each "bit" requires its own key in Redis, you do incur a much larger memory overhead.
bf = BloomFilter::CountingRedis.new(:ttl => 2)
bf.insert('test')
bf.include?('test') # => true
sleep(2)
bf.include?('test') # => false
Tatsuya Mori valdzone@gmail.com (Original C implementation: http://vald.x0.com/sb/)
Author: igrigorik
Source Code: https://github.com/igrigorik/bloomfilter-rb
License: MIT License
1656784920
The Windows, Android, iOS and macOS implementation of flutter_exprtk
.
This package has been endorsed, meaning that you only need to add flutter_exprtk
as a dependency in your pubspec.yaml
. It will be automatically included in your app when you depend on package:flutter_exprtk
.
This is what the above means to your pubspec.yaml
:
...
dependencies:
...
flutter_exprtk: ^0.0.7
...
If you wish to use the package without web support, you can add flutter_exprtk_native
as a dependency:
...
dependencies:
...
flutter_exprtk_native: ^0.0.7
...
Run this command:
With Flutter:
$ flutter pub add flutter_exprtk_native
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
flutter_exprtk_native: ^0.0.7
Alternatively, your editor might support flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:flutter_exprtk_native/flutter_exprtk_native.dart';
Original article source at: https://pub.dev/packages/flutter_exprtk_native
1654941840
Typescript and Native ESM in Node.js How to Work on A Npm Package That Can Be Published As an Esm Package with Typescript. We Walk You Through Tsconfig And Package.json Settings.
1653627660
¿Está buscando una manera de convertir su aplicación Angular existente en una aplicación móvil sin tener que volver a escribir todo desde cero? Bueno, has venido al lugar indicado. En este artículo, usaremos Capacitor para lograr eso.
requisitos previos
Antes de comenzar, asegúrese de configurar su entorno instalando las siguientes dependencias:
¿Qué es el condensador?
Capacitor es un tiempo de ejecución nativo multiplataforma para crear aplicaciones web modernas que pueden ejecutarse de forma nativa en iOS o Android con facilidad. Ofrece características geniales como compatibilidad con PWA, Capacitor CLI y complementos increíbles.
Consulte los documentos para obtener más información .
1 — Generar una nueva aplicación angular
En aras de la simplicidad, solo convertiremos el proyecto angular generado desde la CLI.
ng new angular-mobile-app
El comando CLI anterior creará e inicializará una nueva aplicación Angular lista para usar.
Cuando finalice la generación, navegue hasta el directorio del proyecto y escriba el siguiente comando para iniciar el servidor de desarrollo:
cd angular-mobile-app
ng serve
Ahora, vaya a su navegador y escriba http://localhost:4200 , debería estar viendo la página a continuación si todo salió bien.
Aplicación angular que se ejecuta en localhost: 4200
2 — Agregue un condensador a su proyecto
Ahora que tenemos una aplicación web en funcionamiento, el siguiente paso es instalar ambas dependencias ejecutando @capacitor/core
los @capacitor/cli
siguientes comandos npm:
cd angular-mobile-app
npm install @capacitor/core
npm install @capacitor/cli
3 — Configure el archivo de configuración del capacitor
Después de instalar las dependencias del capacitor, ejecute el siguiente comando para generar el capacitor.config.ts
archivo:
npx cap init
La CLI le hará algunas preguntas generales sobre la aplicación. Solo deja todo como predeterminado por ahora.
El archivo generado se verá así:
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'angular-mobile-app',
webDir: 'dist/angular-mobile-app',
bundledWebRuntime: false
};
export default config;
capacitor.config.ts
appId
: El identificador único de su aplicación empaquetada.appName
: el nombre de su aplicación.webDir
: El directorio de sus activos web compilados.bundledWebRuntime
: Si copiar o no el paquete de tiempo de ejecución de Capacitor.Nota: asegúrese de cambiar el valor de webDir
a dist/(your app name)
ya que esa es la ruta de compilación habitual para una aplicación Angular. Ahora, capacitor podrá encontrar los activos web.
4 — Agregue paquetes nativos de iOS y Android
El siguiente paso es instalar los paquetes de acuerdo con iOS y Android y luego agregar los proyectos nativos ejecutando los siguientes comandos:
npm install @capacitor/ios @capacitor/androidnpx cap add iosnpx cap add android
Después de ejecutar los comandos CLI anteriores, debería ver dos nuevas carpetas creadas para Android e iOS.
5 — Cree su aplicación
Ahora construyamos nuestra aplicación Angular y sincronicemos los cambios a los proyectos nativos de Android/iOS ejecutando los siguientes comandos:
ng build --prodnpx cap sync
npx cap sync
copiará la carpeta de compilación en el proyecto iOS/Android.
6 — Abre el estudio de Android o Xcode
Puede abrir directamente el proyecto móvil en Android Studio o Xcode con el siguiente comando en su proyecto:
npx cap open ios
npx cap open android
Nota: asegúrese de configurar su entorno primero antes de ejecutar los comandos anteriores.
Ahora, el siguiente paso es usar Android Studio o Xcode para implementar su aplicación directamente en un dispositivo conectado o en un dispositivo virtual.
En mi caso, estoy usando Android Studio. Como puede ver en la imagen a continuación, estoy seleccionando un emulador de Pixel 4.
estudio android
Finalmente, haga clic en el botón ejecutar o depurar para ejecutar su aplicación.
7 — ¡Ejecuta la aplicación!
aplicación que se ejecuta en un dispositivo Android virtual
Terminando
Como puede ver, Capacitor es muy poderoso para convertir una aplicación angular existente en una aplicación móvil nativa.
Tener una base de código para múltiples plataformas es la última habilidad que se debe tener en estos días para que no solo ahorremos tiempo y dinero, sino que también aumentemos la productividad. ¡Disfrutar!
¡Gracias por leer y nos vemos en la próxima!
1653616800
すべてを最初から書き直すことなく、既存のAngularアプリケーションをモバイルアプリに変換する方法をお探しですか?さて、あなたは正しい場所に来ました。この記事では、それを実現するためにコンデンサを使用します。
前提条件
開始する前に、以下の依存関係をインストールして環境を設定してください。
コンデンサとは何ですか?
Capacitorは、iOSまたはAndroidで簡単にネイティブに実行できる最新のWebアプリを構築するためのクロスプラットフォームのネイティブランタイムです。PWAサポート、コンデンサCLI、すばらしいプラグインなどの優れた機能を提供します。
1 —新しいAngularアプリケーションを生成します
簡単にするために、CLIから生成されたAngularプロジェクトを変換するだけです。
ng new angular-mobile-app
上記のCLIコマンドは、箱から出して新しいAngularアプリケーションを作成して初期化します。
生成が終了したら、プロジェクトディレクトリに移動し、次のコマンドを入力して開発サーバーを起動します。
cd angular-mobile-app
ng serve
ここで、ブラウザに移動してhttp:// localhost:4200と入力します。すべてがうまくいった場合は、以下のページが表示されます。
localhost:4200で実行されているAngularアプリ
2 —プロジェクトにコンデンサを追加します
Webアプリが起動して実行されたので、次のステップは、次のnpmコマンドを実行して、依存関係@capacitor/core
と依存関係の両方をインストールすることです。@capacitor/cli
cd angular-mobile-app
npm install @capacitor/core
npm install @capacitor/cli
3 —コンデンサ設定ファイルを設定します
コンデンサの依存関係をインストールした後、次のコマンドを実行してcapacitor.config.ts
ファイルを生成します。
npx cap init
CLIは、アプリに関するいくつかの一般的な質問をします。今のところ、すべてをデフォルトのままにしておきます。
生成されたファイルは次のようになります。
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'angular-mobile-app',
webDir: 'dist/angular-mobile-app',
bundledWebRuntime: false
};
export default config;
コンデンサ.config.ts
appId
:パッケージ化されたアプリの一意の識別子。appName
:アプリの名前。webDir
:コンパイルされたWebアセットのディレクトリ。bundledWebRuntime
: Capacitorランタイムバンドルをコピーするかどうか。注:これはAngularアプリの通常のビルドパスであるため、必ずwebDir
toの値を変更してください。dist/(your app name)
これで、コンデンサはWebアセットを見つけることができるようになります。
4 —ネイティブiOSおよびAndroidパッケージを追加します
次のステップは、iOSおよびAndroidのパッケージに従ってインストールし、以下のコマンドを実行してネイティブプロジェクトを追加することです。
npm install @capacitor/ios @capacitor/androidnpx cap add iosnpx cap add android
上記のCLIコマンドを実行すると、AndroidとiOSの両方用に作成された2つの新しいフォルダーが表示されます。
5 —アプリを作成する
次に、Angularアプリをビルドし、以下のコマンドを実行して、ネイティブのAndroid/iOSプロジェクトへの変更を同期しましょう。
ng build --prodnpx cap sync
npx cap sync
iOS/Androidプロジェクトのビルドフォルダをコピーします。
6 —AndroidStudioまたはXcodeを開きます
プロジェクトで次のコマンドを使用して、AndroidStudioまたはXcodeでモバイルプロジェクトを直接開くことができます。
npx cap open ios
npx cap open android
注:上記のコマンドを実行する前に、まず環境をセットアップしてください。
Now the next step is to use Android Studio or Xcode to deploy your app directly to either a connected device or a virtual device.
In my case, I’m using Android Studio. As you can see in the image below, I’m selecting a Pixel 4 emulator.
android studio
Finally, click the run or debug button to run your app.
7 — Run the app!
app running on a virtual android device
Wrapping up
As you can see, Capacitor is very powerful at converting an existing angular application to a native mobile app.
Having one codebase for multiple platforms is the ultimate skill to have these days in order for us to not only save time and money but also to boost productivity. Enjoy!
Thanks for reading and see you in the next one!
1653379200
パフォーマンスは、世の中に出回っているほとんどのソフトウェア製品の機能ですが、他の製品よりもパフォーマンスに敏感なプログラムはほとんどありません。私はAndroidカメラアプリに取り組んでおり、私のチームはパフォーマンスを非常に優先しています。
[Googleが見つけた]10件の結果を含むページの生成には0.4秒かかりました。結果が30のページは0.9秒かかりました。0.5秒の遅延により、トラフィックが20%減少しました。0.5秒の遅延により、ユーザーの満足度が低下しました。
performance
マネージャーやあなたにとって重要性を売りすぎる必要はありませんが、パフォーマンスを強化するこのスニペットは機能構成であることに気づきました。事実を知っておくのは良いことです。
カメラでキャプチャした大きな画像やデバイス上の既存の画像を処理するアプリケーションを作成する場合は、特に注意する必要があります。最近の携帯電話のカメラには、高解像度センサーが簡単に搭載されています。現在Androidデバイスに搭載されている13MP(MegaPixels)、24MP、48MP、さらには108MPのカメラを簡単に見つけることができます。
13MPの画像を見てみましょう。13,000,000ピクセルです。画像に対して1つの簡単な計算を実行したい場合は、たとえば、すべてのピクセルに対して画像の露出を増やします。
image(x, y) = std::clamp(alpha * image(x, y) + beta, 0, 255);
あなたはそれを1300万回行う必要があります。
最近のスマートフォンにはマルチコアのSIMD対応CPUも搭載されているため、シリアル化された1,300万回の反復よりも高速に実行する方法がありますが、同時に、実行するアルゴリズムのタイプは通常、私よりもはるかに複雑です。ちょうど述べた。
私の経験では、これらの複雑な画像処理操作をネイティブコードで処理する方が簡単であり、特にパフォーマンスを維持するために優れています。
これは、Androidでネイティブコードを使用して画像処理を行う方法を示す非常に基本的な記事です。また、非常に単純で最適化されていないC ++コードのパフォーマンスが、同じ問題ステートメントに対してかなり最適化されたJavaコードに非常に近いことを例で示します。「AndroidでJavaNativeInterfaceまたはJNIを使用した高速画像処理」をお探しの場合—適切な場所に来たと思いますが、この記事は同じように役立ちます。
問題ステートメントの例:YUVからRGBへの変換
問題の説明は、 1つの平面チャネルと2つの半平面サブサンプリングチャネルを持つYUV_420_888と呼ばれる特定の形式の8MP(3264x2448)画像を、Androidのビットマップで一般的にサポートされているARGB_8888形式に変換することです。ウィキペディアでYUVフォーマットの詳細を読むことができます。また、以下の記事には、問題の説明についてのより適切な説明があります。YUV
これを問題ステートメントとして選択した理由はYUV_420_888
、Android Camera APIでサポートされている最も一般的なOUTPUT形式の1つであり、画像はBitmap
Androidと同様に一般的に使用されるため、これを対処するためのかなり一般的な問題ステートメントにします。
私は、Androidでの画像処理のパフォーマンスを理解するために、さまざまなフレームワークやテクノロジーのパフォーマンスを実験してきました。これを問題の説明として取り上げています。これが私がテストした他のテクニックを使った同じもののいくつかの例です:
さまざまなアプローチのパフォーマンスを説明する完全なシリーズ記事を書く予定です。これまでに公開されたアプローチの数値は次のとおりです。
8MP (3264x2448)
表1: Pixel4aデバイスでYUV画像をビットマップに変換する場合のパフォーマンスレイテンシ。
ネイティブコードによる画像処理
ネイティブスペースで直接操作している場合、基本的なスケルトンプログラムは次のようになります。
struct RGBA_8888 {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a = 255;
};
RGBA_8888 yuv2rgb(uint8_t y, uint8_t u, uint8_t v) {
int r = y + (1.370705 * (v - 128));
int g = y - (0.698001 * (v - 128)) - (0.337633 * (u - 128));;
int b = y + (1.732446 * (u - 128));
r = clamp(r, 0, 255);
g = clamp(g, 0, 255);
b = clamp(b, 0, 255);
return {.r = r, ,g = g, .b = b};
}
この関数は、特定のピクセルのチャネル値を取り、対応する値を返しy
ます。uvRGBA_8888
AndroidプログラムはデフォルトでJavaまたはKotlin言語で記述されていますが、AndroidツールチェーンにはAndroid NDKと呼ばれるネイティブ開発キットが付属しており、CやC++などの言語を使用してアプリのセクションを実装できます。Androidのドキュメントでは、NDKが次の2つのシナリオで役立つと説明されています。
デバイスから余分なパフォーマンスを引き出して、低遅延を実現したり、ゲームや物理シミュレーションなどの計算量の多いアプリケーションを実行したりします。
自分または他の開発者のCまたはC++ライブラリを再利用します。
次のいくつかのセクションでは、ネイティブコード統合を使用してYUV_420_888形式のJavaイメージをJavaオブジェクトビットマップに変換する方法を簡単に説明します。
したがって、基本的に、このスケルトンJava関数を入力する必要があります。
Bitmap toBitmap(Image image) {
if (image.getFormat() != ImageFormat.YUV_420_888) {
throw new IllegalArgumentException("Invalid image format");
}
// Do some magic here
throw new Exception("Not yet implemented");
}
深く掘り下げる前に、JNIを紹介したいと思います。
JNI
JNIは、JavaNativeInterfaceの略です。これは、JavaまたはKotlinコードからコンパイルされたバイトコードがCまたはC++で記述されたネイティブコードと対話する方法を定義します。名前が示すように、Javaコードをネイティブコードとインターフェースするのに役立ちます。
JNIをさらに活用することになった場合は、 JNIに関するAndroidのヒント—JNIのヒントを読むことをお勧めします。
NDKとJNIの使用を開始する
冗長性を回避するために、次の記事をチェックして試して、最初のJNIベースのAndroidアプリをセットアップすることをお勧めします。これらの記事は、AndroidアプリをNDKおよびネイティブコードとともにコンパイルするために必要なツールチェーンを設定するのにも役立ちます。
これらの記事が十分に明確でない場合は、コメントで知らせてください。JNIアプリを正常に作成して実行した場合、または構成をすでに認識している場合は、次のセクションに進んでください。
YUVからビットマップへの変換用のネイティブコード
にライブラリとソースファイルyuv2rgb.h/cc
を作成しますapp/src/main/cpp
。
namespace MyProject {
// Converts the YUV image with the given properties to ARGB image and write to
// `argb_output` destination.
void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
int y_row_stride, int uv_row_stride, int* argb_output);
}
#include "yuv2rgb.h"
namespace MyProject {
void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
int y_row_stride, int uv_row_stride, int* argb_output) {
uint32_t a = (255u << 24);
uint8_t r, g, b;
int16_t y_val, u_val, v_val;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
// Y plane should have positive values belonging to [0...255]
int y_idx = (y * y_row_stride) + (x * y_pixel_stride);
y_val = static_cast<int16_t>(y_buffer[y_idx]);
int uvx = x / 2;
int uvy = y / 2;
// U/V Values are sub-sampled i.e. each pixel in U/V channel in a
// YUV_420 image act as chroma value for 4 neighbouring pixels
int uv_idx = (uvy * uv_row_stride) + (uvx * uv_pixel_stride);
u_val = static_cast<int16_t>(u_buffer[uv_idx]) - 128;
v_val = static_cast<int16_t>(v_buffer[uv_idx]) - 128;
// Compute RGB values per formula above.
r = y_val + 1.370705f * v_val;
g = y_val - (0.698001f * v_val) - (0.337633f * u_val);
b = y_val + 1.732446f * u_val;
int argb_idx = y * width + x;
argb_output[argb_idx] = a | r << 16 | g << 8 | b;
}
}
}
}
理想的には、このための単体テストも作成する必要がありますが、それはこの記事の範囲外です。ネイティブテストの追加について詳しくは、Androidのドキュメントをご覧ください。
次に、最初に使用したJavaスケルトンに接続するためのJNIレイヤーを作成します。
Java+JNI統合
次に、Javaライブラリをネイティブライブラリに接続するためのJNIファイルが必要です。yuv2rgb-jni.cc
を追加しましょうapp/src/main/cpp
。
com.example.myproject
また、Javaスケルトン関数がパッケージ内にあり、という静的クラスの下にあると仮定するとYuvConvertor
、JNIファイルは次のようになります。
重要な注意:パッケージ名、クラス名、およびネイティブメソッド名は、ランタイムが適切なネイティブ関数を呼び出すために使用するため、ここでは重要です。詳細なコンテキストを取得するには、以下のJNIコードのメソッド名を参照してください。
#include <jni.h>
#include <android/log.h>
#include "yuv2rgb.h"
extern "C" {
jboolean
Java_com_example_myproject_YuvConvertor_yuv420toArgbNative(
JNIEnv* env, jclass clazz, jint width, jint height, jobject y_byte_buffer,
jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride,
jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride,
jintArray argb_array) {
auto y_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(y_byte_buffer));
auto u_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(u_byte_buffer));
auto v_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(v_byte_buffer));
jint* argb_result_array = env->GetIntArrayElements(argb_array, nullptr);
if (argb_result_array == nullptr || y_buffer == nullptr || u_buffer == nullptr
|| v_buffer == nullptr) {
__android_log_print(ANDROID_LOG_ERROR, PRIVATE_TAG,
"[yuv420toArgbNative] One or more inputs are null.");
return false;
}
Yuv2Rgb(width, height, reinterpret_cast<const uint8_t*>(y_buffer),
reinterpret_cast<const uint8_t*>(u_buffer),
reinterpret_cast<const uint8_t*>(v_buffer),
y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride,
argb_result_array);
return true;
}
}
そして最後に、これをJavaライブラリから呼び出します
package com.example.myproject;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.ImageFormat;
import android.media.Image;
public class YuvConvertor {
private YuvConvertor() {}
static {
// define this in CMakeLists.txt file.
System.loadLibrary("yuv2rgb-lib");
}
public Bitmap toBitmap(Image image) throws Exception {
if (image.getFormat() != ImageFormat.YUV_420_888) {
throw new IllegalArgumentException("Invalid image format");
}
int[] argbOutput = new int[image.getWidth() * image.getHeight()];
if (!yuv420toArgbNative(
image.getWidth(),
image.getHeight(),
image.getPlanes()[0].getBuffer(), // Y buffer
image.getPlanes()[1].getBuffer(), // U buffer
image.getPlanes()[2].getBuffer(), // V buffer
image.getPlanes()[0].getPixelStride(), // Y pixel stride
image.getPlanes()[1].getPixelStride(), // U/V pixel stride
image.getPlanes()[0].getRowStride(), // Y row stride
image.getPlanes()[1].getRowStride(), // U/V row stride
argbOutput)) {
// Handle this based on your usecase.
throw new Exception("Failed to convert YUV to Bitmap");
}
return Bitmap.createBitmap(
argbOutput, image.getWidth(), image.getHeight(), Config.ARGB_8888);
}
private static native boolean yuv420toArgbNative(
int width,
int height,
ByteBuffer yByteBuffer,
ByteBuffer uByteBuffer,
ByteBuffer vByteBuffer,
int yPixelStride,
int uvPixelStride,
int yRowStride,
int uvRowStride,
int[] argbOutput);
}
ここで注意すべきいくつかの重要なポイントは次のとおりです。
これにより、JNIを呼び出すJavaコードが作成されます。これにより、入力データと出力データへのポインターがネイティブ形式で取得され、ネイティブライブラリに渡されて処理されます。ネイティブライブラリはかなり一般的なC++コードであり、Androidの外部でも使用できます。
makeファイル(app/src/main/cpp/CMakeLists.txt
この場合はこの場合)は、AndroidAPKを使用したネイティブコードのビルドをサポートするように適切に構成されている必要があります。この例では、少なくともこれらの定義が必要です。
# Sets the minimum version of CMake required to build the native
# library.
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_CXX_FLAGS "-Wall -Wextra ${CMAKE_CXX_FLAGS}")
find_library(log-lib
log )
add_library(
yuv2rgb-lib
SHARED
yuv2rgb-jni.cc
yuv2rgb.cc)
target_link_libraries( # Specifies the target library.
yuv2rgb-lib
-O3
# Links the target library to the log library included in the NDK.
${log-lib} )
include(AndroidNdkModules)
パフォーマンス
パフォーマンスを見るとき、これはC ++コードのかなり単純な形式であり、Androidデバイスで実行できるマルチスレッドまたはSIMD命令セットを明示的に利用していないことに注意してください。せいぜい、コードの一部はコンパイラによって最適化されます(たとえば、メインのforループは自動ベクトル化される可能性があります)。-O3
コードは最適化フラグを使用してコンパイルされました。
8MP
画像(3264x2448)の場合、このコードは76.4ms
同じ参照デバイスで実行されます。
8MP (3264x2448)
表2: Pixel4aデバイスでYUV画像をビットマップに変換する場合のパフォーマンスレイテンシ。
ここで、このバージョンのネイティブコードのパフォーマンスは、非常に最適化されたマルチスレッドJavaコードよりも1.4倍遅いことがわかります。これは、悪いニュースではありません。スターターJavaコードは353 ms
、同じアルゴリズムを実行するために必要でした(ただし、複合体ByteBuffer
が直接配列アクセスを提供しない理由がある可能性があります-続きを読む)。より最適化されたネイティブコードの記述の詳細について、またはコンパイラを言語構造に利用する方法については、この記事を読むことをお勧めします—C++コンパイラをガイドしてコードを自動ベクトル化する
クロージングノート
ネイティブコードは、Androidで計算量の多いアルゴリズムを実行するのに最適であると再度主張します。また、ほとんどの画像処理アルゴリズムは通常、このカテゴリに分類されます。これは、多数のピクセルを処理する必要があるため、したがって多数の反復を処理する必要があるためと考えられます。
この同じ一連の実験では、マルチスレッド、NEON API(SIMD)、およびいくつかのアセンブリコードを利用すること12.1 ms
で、このシリーズの勝者であるレイテンシーを取得することができました(スポイラーアラート)。ただし、書き込みと保守の両方を行うのはかなり複雑であり、同じ問題ステートメントを処理するHalide
ベースのソリューション28ms
は、パフォーマンス、保守、および書き込みの容易さの点で完璧なソリューションですらあります。
読んでくれてありがとう、次の一連の記事でこれらの両方について書きます。乞うご期待!!
ソース:https ://betterprogramming.pub/processing-images-fast-with-native-code-in-android-db8b21001fa9
1653379200
El rendimiento es una característica de la mayoría de los productos de software que existen, pero hay pocos programas que sean más sensibles al rendimiento que los demás. Trabajo en una aplicación de cámara de Android y mi equipo le da una prioridad muy alta al rendimiento.
[Google descubrió que] la página con 10 resultados tardó 0,4 segundos en generarse. La página con 30 resultados tardó 0,9 segundos. Medio segundo de retraso provocó una caída del 20% en el tráfico. Medio segundo de retraso mató la satisfacción del usuario.
No necesito exagerar la importancia de performance
mi gerente o de usted, pero me encontré con este fragmento que fortalece el rendimiento es una construcción característica; es bueno saberlo.
Si está escribiendo aplicaciones que procesan imágenes grandes capturadas con una cámara o una imagen existente en el dispositivo, debe tener mucho cuidado. En estos días, las cámaras de los teléfonos se equipan fácilmente con sensores de alta resolución. Es fácil encontrar cámaras de 13 MP (megapíxeles), 24 MP, 48 MP o incluso 108 MP que ahora se envían en dispositivos Android.
Veamos una imagen de 13MP. Tiene 13.000.000 píxeles. Si quisiera hacer solo un cálculo simple en la imagen, digamos aumentar la exposición de la imagen, es decir, para cada píxel
image(x, y) = std::clamp(alpha * image(x, y) + beta, 0, 255);
Necesitas hacerlo 13 millones de veces.
Los teléfonos inteligentes en estos días también están equipados con CPU multinúcleo compatibles con SIMD, por lo que hay formas de hacerlo más rápido que serializar 13 millones de iteraciones pero, al mismo tiempo, los tipos de algoritmos que queremos ejecutar suelen ser mucho más complejos que el que yo recién dicho.
En mi experiencia, es más fácil y mejor manejar estas complejas operaciones de procesamiento de imágenes con código nativo, muy particularmente para mantener su rendimiento.
Este es un artículo muy básico que demuestra cómo procesar imágenes con código nativo en Android. También mostraré con un ejemplo que el rendimiento de un código C++ muy simple y no optimizado se acerca mucho al código Java bastante optimizado para la misma declaración del problema. Si está buscando "Procesamiento rápido de imágenes usando la interfaz nativa de Java o JNI en Android", creo que ha venido al lugar correcto, este artículo lo ayudará de la misma manera.
Ejemplo de declaración del problema: conversión de YUV a RGB
La declaración del problema es convertir una imagen de 8MP (3264x2448) en un formato determinado llamado YUV_420_888 que tiene un Y
canal planar y dos UV
canales submuestreados semiplanares al formato ARGB_8888 que comúnmente es compatible con Bitmap en Android. Puede leer más sobre el formato YUV en Wikipedia . Además, los artículos a continuación tienen una mejor descripción de la declaración del problema.
La razón por la que elegí esto como la declaración del problema es porque YUV_420_888
es uno de los formatos de SALIDA más comunes compatibles con las API de la cámara de Android y las imágenes se consumen comúnmente como Bitmap
en Android, lo que hace que esta sea una declaración del problema bastante común para abordar.
He estado experimentando con el rendimiento de diferentes marcos o tecnologías para comprender el rendimiento del procesamiento de imágenes en Android tomando esto como el enunciado del problema. Aquí hay algunos ejemplos de lo mismo usando otras técnicas que he probado:
Planeo escribir una serie completa de artículos que explique el desempeño de diferentes enfoques. Aquí están los números de los enfoques publicados hasta ahora:
Tabla 1: Latencia de rendimiento de convertir una 8MP (3264x2448)
imagen YUV a mapa de bits en un dispositivo Pixel 4a .
Procesamiento de imágenes con código nativo
Si estuviéramos operando en el espacio nativo directamente, el programa básico básico se vería así:
struct RGBA_8888 {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a = 255;
};
RGBA_8888 yuv2rgb(uint8_t y, uint8_t u, uint8_t v) {
int r = y + (1.370705 * (v - 128));
int g = y - (0.698001 * (v - 128)) - (0.337633 * (u - 128));;
int b = y + (1.732446 * (u - 128));
r = clamp(r, 0, 255);
g = clamp(g, 0, 255);
b = clamp(b, 0, 255);
return {.r = r, ,g = g, .b = b};
}
La función toma y
y canaliza el valor de un determinado píxel u
y v
devuelve el RGBA_8888
valor correspondiente.
Los programas de Android están escritos de manera predeterminada en lenguaje Java o Kotlin, pero las cadenas de herramientas de Android vienen con un kit de desarrollo nativo llamado NDK de Android que le permite implementar secciones de su aplicación usando lenguajes como C y C++. La documentación de Android describe que el NDK es útil para dos escenarios:
Exprime el rendimiento adicional de un dispositivo para lograr una baja latencia o ejecutar aplicaciones de computación intensiva, como juegos o simulaciones de física.
Reutilice sus propias bibliotecas C o C++ o las de otros desarrolladores.
En las próximas secciones, intentaré explicar brevemente cómo convertir una imagen de Java en formato YUV_420_888 a un objeto de mapa de bits de Java utilizando la integración de código nativo.
Básicamente, tenemos que completar esta función básica de Java:
Bitmap toBitmap(Image image) {
if (image.getFormat() != ImageFormat.YUV_420_888) {
throw new IllegalArgumentException("Invalid image format");
}
// Do some magic here
throw new Exception("Not yet implemented");
}
Antes de profundizar, me gustaría presentarles JNI.
JNI
JNI significa Interfaz nativa de Java. Define un código de bytes de forma compilado a partir de código Java o Kotlin para interactuar con código nativo escrito en C o C++. Como su nombre lo dice, nos ayuda a conectar el código Java con el código nativo.
Si termina trabajando más con JNI, le recomiendo leer los consejos de Android sobre JNI — Consejos de JNI .
Primeros pasos con NDK y JNI
Para evitar la redundancia, recomiendo consultar y probar los siguientes artículos para configurar su primera aplicación de Android basada en JNI. Estos artículos también lo ayudarán a configurar las cadenas de herramientas necesarias para compilar aplicaciones de Android junto con NDK y código nativo.
Déjame saber sobre los comentarios si estos artículos no son lo suficientemente claros. Continúe con la siguiente sección si ha creado y ejecutado con éxito su aplicación JNI o si ya conoce las construcciones.
Código nativo para la conversión de YUV a mapa de bits
Cree una biblioteca y un archivo fuente yuv2rgb.h/cc
en formato app/src/main/cpp
.
namespace MyProject {
// Converts the YUV image with the given properties to ARGB image and write to
// `argb_output` destination.
void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
int y_row_stride, int uv_row_stride, int* argb_output);
}
#include "yuv2rgb.h"
namespace MyProject {
void Yuv2Rgb(int width, int height, const uint8_t* y_buffer, const uint8_t* u_buffer,
const uint8_t* v_buffer, int y_pixel_stride, int uv_pixel_stride,
int y_row_stride, int uv_row_stride, int* argb_output) {
uint32_t a = (255u << 24);
uint8_t r, g, b;
int16_t y_val, u_val, v_val;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
// Y plane should have positive values belonging to [0...255]
int y_idx = (y * y_row_stride) + (x * y_pixel_stride);
y_val = static_cast<int16_t>(y_buffer[y_idx]);
int uvx = x / 2;
int uvy = y / 2;
// U/V Values are sub-sampled i.e. each pixel in U/V channel in a
// YUV_420 image act as chroma value for 4 neighbouring pixels
int uv_idx = (uvy * uv_row_stride) + (uvx * uv_pixel_stride);
u_val = static_cast<int16_t>(u_buffer[uv_idx]) - 128;
v_val = static_cast<int16_t>(v_buffer[uv_idx]) - 128;
// Compute RGB values per formula above.
r = y_val + 1.370705f * v_val;
g = y_val - (0.698001f * v_val) - (0.337633f * u_val);
b = y_val + 1.732446f * u_val;
int argb_idx = y * width + x;
argb_output[argb_idx] = a | r << 16 | g << 8 | b;
}
}
}
}
Idealmente, también debería escribir una prueba unitaria para esto, pero eso está fuera del alcance de este artículo. Puede obtener más información sobre cómo agregar pruebas nativas en la documentación de Android .
A continuación, escribiremos una capa JNI para conectarla con el esqueleto de Java con el que comenzamos.
Integración Java + JNI
Ahora necesita tener un archivo JNI para conectar la biblioteca Java a la biblioteca nativa. Agreguemos . yuv2rgb-jni.cc
_app/src/main/cpp
Además, supongamos que nuestra función básica de Java está en el paquete com.example.myproject
y bajo una clase estática llamada YuvConvertor
, su archivo JNI debería tener este aspecto.
Nota importante : el nombre del paquete, el nombre de la clase y el nombre del método nativo son importantes aquí, ya que el tiempo de ejecución los utiliza para llamar a la función nativa correcta. Consulte el nombre del método en el código JNI a continuación para obtener más contexto.
#include <jni.h>
#include <android/log.h>
#include "yuv2rgb.h"
extern "C" {
jboolean
Java_com_example_myproject_YuvConvertor_yuv420toArgbNative(
JNIEnv* env, jclass clazz, jint width, jint height, jobject y_byte_buffer,
jobject u_byte_buffer, jobject v_byte_buffer, jint y_pixel_stride,
jint uv_pixel_stride, jint y_row_stride, jint uv_row_stride,
jintArray argb_array) {
auto y_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(y_byte_buffer));
auto u_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(u_byte_buffer));
auto v_buffer = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(v_byte_buffer));
jint* argb_result_array = env->GetIntArrayElements(argb_array, nullptr);
if (argb_result_array == nullptr || y_buffer == nullptr || u_buffer == nullptr
|| v_buffer == nullptr) {
__android_log_print(ANDROID_LOG_ERROR, PRIVATE_TAG,
"[yuv420toArgbNative] One or more inputs are null.");
return false;
}
Yuv2Rgb(width, height, reinterpret_cast<const uint8_t*>(y_buffer),
reinterpret_cast<const uint8_t*>(u_buffer),
reinterpret_cast<const uint8_t*>(v_buffer),
y_pixel_stride, uv_pixel_stride, y_row_stride, uv_row_stride,
argb_result_array);
return true;
}
}
Y finalmente, llame a esto desde la biblioteca de Java
package com.example.myproject;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.ImageFormat;
import android.media.Image;
public class YuvConvertor {
private YuvConvertor() {}
static {
// define this in CMakeLists.txt file.
System.loadLibrary("yuv2rgb-lib");
}
public Bitmap toBitmap(Image image) throws Exception {
if (image.getFormat() != ImageFormat.YUV_420_888) {
throw new IllegalArgumentException("Invalid image format");
}
int[] argbOutput = new int[image.getWidth() * image.getHeight()];
if (!yuv420toArgbNative(
image.getWidth(),
image.getHeight(),
image.getPlanes()[0].getBuffer(), // Y buffer
image.getPlanes()[1].getBuffer(), // U buffer
image.getPlanes()[2].getBuffer(), // V buffer
image.getPlanes()[0].getPixelStride(), // Y pixel stride
image.getPlanes()[1].getPixelStride(), // U/V pixel stride
image.getPlanes()[0].getRowStride(), // Y row stride
image.getPlanes()[1].getRowStride(), // U/V row stride
argbOutput)) {
// Handle this based on your usecase.
throw new Exception("Failed to convert YUV to Bitmap");
}
return Bitmap.createBitmap(
argbOutput, image.getWidth(), image.getHeight(), Config.ARGB_8888);
}
private static native boolean yuv420toArgbNative(
int width,
int height,
ByteBuffer yByteBuffer,
ByteBuffer uByteBuffer,
ByteBuffer vByteBuffer,
int yPixelStride,
int uvPixelStride,
int yRowStride,
int uvRowStride,
int[] argbOutput);
}
Algunos puntos importantes a tener en cuenta aquí son:
Entonces, con esto, tiene su código Java llamando al JNI, que recupera el puntero a los datos de entrada y salida en formato nativo y lo pasa a la biblioteca nativa para su procesamiento. La biblioteca nativa es un código C++ bastante común y también se puede usar fuera de Android.
Su archivo de creación (debe estar en app/src/main/cpp/CMakeLists.txt
este caso) debe estar configurado correctamente para admitir la creación de código nativo con el APK de Android. Para este ejemplo, debería tener al menos estas definiciones.
# Sets the minimum version of CMake required to build the native
# library.
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_CXX_FLAGS "-Wall -Wextra ${CMAKE_CXX_FLAGS}")
find_library(log-lib
log )
add_library(
yuv2rgb-lib
SHARED
yuv2rgb-jni.cc
yuv2rgb.cc)
target_link_libraries( # Specifies the target library.
yuv2rgb-lib
-O3
# Links the target library to the log library included in the NDK.
${log-lib} )
include(AndroidNdkModules)
Actuación
Al observar el rendimiento, tenga en cuenta que esta es una forma bastante simple de código C++, no aprovecha explícitamente los conjuntos de instrucciones SIMD o de subprocesos múltiples que pueden ejecutarse en dispositivos Android. En el mejor de los casos, el compilador optimiza una parte del código (por ejemplo, el bucle for principal podría auto-vectorizarse). El código fue compilado con -O3
bandera de optimización.
Para una 8MP
imagen (3264x2448), este código ocupa aproximadamente 76.4ms
el mismo dispositivo de referencia.
Tabla 2: Latencia de rendimiento de convertir una 8MP (3264x2448)
imagen YUV a mapa de bits en un dispositivo Pixel 4a .
Puede ver aquí que el rendimiento de esta versión del código nativo es 1,4 veces más lento que el código Java multiproceso muy optimizado, lo cual no es una mala noticia. El código Java de inicio tomó 353 ms
para ejecutar el mismo algoritmo (aunque probablemente tenga razones en torno al complejo ByteBuffer
que no brinda acceso directo a la matriz; lea más ). Para obtener más información sobre cómo escribir un código nativo más optimizado o aprovechar el compilador para construcciones de lenguaje, recomiendo leer este artículo: guíe el compilador de C++ para vectorizar automáticamente el código.
Notas de cierre
Reafirmaría que el código nativo ha sido excelente para ejecutar algoritmos computacionalmente intensivos en Android. Y la mayoría de los algoritmos de procesamiento de imágenes generalmente se incluyen en esta categoría, probablemente debido a la necesidad de procesar una gran cantidad de píxeles y, por lo tanto, una gran cantidad de iteraciones.
En esta misma serie de experimentos, al aprovechar los subprocesos múltiples, las API de NEON (SIMD) y algunos códigos ensambladores, pude reducir la latencia a 12.1 ms
cuál es el ganador en esta serie (alerta de spoiler). Sin embargo, es bastante complicado escribirlo y mantenerlo, y una Halide
solución basada 28ms
en la misma declaración del problema es incluso la solución perfecta en términos de rendimiento, mantenimiento y facilidad de escritura.
Escribiré sobre ambos en mi próxima serie de artículos, gracias por leer. ¡¡Manténganse al tanto!!
Fuente: https://betterprogramming.pub/processing-images-fast-with-native-code-in-android-db8b21001fa9